Skip to content

Commit 8e87be9

Browse files
fix: detect Ed25519 support in WebCrypto (#3100)
Try creating an Ed25519 key, if it's successful, use WebCrypto for all Ed25519 operations instead of the pure-js `@noble/curves` module for the performance benefit. --------- Co-authored-by: Alex Potsides <[email protected]>
1 parent bb4ad31 commit 8e87be9

File tree

3 files changed

+102
-15
lines changed

3 files changed

+102
-15
lines changed

packages/crypto/src/keys/ed25519/ed25519.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { base58btc } from 'multiformats/bases/base58'
22
import { CID } from 'multiformats/cid'
33
import { identity } from 'multiformats/hashes/identity'
44
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
5+
import { isPromise } from '../../util.ts'
56
import { publicKeyToProtobuf } from '../index.js'
67
import { ensureEd25519Key } from './utils.js'
78
import * as crypto from './index.js'
@@ -37,9 +38,18 @@ export class Ed25519PublicKey implements Ed25519PublicKeyInterface {
3738
return uint8ArrayEquals(this.raw, key.raw)
3839
}
3940

40-
verify (data: Uint8Array | Uint8ArrayList, sig: Uint8Array, options?: AbortOptions): boolean {
41+
verify (data: Uint8Array | Uint8ArrayList, sig: Uint8Array, options?: AbortOptions): boolean | Promise<boolean> {
4142
options?.signal?.throwIfAborted()
42-
return crypto.hashAndVerify(this.raw, sig, data)
43+
const result = crypto.hashAndVerify(this.raw, sig, data)
44+
45+
if (isPromise<boolean>(result)) {
46+
return result.then(res => {
47+
options?.signal?.throwIfAborted()
48+
return res
49+
})
50+
}
51+
52+
return result
4353
}
4454
}
4555

@@ -63,8 +73,18 @@ export class Ed25519PrivateKey implements Ed25519PrivateKeyInterface {
6373
return uint8ArrayEquals(this.raw, key.raw)
6474
}
6575

66-
sign (message: Uint8Array | Uint8ArrayList, options?: AbortOptions): Uint8Array {
76+
sign (message: Uint8Array | Uint8ArrayList, options?: AbortOptions): Uint8Array | Promise<Uint8Array> {
77+
options?.signal?.throwIfAborted()
78+
const sig = crypto.hashAndSign(this.raw, message)
79+
80+
if (isPromise<Uint8Array>(sig)) {
81+
return sig.then(res => {
82+
options?.signal?.throwIfAborted()
83+
return res
84+
})
85+
}
86+
6787
options?.signal?.throwIfAborted()
68-
return crypto.hashAndSign(this.raw, message)
88+
return sig
6989
}
7090
}

packages/crypto/src/keys/ed25519/index.browser.ts

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { ed25519 as ed } from '@noble/curves/ed25519'
2+
import { toString as uint8arrayToString } from 'uint8arrays/to-string'
3+
import crypto from '../../webcrypto/index.js'
24
import type { Uint8ArrayKeyPair } from '../interface.js'
35
import type { Uint8ArrayList } from 'uint8arraylist'
46

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

14+
// memoize support result to skip additional awaits every time we use an ed key
15+
let ed25519Supported: boolean | undefined
16+
const webCryptoEd25519SupportedPromise = (async () => {
17+
try {
18+
await crypto.get().subtle.generateKey({ name: 'Ed25519' }, true, ['sign', 'verify'])
19+
return true
20+
} catch {
21+
return false
22+
}
23+
})()
24+
1225
export function generateKey (): Uint8ArrayKeyPair {
1326
// the actual private key (32 bytes)
1427
const privateKeyRaw = ed.utils.randomPrivateKey()
@@ -23,9 +36,6 @@ export function generateKey (): Uint8ArrayKeyPair {
2336
}
2437
}
2538

26-
/**
27-
* Generate keypair from a 32 byte uint8array
28-
*/
2939
export function generateKeyFromSeed (seed: Uint8Array): Uint8ArrayKeyPair {
3040
if (seed.length !== KEYS_BYTE_LENGTH) {
3141
throw new TypeError('"seed" must be 32 bytes in length.')
@@ -45,16 +55,73 @@ export function generateKeyFromSeed (seed: Uint8Array): Uint8ArrayKeyPair {
4555
}
4656
}
4757

48-
export function hashAndSign (privateKey: Uint8Array, msg: Uint8Array | Uint8ArrayList): Uint8Array {
58+
async function hashAndSignWebCrypto (privateKey: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise<Uint8Array> {
59+
let privateKeyRaw: Uint8Array
60+
if (privateKey.length === PRIVATE_KEY_BYTE_LENGTH) {
61+
privateKeyRaw = privateKey.subarray(0, 32)
62+
} else {
63+
privateKeyRaw = privateKey
64+
}
65+
66+
const jwk: JsonWebKey = {
67+
crv: 'Ed25519',
68+
kty: 'OKP',
69+
x: uint8arrayToString(privateKey.subarray(32), 'base64url'),
70+
d: uint8arrayToString(privateKeyRaw, 'base64url'),
71+
ext: true,
72+
key_ops: ['sign']
73+
}
74+
75+
const key = await crypto.get().subtle.importKey('jwk', jwk, { name: 'Ed25519' }, true, ['sign'])
76+
const sig = await crypto.get().subtle.sign({ name: 'Ed25519' }, key, msg instanceof Uint8Array ? msg : msg.subarray())
77+
78+
return new Uint8Array(sig, 0, sig.byteLength)
79+
}
80+
81+
function hashAndSignNoble (privateKey: Uint8Array, msg: Uint8Array | Uint8ArrayList): Uint8Array {
4982
const privateKeyRaw = privateKey.subarray(0, KEYS_BYTE_LENGTH)
5083

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

54-
export function hashAndVerify (publicKey: Uint8Array, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): boolean {
87+
export async function hashAndSign (privateKey: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise<Uint8Array> {
88+
if (ed25519Supported == null) {
89+
ed25519Supported = await webCryptoEd25519SupportedPromise
90+
}
91+
92+
if (ed25519Supported) {
93+
return hashAndSignWebCrypto(privateKey, msg)
94+
}
95+
96+
return hashAndSignNoble(privateKey, msg)
97+
}
98+
99+
async function hashAndVerifyWebCrypto (publicKey: Uint8Array, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise<boolean> {
100+
if (publicKey.buffer instanceof ArrayBuffer) {
101+
const key = await crypto.get().subtle.importKey('raw', publicKey.buffer, { name: 'Ed25519' }, false, ['verify'])
102+
const isValid = await crypto.get().subtle.verify({ name: 'Ed25519' }, key, sig, msg instanceof Uint8Array ? msg : msg.subarray())
103+
return isValid
104+
}
105+
106+
throw new TypeError('WebCrypto does not support SharedArrayBuffer for Ed25519 keys')
107+
}
108+
109+
function hashAndVerifyNoble (publicKey: Uint8Array, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): boolean {
55110
return ed.verify(sig, msg instanceof Uint8Array ? msg : msg.subarray(), publicKey)
56111
}
57112

113+
export async function hashAndVerify (publicKey: Uint8Array, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise<boolean> {
114+
if (ed25519Supported == null) {
115+
ed25519Supported = await webCryptoEd25519SupportedPromise
116+
}
117+
118+
if (ed25519Supported) {
119+
return hashAndVerifyWebCrypto(publicKey, sig, msg)
120+
}
121+
122+
return hashAndVerifyNoble(publicKey, sig, msg)
123+
}
124+
58125
function concatKeys (privateKeyRaw: Uint8Array, publicKey: Uint8Array): Uint8Array {
59126
const privateKey = new Uint8Array(PRIVATE_KEY_BYTE_LENGTH)
60127
for (let i = 0; i < KEYS_BYTE_LENGTH; i++) {

packages/crypto/test/keys/ed25519.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ describe('ed25519', function () {
5757
it('signs', async () => {
5858
const text = randomBytes(512)
5959
const sig = await key.sign(text)
60-
const res = key.publicKey.verify(text, sig)
60+
const res = await key.publicKey.verify(text, sig)
6161
expect(res).to.be.be.true()
6262
})
6363

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

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

74-
expect(key.publicKey.verify(text, sig))
74+
expect(await key.publicKey.verify(text, sig))
7575
.to.be.true('did not verify message as list')
76-
expect(key.publicKey.verify(text.subarray(), sig))
76+
expect(await key.publicKey.verify(text.subarray(), sig))
7777
.to.be.true('did not verify message as single buffer')
7878
})
7979

@@ -143,7 +143,7 @@ describe('ed25519', function () {
143143
it('sign and verify', async () => {
144144
const data = uint8ArrayFromString('hello world')
145145
const sig = await key.sign(data)
146-
const valid = key.publicKey.verify(data, sig)
146+
const valid = await key.publicKey.verify(data, sig)
147147
expect(valid).to.be.true()
148148
})
149149

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

0 commit comments

Comments
 (0)