UNPKG

@dfinity/identity

Version:

JavaScript and TypeScript library to manage identity with the Internet Computer

240 lines (208 loc) 7.85 kB
import { type DerEncodedPublicKey, type KeyPair, type PublicKey, type Signature, SignIdentity, ED25519_OID, unwrapDER, wrapDER, } from '@dfinity/agent'; import { uint8Equals, uint8FromBufLike } from '@dfinity/candid'; import { ed25519 } from '@noble/curves/ed25519'; import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; declare type KeyLike = PublicKey | DerEncodedPublicKey | ArrayBuffer | ArrayBufferView; function isObject(value: unknown) { return value !== null && typeof value === 'object'; } export class Ed25519PublicKey implements PublicKey { /** * Construct Ed25519PublicKey from an existing PublicKey * @param {unknown} maybeKey - existing PublicKey, ArrayBuffer, DerEncodedPublicKey, or hex string * @returns {Ed25519PublicKey} Instance of Ed25519PublicKey */ public static from(maybeKey: unknown): Ed25519PublicKey { if (typeof maybeKey === 'string') { const key = hexToBytes(maybeKey); return this.fromRaw(key); } else if (isObject(maybeKey)) { const key = maybeKey as KeyLike; if (isObject(key) && Object.hasOwnProperty.call(key, '__derEncodedPublicKey__')) { return this.fromDer(key as DerEncodedPublicKey); } else if (ArrayBuffer.isView(key)) { const view = key as ArrayBufferView; return this.fromRaw(uint8FromBufLike(view.buffer)); } else if (key instanceof ArrayBuffer) { return this.fromRaw(uint8FromBufLike(key)); } else if ('rawKey' in key && key.rawKey instanceof Uint8Array) { return this.fromRaw(key.rawKey); } else if ('derKey' in key) { return this.fromDer(key.derKey as DerEncodedPublicKey); } else if ('toDer' in key) { return this.fromDer(key.toDer()); } } throw new Error('Cannot construct Ed25519PublicKey from the provided key.'); } public static fromRaw(rawKey: Uint8Array): Ed25519PublicKey { return new Ed25519PublicKey(rawKey); } public static fromDer(derKey: DerEncodedPublicKey): Ed25519PublicKey { return new Ed25519PublicKey(this.derDecode(derKey)); } // The length of Ed25519 public keys is always 32 bytes. private static RAW_KEY_LENGTH = 32; private static derEncode(publicKey: Uint8Array): DerEncodedPublicKey { const key = wrapDER(publicKey, ED25519_OID) as DerEncodedPublicKey; key.__derEncodedPublicKey__ = undefined; return key; } private static derDecode(key: DerEncodedPublicKey): Uint8Array { const unwrapped = unwrapDER(key, ED25519_OID); if (unwrapped.length !== this.RAW_KEY_LENGTH) { throw new Error('An Ed25519 public key must be exactly 32bytes long'); } return unwrapped; } #rawKey: Uint8Array; public get rawKey(): Uint8Array { return this.#rawKey; } #derKey: DerEncodedPublicKey; public get derKey(): DerEncodedPublicKey { return this.#derKey; } // `fromRaw` and `fromDer` should be used for instantiation, not this constructor. private constructor(key: Uint8Array) { if (key.byteLength !== Ed25519PublicKey.RAW_KEY_LENGTH) { throw new Error('An Ed25519 public key must be exactly 32bytes long'); } this.#rawKey = key; this.#derKey = Ed25519PublicKey.derEncode(key); } public toDer(): DerEncodedPublicKey { return this.derKey; } public toRaw(): Uint8Array { return this.rawKey; } } /** * Ed25519KeyIdentity is an implementation of SignIdentity that uses Ed25519 keys. This class is used to sign and verify messages for an agent. */ export class Ed25519KeyIdentity extends SignIdentity { /** * Generate a new Ed25519KeyIdentity. * @param seed a 32-byte seed for the private key. If not provided, a random seed will be generated. * @returns Ed25519KeyIdentity */ public static generate(seed?: Uint8Array): Ed25519KeyIdentity { if (seed && seed.length !== 32) { throw new Error('Ed25519 Seed needs to be 32 bytes long.'); } if (!seed) seed = ed25519.utils.randomPrivateKey(); // Check if the seed is all zeros if (uint8Equals(seed, new Uint8Array(new Array(32).fill(0)))) { console.warn( 'Seed is all zeros. This is not a secure seed. Please provide a seed with sufficient entropy if this is a production environment.', ); } const sk = new Uint8Array(32); for (let i = 0; i < 32; i++) { sk[i] = seed[i]; } const pk = ed25519.getPublicKey(sk); return Ed25519KeyIdentity.fromKeyPair(pk, sk); } public static fromParsedJson(obj: JsonnableEd25519KeyIdentity): Ed25519KeyIdentity { const [publicKeyDer, privateKeyRaw] = obj; return new Ed25519KeyIdentity( Ed25519PublicKey.fromDer(hexToBytes(publicKeyDer) as DerEncodedPublicKey), hexToBytes(privateKeyRaw), ); } public static fromJSON(json: string): Ed25519KeyIdentity { const parsed = JSON.parse(json); if (Array.isArray(parsed)) { if (typeof parsed[0] === 'string' && typeof parsed[1] === 'string') { return this.fromParsedJson([parsed[0], parsed[1]]); } else { throw new Error('Deserialization error: JSON must have at least 2 items.'); } } throw new Error(`Deserialization error: Invalid JSON type for string: ${JSON.stringify(json)}`); } public static fromKeyPair(publicKey: Uint8Array, privateKey: Uint8Array): Ed25519KeyIdentity { return new Ed25519KeyIdentity(Ed25519PublicKey.fromRaw(publicKey), privateKey); } public static fromSecretKey(secretKey: Uint8Array): Ed25519KeyIdentity { const publicKey = ed25519.getPublicKey(secretKey); return Ed25519KeyIdentity.fromKeyPair(publicKey, secretKey); } #publicKey: Ed25519PublicKey; #privateKey: Uint8Array; // `fromRaw` and `fromDer` should be used for instantiation, not this constructor. protected constructor(publicKey: PublicKey, privateKey: Uint8Array) { super(); this.#publicKey = Ed25519PublicKey.from(publicKey); this.#privateKey = privateKey; } /** * Serialize this key to JSON. */ public toJSON(): JsonnableEd25519KeyIdentity { return [bytesToHex(this.#publicKey.toDer()), bytesToHex(this.#privateKey)]; } /** * Return a copy of the key pair. */ public getKeyPair(): KeyPair { return { secretKey: this.#privateKey, publicKey: this.#publicKey, }; } /** * Return the public key. */ public getPublicKey(): Required<PublicKey> { return this.#publicKey; } /** * Signs a blob of data, with this identity's private key. * @param challenge - challenge to sign with this identity's secretKey, producing a signature */ public async sign(challenge: Uint8Array): Promise<Signature> { // Some implementations of Ed25519 private keys append a public key to the end of the private key. We only want the private key. const signature = ed25519.sign(challenge, this.#privateKey.slice(0, 32)); // add { __signature__: void; } to the signature to make it compatible with the agent Object.defineProperty(signature, '__signature__', { enumerable: false, value: undefined, }); return signature as Signature; } /** * Verify * @param sig - signature to verify * @param msg - message to verify * @param pk - public key * @returns - true if the signature is valid, false otherwise */ public static verify( sig: ArrayBuffer | Uint8Array | string, msg: ArrayBuffer | Uint8Array | string, pk: ArrayBuffer | Uint8Array | string, ) { const [signature, message, publicKey] = [sig, msg, pk].map(x => { if (typeof x === 'string') { x = hexToBytes(x); } return uint8FromBufLike(x); }); return ed25519.verify(signature, message, publicKey); } } type PublicKeyHex = string; type SecretKeyHex = string; export type JsonnableEd25519KeyIdentity = [PublicKeyHex, SecretKeyHex];