Skip to content
Merged
28 changes: 24 additions & 4 deletions packages/crypto/src/keys/ed25519/ed25519.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { CID } from 'multiformats/cid'
import { identity } from 'multiformats/hashes/identity'
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
import { isPromise } from '../../util.ts'
import { publicKeyToProtobuf } from '../index.js'
import { ensureEd25519Key } from './utils.js'
import * as crypto from './index.js'
Expand Down Expand Up @@ -37,9 +38,18 @@
return uint8ArrayEquals(this.raw, key.raw)
}

verify (data: Uint8Array | Uint8ArrayList, sig: Uint8Array, options?: AbortOptions): boolean {
verify (data: Uint8Array | Uint8ArrayList, sig: Uint8Array, options?: AbortOptions): boolean | Promise<boolean> {
options?.signal?.throwIfAborted()
return crypto.hashAndVerify(this.raw, sig, data)
const result = crypto.hashAndVerify(this.raw, sig, data)

if (isPromise<boolean>(result)) {
return result.then(res => {
options?.signal?.throwIfAborted()
return res
})
}

Check warning on line 50 in packages/crypto/src/keys/ed25519/ed25519.ts

View check run for this annotation

Codecov / codecov/patch

packages/crypto/src/keys/ed25519/ed25519.ts#L46-L50

Added lines #L46 - L50 were not covered by tests

return result
}
}

Expand All @@ -63,8 +73,18 @@
return uint8ArrayEquals(this.raw, key.raw)
}

sign (message: Uint8Array | Uint8ArrayList, options?: AbortOptions): Uint8Array {
sign (message: Uint8Array | Uint8ArrayList, options?: AbortOptions): Uint8Array | Promise<Uint8Array> {
options?.signal?.throwIfAborted()
const sig = crypto.hashAndSign(this.raw, message)

if (isPromise<Uint8Array>(sig)) {
return sig.then(res => {
options?.signal?.throwIfAborted()
return res
})
}

Check warning on line 85 in packages/crypto/src/keys/ed25519/ed25519.ts

View check run for this annotation

Codecov / codecov/patch

packages/crypto/src/keys/ed25519/ed25519.ts#L81-L85

Added lines #L81 - L85 were not covered by tests

options?.signal?.throwIfAborted()
return crypto.hashAndSign(this.raw, message)
return sig
}
}
77 changes: 72 additions & 5 deletions packages/crypto/src/keys/ed25519/index.browser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { ed25519 as ed } from '@noble/curves/ed25519'
import { toString as uint8arrayToString } from 'uint8arrays/to-string'
import crypto from '../../webcrypto/index.js'
import type { Uint8ArrayKeyPair } from '../interface.js'
import type { Uint8ArrayList } from 'uint8arraylist'

Expand All @@ -9,6 +11,17 @@ const KEYS_BYTE_LENGTH = 32
export { PUBLIC_KEY_BYTE_LENGTH as publicKeyLength }
export { PRIVATE_KEY_BYTE_LENGTH as privateKeyLength }

// memoize support result to skip additional awaits every time we use an ed key
let ed25519Supported: boolean | undefined
const webCryptoEd25519SupportedPromise = (async () => {
try {
await crypto.get().subtle.generateKey({ name: 'Ed25519' }, true, ['sign', 'verify'])
return true
} catch {
return false
}
})()

export function generateKey (): Uint8ArrayKeyPair {
// the actual private key (32 bytes)
const privateKeyRaw = ed.utils.randomPrivateKey()
Expand All @@ -23,9 +36,6 @@ export function generateKey (): Uint8ArrayKeyPair {
}
}

