UNPKG

ox

Version:

Ethereum Standard Library

280 lines (241 loc) 9.06 kB
import { Hex, PublicKey, WebCryptoP256 } from 'ox' import { describe, expect, test } from 'vp/test' const { privateKey, publicKey } = await WebCryptoP256.createKeyPair() describe('createKeyPair', () => { test('default', async () => { const key = await WebCryptoP256.createKeyPair() expect(key.privateKey).toBeDefined() expect(key.publicKey.prefix).toBeDefined() expect(key.publicKey.x).toBeDefined() expect(key.publicKey.y).toBeDefined() }) }) describe('createKeyPairECDH', () => { test('default', async () => { const key = await WebCryptoP256.createKeyPairECDH() expect(key.privateKey).toBeDefined() expect(key.privateKey.algorithm.name).toBe('ECDH') expect((key.privateKey.algorithm as EcKeyAlgorithm).namedCurve).toBe( 'P-256', ) expect(key.privateKey.usages).toContain('deriveBits') expect(key.publicKey.prefix).toBeDefined() expect(key.publicKey.x).toBeDefined() expect(key.publicKey.y).toBeDefined() }) test('options: extractable', async () => { const keyExtractable = await WebCryptoP256.createKeyPairECDH({ extractable: true, }) const keyNonExtractable = await WebCryptoP256.createKeyPairECDH({ extractable: false, }) expect(keyExtractable.privateKey.extractable).toBe(true) expect(keyNonExtractable.privateKey.extractable).toBe(false) }) }) describe('getSharedSecret', () => { test('default', async () => { const { privateKey: privateKeyA, publicKey: publicKeyA } = await WebCryptoP256.createKeyPairECDH() const { privateKey: privateKeyB, publicKey: publicKeyB } = await WebCryptoP256.createKeyPairECDH() const sharedSecretA = await WebCryptoP256.getSharedSecret({ privateKey: privateKeyA, publicKey: publicKeyB, }) const sharedSecretB = await WebCryptoP256.getSharedSecret({ privateKey: privateKeyB, publicKey: publicKeyA, }) expect(sharedSecretA).toEqual(sharedSecretB) }) test('behavior: same key pairs produce same secret', async () => { const { privateKey: privateKeyA } = await WebCryptoP256.createKeyPairECDH() const { publicKey: publicKeyB } = await WebCryptoP256.createKeyPairECDH() const sharedSecret1 = await WebCryptoP256.getSharedSecret({ privateKey: privateKeyA, publicKey: publicKeyB, }) const sharedSecret2 = await WebCryptoP256.getSharedSecret({ privateKey: privateKeyA, publicKey: publicKeyB, }) // Should be deterministic - same inputs produce same output expect(sharedSecret1).toEqual(sharedSecret2) }) test('behavior: different key pairs produce different secrets', async () => { const { privateKey: privateKeyA } = await WebCryptoP256.createKeyPairECDH() const { publicKey: publicKeyB } = await WebCryptoP256.createKeyPairECDH() const { publicKey: publicKeyC } = await WebCryptoP256.createKeyPairECDH() const sharedSecretAB = await WebCryptoP256.getSharedSecret({ privateKey: privateKeyA, publicKey: publicKeyB, }) const sharedSecretAC = await WebCryptoP256.getSharedSecret({ privateKey: privateKeyA, publicKey: publicKeyC, }) // Different key pairs should produce different shared secrets expect(sharedSecretAB).not.toEqual(sharedSecretAC) }) test('options: as', async () => { const { privateKey: privateKeyA } = await WebCryptoP256.createKeyPairECDH() const { publicKey: publicKeyB } = await WebCryptoP256.createKeyPairECDH() // Test Hex output (default) const sharedSecretHex = await WebCryptoP256.getSharedSecret({ privateKey: privateKeyA, publicKey: publicKeyB, }) // Test Bytes output const sharedSecretBytes = await WebCryptoP256.getSharedSecret({ privateKey: privateKeyA, publicKey: publicKeyB, as: 'Bytes', }) // Verify formats expect(typeof sharedSecretHex).toBe('string') expect(sharedSecretHex).toMatch(/^0x[0-9a-f]{64}$/) expect(sharedSecretBytes).toBeInstanceOf(Uint8Array) expect(sharedSecretBytes.length).toBe(32) // 32 bytes for raw shared secret // Verify they represent the same data expect(Hex.fromBytes(sharedSecretBytes)).toEqual(sharedSecretHex) }) test('behavior: compressed public key', async () => { const { privateKey: privateKeyA, ...A } = await WebCryptoP256.createKeyPairECDH() const { privateKey: privateKeyB, ...B } = await WebCryptoP256.createKeyPairECDH() const publicKeyA = PublicKey.compress(A.publicKey) const publicKeyB = PublicKey.compress(B.publicKey) const sharedSecret = await WebCryptoP256.getSharedSecret({ privateKey: privateKeyA, publicKey: publicKeyB, }) const sharedSecret2 = await WebCryptoP256.getSharedSecret({ privateKey: privateKeyB, publicKey: publicKeyA, }) expect(sharedSecret).toEqual(sharedSecret2) }) test('error: private key not for ECDH', async () => { const { privateKey: ecdsaPrivateKey } = await WebCryptoP256.createKeyPair() // ECDSA key const { publicKey: publicKeyB } = await WebCryptoP256.createKeyPairECDH() await expect( WebCryptoP256.getSharedSecret({ privateKey: ecdsaPrivateKey, publicKey: publicKeyB, }), ).rejects.toThrowErrorMatchingInlineSnapshot( '[WebCryptoP256.InvalidPrivateKeyAlgorithmError: privateKey is not compatible with ECDH. Please use `createKeyPairECDH` to create an ECDH key.]', ) }) test('error: invalid public key', async () => { const { privateKey: privateKeyA } = await WebCryptoP256.createKeyPairECDH() const invalidPublicKey = { prefix: 4, x: '0x0000000000000000000000000000000000000000000000000000000000000000', y: '0x0000000000000000000000000000000000000000000000000000000000000000', } as const await expect( WebCryptoP256.getSharedSecret({ privateKey: privateKeyA, publicKey: invalidPublicKey, }), ).rejects.toThrowErrorMatchingInlineSnapshot('[DataError: Invalid keyData]') }) }) describe('sign', () => { test('default', async () => { const signature = await WebCryptoP256.sign({ payload: '0xd9eba16ed0ecae432b71fe008c98cc872bb4cc214d3220a36f365326cf807d68', privateKey, }) expect(signature.r).toBeDefined() expect(signature.s).toBeDefined() }) }) describe('verify', () => { test('default', async () => { const payload = '0xdeadbeef' const { r, s } = await WebCryptoP256.sign({ payload, privateKey }) expect( await WebCryptoP256.verify({ publicKey, payload, signature: { r, s } }), ).toBe(true) }) test('behavior: signature with leading-zero r/s is verified', async () => { // Sign repeatedly until we hit a signature whose r or s value has its // most-significant byte equal to zero. With variable-width number // encoding such signatures fail to verify; with fixed-width 32-byte // encoding (the regression fix) they verify correctly. const payload = '0xdeadbeef' for (let i = 0; i < 256; i++) { const signature = await WebCryptoP256.sign({ payload, privateKey }) if (signature.r.startsWith('0x00') || signature.s.startsWith('0x00')) { expect( await WebCryptoP256.verify({ publicKey, payload, signature }), ).toBe(true) return } } }) }) test('exports', () => { expect(Object.keys(WebCryptoP256)).toMatchInlineSnapshot(` [ "createKeyPair", "createKeyPairECDH", "getSharedSecret", "sign", "verify", "InvalidPrivateKeyAlgorithmError", ] `) }) describe('as option / serialized inputs', () => { test("sign: as 'Hex' returns hex string", async () => { const { privateKey } = await WebCryptoP256.createKeyPair() const sigHex = await WebCryptoP256.sign({ payload: '0xdeadbeef', privateKey, as: 'Hex', }) expect(typeof sigHex).toBe('string') expect((sigHex as Hex.Hex).startsWith('0x')).toBe(true) }) test("sign: as 'Bytes' returns Uint8Array", async () => { const { privateKey } = await WebCryptoP256.createKeyPair() const sigBytes = await WebCryptoP256.sign({ payload: '0xdeadbeef', privateKey, as: 'Bytes', }) expect(sigBytes).toBeInstanceOf(Uint8Array) }) test('default as remains Object', async () => { const { privateKey } = await WebCryptoP256.createKeyPair() const sig = await WebCryptoP256.sign({ payload: '0xdeadbeef', privateKey, }) expect(typeof sig).toBe('object') expect('r' in sig).toBe(true) }) test('verify accepts Hex publicKey + Hex signature', async () => { const { privateKey, publicKey } = await WebCryptoP256.createKeyPair() const sigHex = await WebCryptoP256.sign({ payload: '0xdeadbeef', privateKey, as: 'Hex', }) const pkHex = PublicKey.toHex(publicKey) expect( await WebCryptoP256.verify({ payload: '0xdeadbeef', publicKey: pkHex, signature: sigHex, }), ).toBe(true) }) })