UNPKG

@dfinity/identity

Version:

JavaScript and TypeScript library to manage identity with the Internet Computer

302 lines 12 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PartialDelegationIdentity = exports.DelegationIdentity = exports.DelegationChain = exports.Delegation = void 0; exports.isDelegationValid = isDelegationValid; const agent_1 = require("@dfinity/agent"); const principal_1 = require("@dfinity/principal"); const partial_ts_1 = require("./partial.js"); const utils_1 = require("@noble/hashes/utils"); /** * Safe wrapper around bytesToHex that handles ArrayBuffer/Uint8Array type conversion. * Required because @noble/hashes v1.8+ strictly expects Uint8Array inputs. * @param data The binary data to convert to hexadecimal string (ArrayBuffer, Uint8Array, or ArrayLike<number>) */ function safeBytesToHex(data) { if (data instanceof Uint8Array) { return (0, utils_1.bytesToHex)(data); } return (0, utils_1.bytesToHex)(new Uint8Array(data)); } function _parseBlob(value) { if (typeof value !== 'string' || value.length < 64) { throw new Error('Invalid public key.'); } return (0, utils_1.hexToBytes)(value); } /** * A single delegation object that is signed by a private key. This is constructed by * `DelegationChain.create()`. * * {@see DelegationChain} */ class Delegation { constructor(pubkey, expiration, targets) { this.pubkey = pubkey; this.expiration = expiration; this.targets = targets; } toCborValue() { return { pubkey: this.pubkey, expiration: this.expiration, ...(this.targets && { targets: this.targets, }), }; } toJSON() { // every string should be hex and once-de-hexed, // discoverable what it is (e.g. de-hex to get JSON with a 'type' property, or de-hex to DER // with an OID). After de-hex, if it's not obvious what it is, it's an ArrayBuffer. return { expiration: this.expiration.toString(16), pubkey: safeBytesToHex(this.pubkey), ...(this.targets && { targets: this.targets.map(p => p.toHex()) }), }; } } exports.Delegation = Delegation; /** * Sign a single delegation object for a period of time. * @param from The identity that lends its delegation. * @param to The identity that receives the delegation. * @param expiration An expiration date for this delegation. * @param targets Limit this delegation to the target principals. */ async function _createSingleDelegation(from, to, expiration, targets) { const delegation = new Delegation(to.toDer(), BigInt(+expiration) * BigInt(1000000), // In nanoseconds. targets); // The signature is calculated by signing the concatenation of the domain separator // and the message. // Note: To ensure Safari treats this as a user gesture, ensure to not use async methods // besides the actualy webauthn functionality (such as `sign`). Safari will de-register // a user gesture if you await an async call thats not fetch, xhr, or setTimeout. const challenge = new Uint8Array([ ...agent_1.IC_REQUEST_AUTH_DELEGATION_DOMAIN_SEPARATOR, ...new Uint8Array((0, agent_1.requestIdOf)({ ...delegation })), ]); const signature = await from.sign(challenge); return { delegation, signature, }; } /** * A chain of delegations. This is JSON Serializable. * This is the object to serialize and pass to a DelegationIdentity. It does not keep any * private keys. */ class DelegationChain { /** * Create a delegation chain between two (or more) keys. By default, the expiration time * will be very short (15 minutes). * * To build a chain of more than 2 identities, this function needs to be called multiple times, * passing the previous delegation chain into the options argument. For example: * @example * const rootKey = createKey(); * const middleKey = createKey(); * const bottomeKey = createKey(); * * const rootToMiddle = await DelegationChain.create( * root, middle.getPublicKey(), Date.parse('2100-01-01'), * ); * const middleToBottom = await DelegationChain.create( * middle, bottom.getPublicKey(), Date.parse('2100-01-01'), { previous: rootToMiddle }, * ); * * // We can now use a delegation identity that uses the delegation above: * const identity = DelegationIdentity.fromDelegation(bottomKey, middleToBottom); * @param from The identity that will delegate. * @param to The identity that gets delegated. It can now sign messages as if it was the * identity above. * @param expiration The length the delegation is valid. By default, 15 minutes from calling * this function. * @param options A set of options for this delegation. expiration and previous * @param options.previous - Another DelegationChain that this chain should start with. * @param options.targets - targets that scope the delegation (e.g. Canister Principals) */ static async create(from, to, expiration = new Date(Date.now() + 15 * 60 * 1000), options = {}) { const delegation = await _createSingleDelegation(from, to, expiration, options.targets); return new DelegationChain([...(options.previous?.delegations || []), delegation], options.previous?.publicKey || from.getPublicKey().toDer()); } /** * Creates a DelegationChain object from a JSON string. * @param json The JSON string to parse. */ static fromJSON(json) { const { publicKey, delegations } = typeof json === 'string' ? JSON.parse(json) : json; if (!Array.isArray(delegations)) { throw new Error('Invalid delegations.'); } const parsedDelegations = delegations.map(signedDelegation => { const { delegation, signature } = signedDelegation; const { pubkey, expiration, targets } = delegation; if (targets !== undefined && !Array.isArray(targets)) { throw new Error('Invalid targets.'); } return { delegation: new Delegation(_parseBlob(pubkey), BigInt('0x' + expiration), // expiration in JSON is an hexa string (See toJSON() below). targets && targets.map((t) => { if (typeof t !== 'string') { throw new Error('Invalid target.'); } return principal_1.Principal.fromHex(t); })), signature: _parseBlob(signature), }; }); return new this(parsedDelegations, _parseBlob(publicKey)); } /** * Creates a DelegationChain object from a list of delegations and a DER-encoded public key. * @param delegations The list of delegations. * @param publicKey The DER-encoded public key of the key-pair signing the first delegation. */ static fromDelegations(delegations, publicKey) { return new this(delegations, publicKey); } constructor(delegations, publicKey) { this.delegations = delegations; this.publicKey = publicKey; } toJSON() { return { delegations: this.delegations.map(signedDelegation => { const { delegation, signature } = signedDelegation; const { targets } = delegation; return { delegation: { expiration: delegation.expiration.toString(16), pubkey: safeBytesToHex(delegation.pubkey), ...(targets && { targets: targets.map(t => t.toHex()), }), }, signature: safeBytesToHex(signature), }; }), publicKey: safeBytesToHex(this.publicKey), }; } } exports.DelegationChain = DelegationChain; /** * An Identity that adds delegation to a request. Everywhere in this class, the name * innerKey refers to the SignIdentity that is being used to sign the requests, while * originalKey is the identity that is being borrowed. More identities can be used * in the middle to delegate. */ class DelegationIdentity extends agent_1.SignIdentity { /** * Create a delegation without having access to delegateKey. * @param key The key used to sign the requests. * @param delegation A delegation object created using `createDelegation`. */ static fromDelegation(key, delegation) { return new this(key, delegation); } constructor(_inner, _delegation) { super(); this._inner = _inner; this._delegation = _delegation; } getDelegation() { return this._delegation; } getPublicKey() { return { derKey: this._delegation.publicKey, toDer: () => this._delegation.publicKey, }; } sign(blob) { return this._inner.sign(blob); } async transformRequest(request) { const { body, ...fields } = request; const requestId = await (0, agent_1.requestIdOf)(body); return { ...fields, body: { content: body, sender_sig: await this.sign(new Uint8Array([...agent_1.IC_REQUEST_DOMAIN_SEPARATOR, ...new Uint8Array(requestId)])), sender_delegation: this._delegation.delegations, sender_pubkey: this._delegation.publicKey, }, }; } } exports.DelegationIdentity = DelegationIdentity; /** * A partial delegated identity, representing a delegation chain and the public key that it targets */ class PartialDelegationIdentity extends partial_ts_1.PartialIdentity { #delegation; /** * The Delegation Chain of this identity. */ get delegation() { return this.#delegation; } constructor(inner, delegation) { super(inner); this.#delegation = delegation; } /** * Create a {@link PartialDelegationIdentity} from a {@link PublicKey} and a {@link DelegationChain}. * @param key The {@link PublicKey} to delegate to. * @param delegation a {@link DelegationChain} targeting the inner key. */ static fromDelegation(key, delegation) { return new PartialDelegationIdentity(key, delegation); } } exports.PartialDelegationIdentity = PartialDelegationIdentity; /** * Analyze a DelegationChain and validate that it's valid, ie. not expired and apply to the * scope. * @param chain The chain to validate. * @param checks Various checks to validate on the chain. */ function isDelegationValid(chain, checks) { // Verify that the no delegation is expired. If any are in the chain, returns false. for (const { delegation } of chain.delegations) { // prettier-ignore if (+new Date(Number(delegation.expiration / BigInt(1000000))) <= +Date.now()) { return false; } } // Check the scopes. const scopes = []; const maybeScope = checks?.scope; if (maybeScope) { if (Array.isArray(maybeScope)) { scopes.push(...maybeScope.map(s => (typeof s === 'string' ? principal_1.Principal.fromText(s) : s))); } else { scopes.push(typeof maybeScope === 'string' ? principal_1.Principal.fromText(maybeScope) : maybeScope); } } for (const s of scopes) { const scope = s.toText(); for (const { delegation } of chain.delegations) { if (delegation.targets === undefined) { continue; } let none = true; for (const target of delegation.targets) { if (target.toText() === scope) { none = false; break; } } if (none) { return false; } } } return true; } //# sourceMappingURL=delegation.js.map