diff --git a/packages/agent/src/auth.ts b/packages/agent/src/auth.ts index d4ed8674..9c4c57d0 100644 --- a/packages/agent/src/auth.ts +++ b/packages/agent/src/auth.ts @@ -28,7 +28,8 @@ export type Signature = ArrayBuffer & { __signature__: void }; */ export interface PublicKey { toDer(): DerEncodedPublicKey; - // rawKey and derKey are optional for backwards compatibility. + // rawKey, toRaw, and derKey are optional for backwards compatibility. + toRaw?(): ArrayBuffer; rawKey?: ArrayBuffer; derKey?: DerEncodedPublicKey; } diff --git a/packages/agent/src/utils/buffer.ts b/packages/agent/src/utils/buffer.ts index 6e339231..0792d0ce 100644 --- a/packages/agent/src/utils/buffer.ts +++ b/packages/agent/src/utils/buffer.ts @@ -20,7 +20,7 @@ export function toHex(buffer: ArrayBuffer): string { return [...new Uint8Array(buffer)].map(x => x.toString(16).padStart(2, '0')).join(''); } -const hexRe = new RegExp(/^([0-9A-F]{2})*$/i); +const hexRe = new RegExp(/^[0-9a-fA-F]+$/); /** * Transforms a hexadecimal string into an array buffer. diff --git a/packages/identity-secp256k1/src/buffer.ts b/packages/identity-secp256k1/src/buffer.ts deleted file mode 100644 index 37366061..00000000 --- a/packages/identity-secp256k1/src/buffer.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Return an array buffer from its hexadecimal representation. - * @param hexString The hexadecimal string. - */ -export function fromHexString(hexString: string): ArrayBuffer { - return new Uint8Array((hexString.match(/.{1,2}/g) ?? []).map(byte => parseInt(byte, 16))).buffer; -} - -/** - * Returns an hexadecimal representation of an array buffer. - * @param bytes The array buffer. - */ -export function toHexString(bytes: ArrayBuffer): string { - return new Uint8Array(bytes).reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), ''); -} diff --git a/packages/identity-secp256k1/src/secp256k1.test.ts b/packages/identity-secp256k1/src/secp256k1.test.ts index 86d47ac2..93b5d9d4 100644 --- a/packages/identity-secp256k1/src/secp256k1.test.ts +++ b/packages/identity-secp256k1/src/secp256k1.test.ts @@ -1,14 +1,9 @@ -import { DerEncodedPublicKey, PublicKey } from '@dfinity/agent'; -import { toHexString } from '@dfinity/candid/lib/cjs/utils/buffer'; +import { DerEncodedPublicKey, PublicKey, fromHex, toHex } from '@dfinity/agent'; import { randomBytes } from 'crypto'; import { sha256 } from '@noble/hashes/sha256'; import { secp256k1 } from '@noble/curves/secp256k1'; import { Secp256k1KeyIdentity, Secp256k1PublicKey } from './secp256k1'; -function fromHexString(hexString: string): ArrayBuffer { - return new Uint8Array((hexString.match(/.{1,2}/g) ?? []).map(byte => parseInt(byte, 16))).buffer; -} - // DER KEY SECP256K1 PREFIX = 3056301006072a8648ce3d020106052b8104000a03420004 // These test vectors contain the hex encoding of the corresponding raw and DER versions // of secp256k1 keys that were generated using OpenSSL as follows: @@ -40,7 +35,7 @@ const goldenSeed = '8caa0410fa5955c05d6877801806f627e5dd313957a59c70f8d8ef252a48 describe('Secp256k1PublicKey Tests', () => { test('create from an existing public key', () => { testVectors.forEach(([rawPublicKeyHex]) => { - const publicKey: PublicKey = Secp256k1PublicKey.fromRaw(fromHexString(rawPublicKeyHex)); + const publicKey: PublicKey = Secp256k1PublicKey.fromRaw(fromHex(rawPublicKeyHex)); const newKey = Secp256k1PublicKey.from(publicKey); expect(newKey).toMatchSnapshot(); @@ -49,8 +44,8 @@ describe('Secp256k1PublicKey Tests', () => { test('DER encoding of SECP256K1 keys', async () => { testVectors.forEach(([rawPublicKeyHex, derEncodedPublicKeyHex]) => { - const publicKey = Secp256k1PublicKey.fromRaw(fromHexString(rawPublicKeyHex)); - const expectedDerPublicKey = fromHexString(derEncodedPublicKeyHex); + const publicKey = Secp256k1PublicKey.fromRaw(fromHex(rawPublicKeyHex)); + const expectedDerPublicKey = fromHex(derEncodedPublicKeyHex); expect(publicKey.toDer()).toEqual(expectedDerPublicKey); }); }); @@ -59,7 +54,7 @@ describe('Secp256k1PublicKey Tests', () => { // Too short. expect(() => { Secp256k1PublicKey.fromDer( - fromHexString( + fromHex( '3056301006072a8648ce3d020106052b8104000a0342000401ec030acd7d1199f73ae3469329c114944e0693c89502f850bcc6bad397a5956767c79b410c29ac6f587eec84878020fdb54ba002a79b02aa153fe47b6', ) as DerEncodedPublicKey, ); @@ -67,7 +62,7 @@ describe('Secp256k1PublicKey Tests', () => { // Too long. expect(() => { Secp256k1PublicKey.fromDer( - fromHexString( + fromHex( '3056301006072a8648ce3d020106052b8104000a0342000401ec030acd7d1199f73ae3469329c114944e0693c89502f850bcc6bad397a5956767c79b410c29ac6f587eec84878020fdb54ba002a79b02aa153fe47b6ffd33' + '1b42211ce', ) as DerEncodedPublicKey, @@ -77,7 +72,7 @@ describe('Secp256k1PublicKey Tests', () => { // Invalid DER-encoding. expect(() => { Secp256k1PublicKey.fromDer( - fromHexString( + fromHex( '0693c89502f850bcc6bad397a5956767c79b410c29ac6f54fdac09ea93a1b9b744b5f19f091ada7978ceb2f045875bca8ef9b75fa8061704e76de023c6a23d77a118c5c8d0f5efaf0dbbfcc3702d5590604717f639f6f00d', ) as DerEncodedPublicKey, ); @@ -131,11 +126,11 @@ describe('Secp256k1KeyIdentity Tests', () => { }); test('generation from a seed should be supported', () => { - const seed = new Uint8Array(fromHexString(goldenSeed)); + const seed = new Uint8Array(fromHex(goldenSeed)); const identity = Secp256k1KeyIdentity.generate(seed); const publicKey = identity.getKeyPair().publicKey as Secp256k1PublicKey; publicKey.toRaw(); - expect(toHexString(publicKey.toRaw())).toEqual( + expect(toHex(publicKey.toRaw())).toEqual( '04e2abe3b762fe0553f690d25f5100259b7eaeb3e476df6bd3dfc1d27e5dae56dad5a84f70bd87acc95ad54af0285f28c2be1e3b2f62a28a2fbad9fe44c84dc904', ); }); @@ -195,7 +190,7 @@ describe('Secp256k1KeyIdentity Tests', () => { }); }); -describe('public key serialization from', () => { +describe('public key serialization from various types', () => { it('should serialize from an existing public key', () => { const baseKey = Secp256k1KeyIdentity.generate(); const publicKey: PublicKey = baseKey.getPublicKey(); @@ -225,7 +220,7 @@ describe('public key serialization from', () => { }); it('should serialize from a hex string', () => { const baseKey = Secp256k1KeyIdentity.generate(); - const publicKey = toHexString(baseKey.getPublicKey().toRaw()); + const publicKey = toHex(baseKey.getPublicKey().toRaw()); const newKey = Secp256k1PublicKey.from(publicKey); expect(newKey).toBeDefined(); }); @@ -235,6 +230,6 @@ describe('public key serialization from', () => { expect(shouldFail).toThrow('Cannot construct Secp256k1PublicKey from the provided key.'); const shouldFailHex = () => Secp256k1PublicKey.from('not a hex string'); - expect(shouldFailHex).toThrow('A Secp256k1 public key must be exactly 33 or 65 bytes long'); + expect(shouldFailHex).toThrow('Invalid hexadecimal string'); }); }); diff --git a/packages/identity-secp256k1/src/secp256k1.ts b/packages/identity-secp256k1/src/secp256k1.ts index 99f68bb4..55588555 100644 --- a/packages/identity-secp256k1/src/secp256k1.ts +++ b/packages/identity-secp256k1/src/secp256k1.ts @@ -5,6 +5,8 @@ import { Signature, uint8ToBuf, bufFromBufLike, + fromHex, + toHex, } from '@dfinity/agent'; import { secp256k1 } from '@noble/curves/secp256k1'; import { sha256 } from '@noble/hashes/sha256'; @@ -12,7 +14,6 @@ import { randomBytes } from '@noble/hashes/utils'; import hdkey from 'hdkey'; import { mnemonicToSeedSync } from 'bip39'; import { PublicKey, SignIdentity } from '@dfinity/agent'; -import { fromHexString, toHexString } from './buffer'; import { SECP256K1_OID, unwrapDER, wrapDER } from './der'; declare type PublicKeyHex = string; @@ -41,7 +42,7 @@ export class Secp256k1PublicKey implements PublicKey { */ public static from(maybeKey: unknown): Secp256k1PublicKey { if (typeof maybeKey === 'string') { - const key = fromHexString(maybeKey); + const key = fromHex(maybeKey); return this.fromRaw(key); } else if (isObject(maybeKey)) { const key = maybeKey as KeyLike; @@ -87,9 +88,6 @@ export class Secp256k1PublicKey implements PublicKey { // `fromRaw` and `fromDer` should be used for instantiation, not this constructor. private constructor(key: ArrayBuffer) { - if (key.byteLength !== 33 && key.byteLength !== 65) { - throw new Error('A Secp256k1 public key must be exactly 33 or 65 bytes long'); - } this.#rawKey = bufFromBufLike(key); this.#derKey = Secp256k1PublicKey.derEncode(key); } @@ -141,8 +139,8 @@ export class Secp256k1KeyIdentity extends SignIdentity { public static fromParsedJson(obj: JsonableSecp256k1Identity): Secp256k1KeyIdentity { const [publicKeyRaw, privateKeyRaw] = obj; return new Secp256k1KeyIdentity( - Secp256k1PublicKey.fromRaw(fromHexString(publicKeyRaw)), - fromHexString(privateKeyRaw), + Secp256k1PublicKey.fromRaw(fromHex(publicKeyRaw)), + fromHex(privateKeyRaw), ); } @@ -216,7 +214,7 @@ export class Secp256k1KeyIdentity extends SignIdentity { * @returns {JsonableSecp256k1Identity} */ public toJSON(): JsonableSecp256k1Identity { - return [toHexString(this._publicKey.toRaw()), toHexString(this._privateKey)]; + return [toHex(this._publicKey.toRaw()), toHex(this._privateKey)]; } /** @@ -232,9 +230,9 @@ export class Secp256k1KeyIdentity extends SignIdentity { /** * Return the public key. - * @returns {Secp256k1PublicKey} + * @returns {Required} */ - public getPublicKey(): Secp256k1PublicKey { + public getPublicKey(): Required { return this._publicKey; } diff --git a/packages/identity/src/identity/ed25519.test.ts b/packages/identity/src/identity/ed25519.test.ts index 0b991927..22fd5d44 100644 --- a/packages/identity/src/identity/ed25519.test.ts +++ b/packages/identity/src/identity/ed25519.test.ts @@ -1,4 +1,4 @@ -import { DerEncodedPublicKey, fromHex } from '@dfinity/agent'; +import { DerEncodedPublicKey, PublicKey, fromHex, toHex } from '@dfinity/agent'; import { Ed25519KeyIdentity, Ed25519PublicKey } from './ed25519'; const testVectors: Array<[string, string]> = [ @@ -129,3 +129,47 @@ test('from JSON', async () => { const isValid = Ed25519KeyIdentity.verify(msg, signature, identity.getPublicKey().rawKey); expect(isValid).toBe(true); }); + +describe('public key serialization from various types', () => { + it('should serialize from an existing public key', () => { + const baseKey = Ed25519KeyIdentity.generate(); + const publicKey: PublicKey = baseKey.getPublicKey(); + const newKey = Ed25519PublicKey.from(publicKey); + expect(newKey).toBeDefined(); + }); + it('should serialize from a raw key', () => { + const baseKey = Ed25519KeyIdentity.generate(); + const publicKey = baseKey.getPublicKey().rawKey; + ArrayBuffer.isView(publicKey); //? + publicKey instanceof ArrayBuffer; //? + + const newKey = Ed25519PublicKey.from(publicKey); + expect(newKey).toBeDefined(); + }); + it('should serialize from a DER key', () => { + const baseKey = Ed25519KeyIdentity.generate(); + const publicKey = baseKey.getPublicKey().derKey; + const newKey = Ed25519PublicKey.from(publicKey); + expect(newKey).toBeDefined(); + }); + it('should serialize from a Uint8Array', () => { + const baseKey = Ed25519KeyIdentity.generate(); + const publicKey = new Uint8Array(baseKey.getPublicKey().toRaw()); + const newKey = Ed25519PublicKey.from(publicKey); + expect(newKey).toBeDefined(); + }); + it('should serialize from a hex string', () => { + const baseKey = Ed25519KeyIdentity.generate(); + const publicKey = toHex(baseKey.getPublicKey().toRaw()); + const newKey = Ed25519PublicKey.from(publicKey); + expect(newKey).toBeDefined(); + }); + it('should fail to parse an invalid key', () => { + const baseKey = 7; + const shouldFail = () => Ed25519PublicKey.from(baseKey as unknown); + expect(shouldFail).toThrow('Cannot construct Ed25519PublicKey from the provided key.'); + + const shouldFailHex = () => Ed25519PublicKey.from('not a hex string'); + expect(shouldFailHex).toThrow('Invalid hexadecimal string'); + }); +}); diff --git a/packages/identity/src/identity/ed25519.ts b/packages/identity/src/identity/ed25519.ts index 3c36c563..c03e9aa1 100644 --- a/packages/identity/src/identity/ed25519.ts +++ b/packages/identity/src/identity/ed25519.ts @@ -10,12 +10,39 @@ import { wrapDER, fromHex, toHex, + bufFromBufLike, } from '@dfinity/agent'; import { ed25519 } from '@noble/curves/ed25519'; +declare type KeyLike = PublicKey | DerEncodedPublicKey | ArrayBuffer | ArrayBufferView; + +function isObject(value: unknown) { + return value !== null && typeof value === 'object'; +} + export class Ed25519PublicKey implements PublicKey { - public static from(key: PublicKey): Ed25519PublicKey { - return this.fromDer(key.toDer()); + public static from(maybeKey: unknown): Ed25519PublicKey { + if (typeof maybeKey === 'string') { + const key = fromHex(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(bufFromBufLike(view.buffer)); + } else if (key instanceof ArrayBuffer) { + return this.fromRaw(key); + } else if ('rawKey' in key) { + return this.fromRaw(key.rawKey as ArrayBuffer); + } else if ('derKey' in key) { + return this.fromDer(key.derKey as DerEncodedPublicKey); + } else if ('toDer' in key) { + return this.fromDer(key.toDer() as ArrayBuffer); + } + } + throw new Error('Cannot construct Ed25519PublicKey from the provided key.'); } public static fromRaw(rawKey: ArrayBuffer): Ed25519PublicKey { @@ -30,7 +57,9 @@ export class Ed25519PublicKey implements PublicKey { private static RAW_KEY_LENGTH = 32; private static derEncode(publicKey: ArrayBuffer): DerEncodedPublicKey { - return wrapDER(publicKey, ED25519_OID).buffer as DerEncodedPublicKey; + const key = wrapDER(publicKey, ED25519_OID).buffer as DerEncodedPublicKey; + key.__derEncodedPublicKey__ = undefined; + return key; } private static derDecode(key: DerEncodedPublicKey): ArrayBuffer {