Skip to content

Commit

Permalink
fix: allow keys to do sync sign/verify (#2258)
Browse files Browse the repository at this point in the history
Removes unnecessary async from Ed/Secp keys sign/verify methods.

Ed is sync everywhere, Secp is sync in node but async in browsers.
  • Loading branch information
achingbrain authored Nov 28, 2023
1 parent 7877a50 commit dd7d17c
Show file tree
Hide file tree
Showing 10 changed files with 125 additions and 59 deletions.
24 changes: 18 additions & 6 deletions packages/crypto/src/keys/ed25519-class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -16,7 +17,7 @@ export class Ed25519PublicKey {
this._key = ensureKey(key, crypto.publicKeyLength)
}

async verify (data: Uint8Array | Uint8ArrayList, sig: Uint8Array): Promise<boolean> {
verify (data: Uint8Array | Uint8ArrayList, sig: Uint8Array): boolean {
return crypto.hashAndVerify(this._key, sig, data)
}

Expand All @@ -35,10 +36,14 @@ export class Ed25519PublicKey {
return uint8ArrayEquals(this.bytes, key.bytes)
}

async hash (): Promise<Uint8Array> {
const { bytes } = await sha256.digest(this.bytes)
hash (): Uint8Array | Promise<Uint8Array> {
const p = sha256.digest(this.bytes)

return bytes
if (isPromise(p)) {
return p.then(({ bytes }) => bytes)
}

return p.bytes
}
}

Expand All @@ -53,7 +58,7 @@ export class Ed25519PrivateKey {
this._publicKey = ensureKey(publicKey, crypto.publicKeyLength)
}

async sign (message: Uint8Array | Uint8ArrayList): Promise<Uint8Array> {
sign (message: Uint8Array | Uint8ArrayList): Uint8Array {
return crypto.hashAndSign(this._key, message)
}

Expand All @@ -77,7 +82,14 @@ export class Ed25519PrivateKey {
}

async hash (): Promise<Uint8Array> {
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
}
Expand Down
27 changes: 18 additions & 9 deletions packages/crypto/src/keys/rsa-class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -20,7 +21,7 @@ export class RsaPublicKey {
this._key = key
}

async verify (data: Uint8Array | Uint8ArrayList, sig: Uint8Array): Promise<boolean> {
verify (data: Uint8Array | Uint8ArrayList, sig: Uint8Array): boolean | Promise<boolean> {
return crypto.hashAndVerify(this._key, sig, data)
}

Expand All @@ -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<Uint8Array> {
const { bytes } = await sha256.digest(this.bytes)
hash (): Uint8Array | Promise<Uint8Array> {
const p = sha256.digest(this.bytes)

if (isPromise(p)) {
return p.then(({ bytes }) => bytes)
}

return bytes
return p.bytes
}
}

Expand All @@ -63,7 +68,7 @@ export class RsaPrivateKey {
return crypto.getRandomValues(16)
}

async sign (message: Uint8Array | Uint8ArrayList): Promise<Uint8Array> {
sign (message: Uint8Array | Uint8ArrayList): Uint8Array | Promise<Uint8Array> {
return crypto.hashAndSign(this._key, message)
}

Expand Down Expand Up @@ -94,10 +99,14 @@ export class RsaPrivateKey {
return uint8ArrayEquals(this.bytes, key.bytes)
}

async hash (): Promise<Uint8Array> {
const { bytes } = await sha256.digest(this.bytes)
hash (): Uint8Array | Promise<Uint8Array> {
const p = sha256.digest(this.bytes)

if (isPromise(p)) {
return p.then(({ bytes }) => bytes)
}

return bytes
return p.bytes
}

/**
Expand Down
30 changes: 23 additions & 7 deletions packages/crypto/src/keys/secp256k1-browser.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<Uint8Array> {
const { digest } = await sha256.digest(msg instanceof Uint8Array ? msg : msg.subarray())
export function hashAndSign (key: Uint8Array, msg: Uint8Array | Uint8ArrayList): Uint8Array | Promise<Uint8Array> {
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')
}
Expand All @@ -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<boolean> {
export function hashAndVerify (key: Uint8Array, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): boolean | Promise<boolean> {
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')
}
Expand Down
24 changes: 18 additions & 6 deletions packages/crypto/src/keys/secp256k1-class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -16,7 +17,7 @@ export class Secp256k1PublicKey {
this._key = key
}

async verify (data: Uint8Array | Uint8ArrayList, sig: Uint8Array): Promise<boolean> {
verify (data: Uint8Array | Uint8ArrayList, sig: Uint8Array): boolean {
return crypto.hashAndVerify(this._key, sig, data)
}

Expand All @@ -36,7 +37,14 @@ export class Secp256k1PublicKey {
}

async hash (): Promise<Uint8Array> {
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
}
Expand All @@ -53,7 +61,7 @@ export class Secp256k1PrivateKey {
crypto.validatePublicKey(this._publicKey)
}

async sign (message: Uint8Array | Uint8ArrayList): Promise<Uint8Array> {
sign (message: Uint8Array | Uint8ArrayList): Uint8Array | Promise<Uint8Array> {
return crypto.hashAndSign(this._key, message)
}

Expand All @@ -76,10 +84,14 @@ export class Secp256k1PrivateKey {
return uint8ArrayEquals(this.bytes, key.bytes)
}

async hash (): Promise<Uint8Array> {
const { bytes } = await sha256.digest(this.bytes)
hash (): Uint8Array | Promise<Uint8Array> {
const p = sha256.digest(this.bytes)

return bytes
if (isPromise(p)) {
return p.then(({ bytes }) => bytes)
}

return p.bytes
}

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/crypto/src/keys/secp256k1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Uint8Array> {
export function hashAndSign (key: Uint8Array, msg: Uint8Array | Uint8ArrayList): Uint8Array {
const hash = crypto.createHash('sha256')

if (msg instanceof Uint8Array) {
Expand All @@ -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<boolean> {
export function hashAndVerify (key: Uint8Array, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): boolean {
const hash = crypto.createHash('sha256')

if (msg instanceof Uint8Array) {
Expand Down
10 changes: 10 additions & 0 deletions packages/crypto/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,13 @@ export function base64urlToBuffer (str: string, len?: number): Uint8Array {

return buf
}

export function isPromise <T = unknown> (thing: any): thing is Promise<T> {
if (thing == null) {
return false
}

return typeof thing.then === 'function' &&
typeof thing.catch === 'function' &&
typeof thing.finally === 'function'
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>, num?: number, skipBuffersAndStrings?: boolean): void {
export function testGarbage (fncName: string, fnc: (...args: Uint8Array[]) => any | Promise<any>, num?: number, skipBuffersAndStrings?: boolean): void {
const count = num ?? 1

garbage.forEach((garbage) => {
Expand Down
26 changes: 13 additions & 13 deletions packages/crypto/test/keys/ed25519.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})

Expand All @@ -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', () => {
Expand Down Expand Up @@ -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)
})

Expand All @@ -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)
})

Expand Down
29 changes: 18 additions & 11 deletions packages/crypto/test/keys/secp256k1.spec.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
Loading

0 comments on commit dd7d17c

Please sign in to comment.