From dd7d17cc478dfcba02211a47789439b7d7ab9627 Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Tue, 28 Nov 2023 10:55:41 +0000 Subject: [PATCH] fix: allow keys to do sync sign/verify (#2258) Removes unnecessary async from Ed/Secp keys sign/verify methods. Ed is sync everywhere, Secp is sync in node but async in browsers. --- packages/crypto/src/keys/ed25519-class.ts | 24 +++++++++++---- packages/crypto/src/keys/rsa-class.ts | 27 +++++++++++------ packages/crypto/src/keys/secp256k1-browser.ts | 30 ++++++++++++++----- packages/crypto/src/keys/secp256k1-class.ts | 24 +++++++++++---- packages/crypto/src/keys/secp256k1.ts | 4 +-- packages/crypto/src/util.ts | 10 +++++++ .../helpers/test-garbage-error-handling.ts | 2 +- packages/crypto/test/keys/ed25519.spec.ts | 26 ++++++++-------- packages/crypto/test/keys/secp256k1.spec.ts | 29 +++++++++++------- packages/interface/src/keys/index.ts | 8 ++--- 10 files changed, 125 insertions(+), 59 deletions(-) diff --git a/packages/crypto/src/keys/ed25519-class.ts b/packages/crypto/src/keys/ed25519-class.ts index ee7f232393..5957783156 100644 --- a/packages/crypto/src/keys/ed25519-class.ts +++ b/packages/crypto/src/keys/ed25519-class.ts @@ -3,6 +3,7 @@ import { base58btc } from 'multiformats/bases/base58' import { identity } from 'multiformats/hashes/identity' import { sha256 } from 'multiformats/hashes/sha2' import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import { isPromise } from '../util.js' import * as crypto from './ed25519.js' import { exporter } from './exporter.js' import * as pbm from './keys.js' @@ -16,7 +17,7 @@ export class Ed25519PublicKey { this._key = ensureKey(key, crypto.publicKeyLength) } - async verify (data: Uint8Array | Uint8ArrayList, sig: Uint8Array): Promise { + verify (data: Uint8Array | Uint8ArrayList, sig: Uint8Array): boolean { return crypto.hashAndVerify(this._key, sig, data) } @@ -35,10 +36,14 @@ export class Ed25519PublicKey { return uint8ArrayEquals(this.bytes, key.bytes) } - async hash (): Promise { - const { bytes } = await sha256.digest(this.bytes) + hash (): Uint8Array | Promise { + const p = sha256.digest(this.bytes) - return bytes + if (isPromise(p)) { + return p.then(({ bytes }) => bytes) + } + + return p.bytes } } @@ -53,7 +58,7 @@ export class Ed25519PrivateKey { this._publicKey = ensureKey(publicKey, crypto.publicKeyLength) } - async sign (message: Uint8Array | Uint8ArrayList): Promise { + sign (message: Uint8Array | Uint8ArrayList): Uint8Array { return crypto.hashAndSign(this._key, message) } @@ -77,7 +82,14 @@ export class Ed25519PrivateKey { } async hash (): Promise { - const { bytes } = await sha256.digest(this.bytes) + const p = sha256.digest(this.bytes) + let bytes: Uint8Array + + if (isPromise(p)) { + ({ bytes } = await p) + } else { + bytes = p.bytes + } return bytes } diff --git a/packages/crypto/src/keys/rsa-class.ts b/packages/crypto/src/keys/rsa-class.ts index e48021c842..6b6e51cd3e 100644 --- a/packages/crypto/src/keys/rsa-class.ts +++ b/packages/crypto/src/keys/rsa-class.ts @@ -5,6 +5,7 @@ import forge from 'node-forge/lib/forge.js' import { equals as uint8ArrayEquals } from 'uint8arrays/equals' import 'node-forge/lib/sha512.js' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { isPromise } from '../util.js' import { exporter } from './exporter.js' import * as pbm from './keys.js' import * as crypto from './rsa.js' @@ -20,7 +21,7 @@ export class RsaPublicKey { this._key = key } - async verify (data: Uint8Array | Uint8ArrayList, sig: Uint8Array): Promise { + verify (data: Uint8Array | Uint8ArrayList, sig: Uint8Array): boolean | Promise { return crypto.hashAndVerify(this._key, sig, data) } @@ -39,14 +40,18 @@ export class RsaPublicKey { return crypto.encrypt(this._key, bytes) } - equals (key: any): boolean { + equals (key: any): boolean | boolean { return uint8ArrayEquals(this.bytes, key.bytes) } - async hash (): Promise { - const { bytes } = await sha256.digest(this.bytes) + hash (): Uint8Array | Promise { + const p = sha256.digest(this.bytes) + + if (isPromise(p)) { + return p.then(({ bytes }) => bytes) + } - return bytes + return p.bytes } } @@ -63,7 +68,7 @@ export class RsaPrivateKey { return crypto.getRandomValues(16) } - async sign (message: Uint8Array | Uint8ArrayList): Promise { + sign (message: Uint8Array | Uint8ArrayList): Uint8Array | Promise { return crypto.hashAndSign(this._key, message) } @@ -94,10 +99,14 @@ export class RsaPrivateKey { return uint8ArrayEquals(this.bytes, key.bytes) } - async hash (): Promise { - const { bytes } = await sha256.digest(this.bytes) + hash (): Uint8Array | Promise { + const p = sha256.digest(this.bytes) + + if (isPromise(p)) { + return p.then(({ bytes }) => bytes) + } - return bytes + return p.bytes } /** diff --git a/packages/crypto/src/keys/secp256k1-browser.ts b/packages/crypto/src/keys/secp256k1-browser.ts index c7de16c560..499f340a37 100644 --- a/packages/crypto/src/keys/secp256k1-browser.ts +++ b/packages/crypto/src/keys/secp256k1-browser.ts @@ -1,6 +1,7 @@ import { CodeError } from '@libp2p/interface/errors' import { secp256k1 as secp } from '@noble/curves/secp256k1' import { sha256 } from 'multiformats/hashes/sha2' +import { isPromise } from '../util.js' import type { Uint8ArrayList } from 'uint8arraylist' const PRIVATE_KEY_BYTE_LENGTH = 32 @@ -14,11 +15,18 @@ export function generateKey (): Uint8Array { /** * Hash and sign message with private key */ -export async function hashAndSign (key: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise { - const { digest } = await sha256.digest(msg instanceof Uint8Array ? msg : msg.subarray()) +export function hashAndSign (key: Uint8Array, msg: Uint8Array | Uint8ArrayList): Uint8Array | Promise { + const p = sha256.digest(msg instanceof Uint8Array ? msg : msg.subarray()) + + if (isPromise(p)) { + return p.then(({ digest }) => secp.sign(digest, key).toDERRawBytes()) + .catch(err => { + throw new CodeError(String(err), 'ERR_INVALID_INPUT') + }) + } + try { - const signature = secp.sign(digest, key) - return signature.toDERRawBytes() + return secp.sign(p.digest, key).toDERRawBytes() } catch (err) { throw new CodeError(String(err), 'ERR_INVALID_INPUT') } @@ -27,10 +35,18 @@ export async function hashAndSign (key: Uint8Array, msg: Uint8Array | Uint8Array /** * Hash message and verify signature with public key */ -export async function hashAndVerify (key: Uint8Array, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise { +export function hashAndVerify (key: Uint8Array, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): boolean | Promise { + const p = sha256.digest(msg instanceof Uint8Array ? msg : msg.subarray()) + + if (isPromise(p)) { + return p.then(({ digest }) => secp.verify(sig, digest, key)) + .catch(err => { + throw new CodeError(String(err), 'ERR_INVALID_INPUT') + }) + } + try { - const { digest } = await sha256.digest(msg instanceof Uint8Array ? msg : msg.subarray()) - return secp.verify(sig, digest, key) + return secp.verify(sig, p.digest, key) } catch (err) { throw new CodeError(String(err), 'ERR_INVALID_INPUT') } diff --git a/packages/crypto/src/keys/secp256k1-class.ts b/packages/crypto/src/keys/secp256k1-class.ts index 6dc8cf84fc..b6531b0562 100644 --- a/packages/crypto/src/keys/secp256k1-class.ts +++ b/packages/crypto/src/keys/secp256k1-class.ts @@ -2,6 +2,7 @@ import { CodeError } from '@libp2p/interface/errors' import { sha256 } from 'multiformats/hashes/sha2' import { equals as uint8ArrayEquals } from 'uint8arrays/equals' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { isPromise } from '../util.js' import { exporter } from './exporter.js' import * as keysProtobuf from './keys.js' import * as crypto from './secp256k1.js' @@ -16,7 +17,7 @@ export class Secp256k1PublicKey { this._key = key } - async verify (data: Uint8Array | Uint8ArrayList, sig: Uint8Array): Promise { + verify (data: Uint8Array | Uint8ArrayList, sig: Uint8Array): boolean { return crypto.hashAndVerify(this._key, sig, data) } @@ -36,7 +37,14 @@ export class Secp256k1PublicKey { } async hash (): Promise { - const { bytes } = await sha256.digest(this.bytes) + const p = sha256.digest(this.bytes) + let bytes: Uint8Array + + if (isPromise(p)) { + ({ bytes } = await p) + } else { + bytes = p.bytes + } return bytes } @@ -53,7 +61,7 @@ export class Secp256k1PrivateKey { crypto.validatePublicKey(this._publicKey) } - async sign (message: Uint8Array | Uint8ArrayList): Promise { + sign (message: Uint8Array | Uint8ArrayList): Uint8Array | Promise { return crypto.hashAndSign(this._key, message) } @@ -76,10 +84,14 @@ export class Secp256k1PrivateKey { return uint8ArrayEquals(this.bytes, key.bytes) } - async hash (): Promise { - const { bytes } = await sha256.digest(this.bytes) + hash (): Uint8Array | Promise { + const p = sha256.digest(this.bytes) - return bytes + if (isPromise(p)) { + return p.then(({ bytes }) => bytes) + } + + return p.bytes } /** diff --git a/packages/crypto/src/keys/secp256k1.ts b/packages/crypto/src/keys/secp256k1.ts index c42855ae39..0be19e4512 100644 --- a/packages/crypto/src/keys/secp256k1.ts +++ b/packages/crypto/src/keys/secp256k1.ts @@ -14,7 +14,7 @@ export function generateKey (): Uint8Array { /** * Hash and sign message with private key */ -export async function hashAndSign (key: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise { +export function hashAndSign (key: Uint8Array, msg: Uint8Array | Uint8ArrayList): Uint8Array { const hash = crypto.createHash('sha256') if (msg instanceof Uint8Array) { @@ -38,7 +38,7 @@ export async function hashAndSign (key: Uint8Array, msg: Uint8Array | Uint8Array /** * Hash message and verify signature with public key */ -export async function hashAndVerify (key: Uint8Array, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise { +export function hashAndVerify (key: Uint8Array, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): boolean { const hash = crypto.createHash('sha256') if (msg instanceof Uint8Array) { diff --git a/packages/crypto/src/util.ts b/packages/crypto/src/util.ts index 0dab953f8a..a7e272982f 100644 --- a/packages/crypto/src/util.ts +++ b/packages/crypto/src/util.ts @@ -40,3 +40,13 @@ export function base64urlToBuffer (str: string, len?: number): Uint8Array { return buf } + +export function isPromise (thing: any): thing is Promise { + if (thing == null) { + return false + } + + return typeof thing.then === 'function' && + typeof thing.catch === 'function' && + typeof thing.finally === 'function' +} diff --git a/packages/crypto/test/helpers/test-garbage-error-handling.ts b/packages/crypto/test/helpers/test-garbage-error-handling.ts index 60316b26f2..adece877e8 100644 --- a/packages/crypto/test/helpers/test-garbage-error-handling.ts +++ b/packages/crypto/test/helpers/test-garbage-error-handling.ts @@ -4,7 +4,7 @@ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' const garbage = [uint8ArrayFromString('00010203040506070809', 'base16'), {}, null, false, undefined, true, 1, 0, uint8ArrayFromString(''), 'aGVsbG93b3JsZA==', 'helloworld', ''] -export function testGarbage (fncName: string, fnc: (...args: Uint8Array[]) => Promise, num?: number, skipBuffersAndStrings?: boolean): void { +export function testGarbage (fncName: string, fnc: (...args: Uint8Array[]) => any | Promise, num?: number, skipBuffersAndStrings?: boolean): void { const count = num ?? 1 garbage.forEach((garbage) => { diff --git a/packages/crypto/test/keys/ed25519.spec.ts b/packages/crypto/test/keys/ed25519.spec.ts index 710e96377b..49650c20cf 100644 --- a/packages/crypto/test/keys/ed25519.spec.ts +++ b/packages/crypto/test/keys/ed25519.spec.ts @@ -57,8 +57,8 @@ describe('ed25519', function () { it('signs', async () => { const text = crypto.randomBytes(512) - const sig = await key.sign(text) - const res = await key.public.verify(text, sig) + const sig = key.sign(text) + const res = key.public.verify(text, sig) expect(res).to.be.eql(true) }) @@ -67,15 +67,15 @@ describe('ed25519', function () { crypto.randomBytes(512), crypto.randomBytes(512) ) - const sig = await key.sign(text) + const sig = key.sign(text) - await expect(key.sign(text.subarray())) - .to.eventually.deep.equal(sig, 'list did not have same signature as a single buffer') + expect(key.sign(text.subarray())) + .to.deep.equal(sig, 'list did not have same signature as a single buffer') - await expect(key.public.verify(text, sig)) - .to.eventually.be.true('did not verify message as list') - await expect(key.public.verify(text.subarray(), sig)) - .to.eventually.be.true('did not verify message as single buffer') + expect(key.public.verify(text, sig)) + .to.be.true('did not verify message as list') + expect(key.public.verify(text.subarray(), sig)) + .to.be.true('did not verify message as single buffer') }) it('encoding', () => { @@ -178,8 +178,8 @@ describe('ed25519', function () { it('sign and verify', async () => { const data = uint8ArrayFromString('hello world') - const sig = await key.sign(data) - const valid = await key.public.verify(data, sig) + const sig = key.sign(data) + const valid = key.public.verify(data, sig) expect(valid).to.eql(true) }) @@ -194,8 +194,8 @@ describe('ed25519', function () { it('fails to verify for different data', async () => { const data = uint8ArrayFromString('hello world') - const sig = await key.sign(data) - const valid = await key.public.verify(uint8ArrayFromString('hello'), sig) + const sig = key.sign(data) + const valid = key.public.verify(uint8ArrayFromString('hello'), sig) expect(valid).to.be.eql(false) }) diff --git a/packages/crypto/test/keys/secp256k1.spec.ts b/packages/crypto/test/keys/secp256k1.spec.ts index 3ac854ea63..bb55f0b3d5 100644 --- a/packages/crypto/test/keys/secp256k1.spec.ts +++ b/packages/crypto/test/keys/secp256k1.spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/await-thenable */ // secp is sync in node, async in browsers /* eslint-env mocha */ import { expect } from 'aegir/chai' import { Uint8ArrayList } from 'uint8arraylist' @@ -48,13 +49,13 @@ describe('secp256k1 keys', () => { ) const sig = await key.sign(text) - await expect(key.sign(text.subarray())) - .to.eventually.deep.equal(sig, 'list did not have same signature as a single buffer') + expect(await key.sign(text.subarray())) + .to.deep.equal(sig, 'list did not have same signature as a single buffer') - await expect(key.public.verify(text, sig)) - .to.eventually.be.true('did not verify message as list') - await expect(key.public.verify(text.subarray(), sig)) - .to.eventually.be.true('did not verify message as single buffer') + expect(await key.public.verify(text, sig)) + .to.be.true('did not verify message as list') + expect(await key.public.verify(text.subarray(), sig)) + .to.be.true('did not verify message as single buffer') }) it('encoding', () => { @@ -169,19 +170,25 @@ describe('crypto functions', () => { }) it('errors if given a null Uint8Array to sign', async () => { - // @ts-expect-error incorrect args - await expect(secp256k1Crypto.hashAndSign(privKey, null)).to.eventually.be.rejected() + await expect((async () => { + // @ts-expect-error incorrect args + await secp256k1Crypto.hashAndSign(privKey, null) + })()).to.eventually.be.rejected() }) it('errors when signing with an invalid key', async () => { - await expect(secp256k1Crypto.hashAndSign(uint8ArrayFromString('42'), uint8ArrayFromString('Hello'))).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_INPUT') + await expect((async () => { + await secp256k1Crypto.hashAndSign(uint8ArrayFromString('42'), uint8ArrayFromString('Hello')) + })()).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_INPUT') }) it('errors if given a null Uint8Array to validate', async () => { const sig = await secp256k1Crypto.hashAndSign(privKey, uint8ArrayFromString('hello')) - // @ts-expect-error incorrect args - await expect(secp256k1Crypto.hashAndVerify(privKey, sig, null)).to.eventually.be.rejected() + await expect((async () => { + // @ts-expect-error incorrect args + await secp256k1Crypto.hashAndVerify(privKey, sig, null) + })()).to.eventually.be.rejected() }) it('throws when compressing an invalid public key', () => { diff --git a/packages/interface/src/keys/index.ts b/packages/interface/src/keys/index.ts index 63d489a09b..b7ade54f57 100644 --- a/packages/interface/src/keys/index.ts +++ b/packages/interface/src/keys/index.ts @@ -2,10 +2,10 @@ import type { Uint8ArrayList } from 'uint8arraylist' export interface PublicKey { readonly bytes: Uint8Array - verify(data: Uint8Array | Uint8ArrayList, sig: Uint8Array): Promise + verify(data: Uint8Array | Uint8ArrayList, sig: Uint8Array): boolean | Promise marshal(): Uint8Array equals(key: PublicKey): boolean - hash(): Promise + hash(): Uint8Array | Promise } /** @@ -14,10 +14,10 @@ export interface PublicKey { export interface PrivateKey { readonly public: PublicKey readonly bytes: Uint8Array - sign(data: Uint8Array | Uint8ArrayList): Promise + sign(data: Uint8Array | Uint8ArrayList): Uint8Array | Promise marshal(): Uint8Array equals(key: PrivateKey): boolean - hash(): Promise + hash(): Uint8Array | Promise /** * Gets the ID of the key. *