Skip to content

feat(Ed25519-Key): Improve validation speeds in browsers #3100

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions packages/crypto/src/keys/ed25519/ed25519.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ export class Ed25519PublicKey implements Ed25519PublicKeyInterface {
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 = await crypto.hashAndVerify(this.raw, sig, data)
options?.signal?.throwIfAborted()
return result
}
}

Expand All @@ -63,8 +65,10 @@ export class Ed25519PrivateKey implements Ed25519PrivateKeyInterface {
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)
options?.signal?.throwIfAborted()
return crypto.hashAndSign(this.raw, message)
return sig
}
}
65 changes: 60 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,15 @@ const KEYS_BYTE_LENGTH = 32
export { PUBLIC_KEY_BYTE_LENGTH as publicKeyLength }
export { PRIVATE_KEY_BYTE_LENGTH as privateKeyLength }

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 +34,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 +53,63 @@ 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 buffer = await crypto.get().subtle.sign({ name: 'Ed25519' }, key, msg instanceof Uint8Array ? msg : msg.subarray())
return new Uint8Array(buffer)
}

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 (await webCryptoEd25519SupportedPromise) {
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 (await webCryptoEd25519SupportedPromise) {
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
13 changes: 6 additions & 7 deletions packages/crypto/test/keys/ed25519.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ describe('ed25519', function () {

before(async () => {
key = await generateKeyPair('Ed25519')

if (key.type !== 'Ed25519') {
throw new Error('Key was incorrect type')
}
Expand Down Expand Up @@ -57,7 +56,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 +67,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 +142,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 +158,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