/**
* Generate keypair from a 32 byte uint8array
*/
export function generateKeyFromSeed (seed: Uint8Array): Uint8ArrayKeyPair {
if (seed.length !== KEYS_BYTE_LENGTH) {
throw new TypeError('"seed" must be 32 bytes in length.')
Expand All @@ -45,16 +55,73 @@ export function generateKeyFromSeed (seed: Uint8Array): Uint8ArrayKeyPair {
}
}

export function hashAndSign (privateKey: Uint8Array, msg: Uint8Array | Uint8ArrayList): Uint8Array {
async function hashAndSignWebCrypto (privateKey: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise<Uint8Array> {
let privateKeyRaw: Uint8Array
if (privateKey.length === PRIVATE_KEY_BYTE_LENGTH) {
privateKeyRaw = privateKey.subarray(0, 32)
} else {
privateKeyRaw = privateKey
}

const jwk: JsonWebKey = {
crv: 'Ed25519',
kty: 'OKP',
x: uint8arrayToString(privateKey.subarray(32), 'base64url'),
d: uint8arrayToString(privateKeyRaw, 'base64url'),
ext: true,
key_ops: ['sign']
}

const key = await crypto.get().subtle.importKey('jwk', jwk, { name: 'Ed25519' }, true, ['sign'])
const sig = await crypto.get().subtle.sign({ name: 'Ed25519' }, key, msg instanceof Uint8Array ? msg : msg.subarray())

return new Uint8Array(sig, 0, sig.byteLength)
}

function hashAndSignNoble (privateKey: Uint8Array, msg: Uint8Array | Uint8ArrayList): Uint8Array {
const privateKeyRaw = privateKey.subarray(0, KEYS_BYTE_LENGTH)

return ed.sign(msg instanceof Uint8Array ? msg : msg.subarray(), privateKeyRaw)
}

export function hashAndVerify (publicKey: Uint8Array, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): boolean {
export async function hashAndSign (privateKey: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise<Uint8Array> {
if (ed25519Supported == null) {
ed25519Supported = await webCryptoEd25519SupportedPromise
}

if (ed25519Supported) {
return hashAndSignWebCrypto(privateKey, msg)
}

return hashAndSignNoble(privateKey, msg)
}

async function hashAndVerifyWebCrypto (publicKey: Uint8Array, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise<boolean> {
if (publicKey.buffer instanceof ArrayBuffer) {
const key = await crypto.get().subtle.importKey('raw', publicKey.buffer, { name: 'Ed25519' }, false, ['verify'])
const isValid = await crypto.get().subtle.verify({ name: 'Ed25519' }, key, sig, msg instanceof Uint8Array ? msg : msg.subarray())
return isValid
}

throw new TypeError('WebCrypto does not support SharedArrayBuffer for Ed25519 keys')
}

function hashAndVerifyNoble (publicKey: Uint8Array, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): boolean {
return ed.verify(sig, msg instanceof Uint8Array ? msg : msg.subarray(), publicKey)
}

export async function hashAndVerify (publicKey: Uint8Array, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise<boolean> {
if (ed25519Supported == null) {
ed25519Supported = await webCryptoEd25519SupportedPromise
}

if (ed25519Supported) {
return hashAndVerifyWebCrypto(publicKey, sig, msg)
}

return hashAndVerifyNoble(publicKey, sig, msg)
}

function concatKeys (privateKeyRaw: Uint8Array, publicKey: Uint8Array): Uint8Array {
const privateKey = new Uint8Array(PRIVATE_KEY_BYTE_LENGTH)
for (let i = 0; i < KEYS_BYTE_LENGTH; i++) {
Expand Down
12 changes: 6 additions & 6 deletions packages/crypto/test/keys/ed25519.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe('ed25519', function () {
it('signs', async () => {
const text = randomBytes(512)
const sig = await key.sign(text)
const res = key.publicKey.verify(text, sig)
const res = await key.publicKey.verify(text, sig)
expect(res).to.be.be.true()
})

Expand All @@ -68,12 +68,12 @@ describe('ed25519', function () {
)
const sig = await key.sign(text)

expect(key.sign(text.subarray()))
expect(await key.sign(text.subarray()))
.to.deep.equal(sig, 'list did not have same signature as a single buffer')

expect(key.publicKey.verify(text, sig))
expect(await key.publicKey.verify(text, sig))
.to.be.true('did not verify message as list')
expect(key.publicKey.verify(text.subarray(), sig))
expect(await key.publicKey.verify(text.subarray(), sig))
.to.be.true('did not verify message as single buffer')
})

Expand Down Expand Up @@ -143,7 +143,7 @@ describe('ed25519', function () {
it('sign and verify', async () => {
const data = uint8ArrayFromString('hello world')
const sig = await key.sign(data)
const valid = key.publicKey.verify(data, sig)
const valid = await key.publicKey.verify(data, sig)
expect(valid).to.be.true()
})

Expand All @@ -159,7 +159,7 @@ describe('ed25519', function () {
it('fails to verify for different data', async () => {
const data = uint8ArrayFromString('hello world')
const sig = await key.sign(data)
const valid = key.publicKey.verify(uint8ArrayFromString('hello'), sig)
const valid = await key.publicKey.verify(uint8ArrayFromString('hello'), sig)
expect(valid).to.be.be.false()
})

Expand Down
Loading