ox 1.6.3 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/core/AesGcm.d.ts +67 -2
  3. package/dist/core/AesGcm.d.ts.map +1 -1
  4. package/dist/core/AesGcm.js +79 -10
  5. package/dist/core/AesGcm.js.map +1 -1
  6. package/dist/core/Ed25519.d.ts +83 -2
  7. package/dist/core/Ed25519.d.ts.map +1 -1
  8. package/dist/core/Ed25519.js +79 -4
  9. package/dist/core/Ed25519.js.map +1 -1
  10. package/dist/core/MlDsa44.d.ts +84 -5
  11. package/dist/core/MlDsa44.d.ts.map +1 -1
  12. package/dist/core/MlDsa44.js +79 -4
  13. package/dist/core/MlDsa44.js.map +1 -1
  14. package/dist/core/P256.d.ts +82 -1
  15. package/dist/core/P256.d.ts.map +1 -1
  16. package/dist/core/P256.js +78 -0
  17. package/dist/core/P256.js.map +1 -1
  18. package/dist/core/Secp256k1.d.ts +85 -1
  19. package/dist/core/Secp256k1.d.ts.map +1 -1
  20. package/dist/core/Secp256k1.js +74 -3
  21. package/dist/core/Secp256k1.js.map +1 -1
  22. package/dist/core/Siwe.d.ts.map +1 -1
  23. package/dist/core/Siwe.js +45 -6
  24. package/dist/core/Siwe.js.map +1 -1
  25. package/dist/core/internal/keyDerivation.d.ts +13 -0
  26. package/dist/core/internal/keyDerivation.d.ts.map +1 -0
  27. package/dist/core/internal/keyDerivation.js +13 -0
  28. package/dist/core/internal/keyDerivation.js.map +1 -0
  29. package/package.json +1 -1
  30. package/src/core/AesGcm.ts +137 -30
  31. package/src/core/Ed25519.ts +135 -4
  32. package/src/core/MlDsa44.ts +135 -4
  33. package/src/core/P256.ts +135 -1
  34. package/src/core/Secp256k1.ts +134 -3
  35. package/src/core/Siwe.ts +45 -5
  36. package/src/core/_test/AesGcm.test-d.ts +19 -0
  37. package/src/core/_test/AesGcm.test.ts +90 -1
  38. package/src/core/_test/Ed25519.test-d.ts +24 -0
  39. package/src/core/_test/Ed25519.test.ts +87 -1
  40. package/src/core/_test/MlDsa44.test-d.ts +24 -0
  41. package/src/core/_test/MlDsa44.test.ts +69 -1
  42. package/src/core/_test/P256.test-d.ts +26 -0
  43. package/src/core/_test/P256.test.ts +98 -1
  44. package/src/core/_test/Secp256k1.test-d.ts +27 -0
  45. package/src/core/_test/Secp256k1.test.ts +112 -1
  46. package/src/core/_test/Siwe.test.ts +98 -0
  47. package/src/core/internal/keyDerivation.ts +34 -0
  48. package/src/version.ts +1 -1
package/src/core/Siwe.ts CHANGED
@@ -23,6 +23,36 @@ export const prefixRegex =
23
23
  export const suffixRegex =
24
24
  /(?:URI: (?<uri>.+))\n(?:Version: (?<version>.+))\n(?:Chain ID: (?<chainId>\d+))\n(?:Nonce: (?<nonce>[a-zA-Z0-9]+))\n(?:Issued At: (?<issuedAt>.+))(?:\nExpiration Time: (?<expirationTime>.+))?(?:\nNot Before: (?<notBefore>.+))?(?:\nRequest ID: (?<requestId>.+))?/
25
25
 
26
+ const siweDateTimeRegex =
27
+ /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])[Tt]([01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/
28
+
29
+ function parseSiweDateTime(value: string): Date {
30
+ const match = value.match(siweDateTimeRegex)
31
+ if (!match) return new Date(Number.NaN)
32
+
33
+ const year = Number(match[1])
34
+ const month = Number(match[2])
35
+ const day = Number(match[3])
36
+ const days = [
37
+ 31,
38
+ year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28,
39
+ 31,
40
+ 30,
41
+ 31,
42
+ 30,
43
+ 31,
44
+ 31,
45
+ 30,
46
+ 31,
47
+ 30,
48
+ 31,
49
+ ]
50
+ const daysInMonth = days[month - 1]
51
+ if (!daysInMonth || day > daysInMonth) return new Date(Number.NaN)
52
+
53
+ return new Date(value.toUpperCase())
54
+ }
55
+
26
56
  /** [EIP-4361](https://eips.ethereum.org/EIPS/eip-4361) message fields. */
27
57
  export type Message = {
28
58
  /**
@@ -388,9 +418,11 @@ export function parseMessage(message: string): ExactPartial<Message> {
388
418
  ...prefix,
389
419
  ...suffix,
390
420
  ...(chainId ? { chainId: Number(chainId) } : {}),
391
- ...(expirationTime ? { expirationTime: new Date(expirationTime) } : {}),
392
- ...(issuedAt ? { issuedAt: new Date(issuedAt) } : {}),
393
- ...(notBefore ? { notBefore: new Date(notBefore) } : {}),
421
+ ...(expirationTime
422
+ ? { expirationTime: parseSiweDateTime(expirationTime) }
423
+ : {}),
424
+ ...(issuedAt ? { issuedAt: parseSiweDateTime(issuedAt) } : {}),
425
+ ...(notBefore ? { notBefore: parseSiweDateTime(notBefore) } : {}),
394
426
  ...(requestId ? { requestId } : {}),
395
427
  ...(resources ? { resources } : {}),
396
428
  ...(scheme ? { scheme } : {}),
@@ -431,8 +463,16 @@ export function validateMessage(value: validateMessage.Value): boolean {
431
463
  if (nonce && message.nonce !== nonce) return false
432
464
  if (scheme && message.scheme !== scheme) return false
433
465
 
434
- if (message.expirationTime && time >= message.expirationTime) return false
435
- if (message.notBefore && time < message.notBefore) return false
466
+ if (Number.isNaN(time.getTime())) return false
467
+
468
+ if (message.expirationTime) {
469
+ if (Number.isNaN(message.expirationTime.getTime())) return false
470
+ if (time >= message.expirationTime) return false
471
+ }
472
+ if (message.notBefore) {
473
+ if (Number.isNaN(message.notBefore.getTime())) return false
474
+ if (time < message.notBefore) return false
475
+ }
436
476
 
437
477
  try {
438
478
  if (!message.address) return false
@@ -0,0 +1,19 @@
1
+ import { AesGcm } from 'ox'
2
+ import { expectTypeOf, test } from 'vp/test'
3
+
4
+ test('fromMnemonic', () => {
5
+ const mnemonic = 'test test test test test test test test test test test junk'
6
+
7
+ expectTypeOf(AesGcm.fromMnemonic(mnemonic)).toEqualTypeOf<
8
+ Promise<CryptoKey>
9
+ >()
10
+ expectTypeOf(
11
+ AesGcm.fromMnemonic(mnemonic, { passphrase: 'qwerty' }),
12
+ ).toEqualTypeOf<Promise<CryptoKey>>()
13
+ })
14
+
15
+ test('fromSeed', () => {
16
+ expectTypeOf(AesGcm.fromSeed(new Uint8Array(32))).toEqualTypeOf<
17
+ Promise<CryptoKey>
18
+ >()
19
+ })
@@ -1,4 +1,4 @@
1
- import { AesGcm, Bytes, Hex } from 'ox'
1
+ import { AesGcm, Bytes, Hex, Mnemonic } from 'ox'
2
2
  import { describe, expect, test } from 'vp/test'
3
3
 
4
4
  describe('decrypt', () => {
@@ -159,6 +159,92 @@ describe('fromPrf', () => {
159
159
  })
160
160
  })
161
161
 
162
+ describe('fromMnemonic', () => {
163
+ const mnemonic = 'test test test test test test test test test test test junk'
164
+
165
+ test('default', async () => {
166
+ const key = await AesGcm.fromMnemonic(mnemonic)
167
+ const expectedKey = await AesGcm.fromSeed(Mnemonic.toSeed(mnemonic))
168
+ const value = Bytes.fromString('i am a secret message')
169
+ const encrypted = await AesGcm.encrypt(value, key)
170
+
171
+ await expect(AesGcm.decrypt(encrypted, expectedKey)).resolves.toEqual(value)
172
+ })
173
+
174
+ test('options: passphrase', async () => {
175
+ const key = await AesGcm.fromMnemonic(mnemonic, {
176
+ passphrase: 'qwerty',
177
+ })
178
+ const defaultKey = await AesGcm.fromMnemonic(mnemonic)
179
+ const encrypted = await AesGcm.encrypt('0xdeadbeef', key)
180
+
181
+ await expect(AesGcm.decrypt(encrypted, defaultKey)).rejects.toThrowError()
182
+ })
183
+ })
184
+
185
+ describe('fromSeed', () => {
186
+ const seed =
187
+ '0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'
188
+
189
+ test('vector', async () => {
190
+ const key = await AesGcm.fromSeed(seed)
191
+ const expectedKey = await globalThis.crypto.subtle.importKey(
192
+ 'raw',
193
+ Bytes.fromHex(
194
+ '0xd42ffe5cb894ce61e9f448e8083f53b2ab90d2acad0929e80b0abb85e915cb3a',
195
+ ),
196
+ { name: 'AES-GCM' },
197
+ false,
198
+ ['decrypt'],
199
+ )
200
+ const value = Bytes.fromString('i am a secret message')
201
+ const encrypted = await AesGcm.encrypt(value, key)
202
+
203
+ await expect(AesGcm.decrypt(encrypted, expectedKey)).resolves.toEqual(value)
204
+ })
205
+
206
+ test('value: Bytes', async () => {
207
+ const key = await AesGcm.fromSeed(Bytes.fromHex(seed))
208
+ const value = Bytes.fromString('i am a secret message')
209
+
210
+ await expect(
211
+ AesGcm.decrypt(await AesGcm.encrypt(value, key), key),
212
+ ).resolves.toEqual(value)
213
+ })
214
+
215
+ test('behavior: accepts seeds longer than 32 bytes', async () => {
216
+ const key = await AesGcm.fromSeed(new Uint8Array(64))
217
+
218
+ expect({
219
+ algorithm: key.algorithm,
220
+ extractable: key.extractable,
221
+ type: key.type,
222
+ usages: key.usages,
223
+ }).toMatchInlineSnapshot(`
224
+ {
225
+ "algorithm": {
226
+ "length": 256,
227
+ "name": "AES-GCM",
228
+ },
229
+ "extractable": false,
230
+ "type": "secret",
231
+ "usages": [
232
+ "encrypt",
233
+ "decrypt",
234
+ ],
235
+ }
236
+ `)
237
+ })
238
+
239
+ test('error: seed is too short', async () => {
240
+ await expect(
241
+ AesGcm.fromSeed(new Uint8Array(31)),
242
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
243
+ `[AesGcm.InvalidSeedSizeError: Seed must contain at least 32 bytes. Received 31 bytes.]`,
244
+ )
245
+ })
246
+ })
247
+
162
248
  describe('getKey', () => {
163
249
  test('default', async () => {
164
250
  const key = await AesGcm.getKey({ password: 'qwerty' })
@@ -180,9 +266,12 @@ test('exports', () => {
180
266
  "decrypt",
181
267
  "encrypt",
182
268
  "fromPrf",
269
+ "fromMnemonic",
270
+ "fromSeed",
183
271
  "getKey",
184
272
  "randomSalt",
185
273
  "InvalidPrfSizeError",
274
+ "InvalidSeedSizeError",
186
275
  ]
187
276
  `)
188
277
  })
@@ -40,6 +40,30 @@ test('fromPrf', () => {
40
40
  ).toEqualTypeOf<Bytes.Bytes>()
41
41
  })
42
42
 
43
+ test('fromMnemonic', () => {
44
+ const mnemonic = 'test test test test test test test test test test test junk'
45
+
46
+ expectTypeOf(Ed25519.fromMnemonic(mnemonic)).toEqualTypeOf<Hex.Hex>()
47
+ expectTypeOf(
48
+ Ed25519.fromMnemonic(mnemonic, { passphrase: 'qwerty' }),
49
+ ).toEqualTypeOf<Hex.Hex>()
50
+ expectTypeOf(
51
+ Ed25519.fromMnemonic(mnemonic, { as: 'Bytes' }),
52
+ ).toEqualTypeOf<Bytes.Bytes>()
53
+ })
54
+
55
+ test('fromSeed', () => {
56
+ const seed =
57
+ '0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'
58
+
59
+ expectTypeOf(Ed25519.fromSeed(seed)).toEqualTypeOf<Hex.Hex>()
60
+ expectTypeOf(Ed25519.fromSeed(new Uint8Array(32))).toEqualTypeOf<Hex.Hex>()
61
+ expectTypeOf(Ed25519.fromSeed(seed, { as: 'Hex' })).toEqualTypeOf<Hex.Hex>()
62
+ expectTypeOf(
63
+ Ed25519.fromSeed(seed, { as: 'Bytes' }),
64
+ ).toEqualTypeOf<Bytes.Bytes>()
65
+ })
66
+
43
67
  test('getPublicKey', () => {
44
68
  // Default behavior (Hex)
45
69
  {
@@ -1,4 +1,4 @@
1
- import { Bytes, Ed25519, Engine, Hex, X25519 } from 'ox'
1
+ import { Bytes, Ed25519, Engine, Hex, Mnemonic, X25519 } from 'ox'
2
2
  import { describe, expect, test } from 'vp/test'
3
3
 
4
4
  describe('createKeyPair', () => {
@@ -146,6 +146,89 @@ describe('fromPrf', () => {
146
146
  })
147
147
  })
148
148
 
149
+ describe('fromMnemonic', () => {
150
+ const mnemonic = 'test test test test test test test test test test test junk'
151
+
152
+ test('default', () => {
153
+ const privateKey = Ed25519.fromMnemonic(mnemonic)
154
+
155
+ expect(privateKey).toBe(Ed25519.fromSeed(Mnemonic.toSeed(mnemonic)))
156
+ expect(privateKey).toMatchInlineSnapshot(
157
+ `"0x23fb07a427d2bc36141d1e6c7e56c72679739eff374a8e8def282f1048674567"`,
158
+ )
159
+ })
160
+
161
+ test('options: passphrase', () => {
162
+ expect(
163
+ Ed25519.fromMnemonic(mnemonic, { passphrase: 'qwerty' }),
164
+ ).toMatchInlineSnapshot(
165
+ `"0xe888791c9d783e772d92b0bf2aa326b1cda8214a03c3c07933615b1a98fc8651"`,
166
+ )
167
+ })
168
+
169
+ test('options: as', () => {
170
+ const privateKey = Ed25519.fromMnemonic(mnemonic, { as: 'Bytes' })
171
+
172
+ expect(privateKey).toBeInstanceOf(Uint8Array)
173
+ expect(privateKey).toHaveLength(32)
174
+ })
175
+ })
176
+
177
+ describe('fromSeed', () => {
178
+ const seed =
179
+ '0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'
180
+
181
+ test('vector', () => {
182
+ expect(Ed25519.fromSeed(seed)).toMatchInlineSnapshot(
183
+ `"0xfd09c7b2acda46034df7e428377fdc37200094c9b51690fa1b733ee3c16a2d07"`,
184
+ )
185
+ })
186
+
187
+ test('value: Bytes', () => {
188
+ expect(Ed25519.fromSeed(Bytes.fromHex(seed))).toMatchInlineSnapshot(
189
+ `"0xfd09c7b2acda46034df7e428377fdc37200094c9b51690fa1b733ee3c16a2d07"`,
190
+ )
191
+ })
192
+
193
+ test('options: as', () => {
194
+ expect(Ed25519.fromSeed(seed, { as: 'Bytes' })).toEqual(
195
+ Bytes.fromHex(
196
+ '0xfd09c7b2acda46034df7e428377fdc37200094c9b51690fa1b733ee3c16a2d07',
197
+ ),
198
+ )
199
+ })
200
+
201
+ test('behavior: output signs and verifies', () => {
202
+ const privateKey = Ed25519.fromSeed(new Uint8Array(64))
203
+ const publicKey = Ed25519.getPublicKey({ privateKey })
204
+ const payload = '0xdeadbeef'
205
+ const signature = Ed25519.sign({ payload, privateKey })
206
+
207
+ expect(Ed25519.verify({ payload, publicKey, signature })).toBe(true)
208
+ })
209
+
210
+ test('behavior: zeroes intermediate key for Hex output', () => {
211
+ const candidate = new Uint8Array(32).fill(1)
212
+ Engine.with(
213
+ {
214
+ Hash: {
215
+ hmacSha256: () => candidate,
216
+ },
217
+ },
218
+ () => Ed25519.fromSeed(new Uint8Array(32)),
219
+ )
220
+
221
+ expect(candidate).toEqual(new Uint8Array(32))
222
+ })
223
+
224
+ test('error: seed is too short', () => {
225
+ expect(() => Ed25519.fromSeed(new Uint8Array(31)))
226
+ .toThrowErrorMatchingInlineSnapshot(`
227
+ [Ed25519.InvalidSeedSizeError: Seed must contain at least 32 bytes. Received 31 bytes.]
228
+ `)
229
+ })
230
+ })
231
+
149
232
  describe('getPublicKey', () => {
150
233
  const privateKey =
151
234
  '0x9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60'
@@ -545,6 +628,8 @@ test('exports', () => {
545
628
  "noble",
546
629
  "createKeyPair",
547
630
  "fromPrf",
631
+ "fromMnemonic",
632
+ "fromSeed",
548
633
  "getPublicKey",
549
634
  "randomPrivateKey",
550
635
  "sign",
@@ -552,6 +637,7 @@ test('exports', () => {
552
637
  "toX25519PublicKey",
553
638
  "toX25519PrivateKey",
554
639
  "InvalidPrfSizeError",
640
+ "InvalidSeedSizeError",
555
641
  ]
556
642
  `)
557
643
  })
@@ -24,6 +24,30 @@ test('fromPrf', () => {
24
24
  ).toEqualTypeOf<Bytes.Bytes>()
25
25
  })
26
26
 
27
+ test('fromMnemonic', () => {
28
+ const mnemonic = 'test test test test test test test test test test test junk'
29
+
30
+ expectTypeOf(MlDsa44.fromMnemonic(mnemonic)).toEqualTypeOf<Hex.Hex>()
31
+ expectTypeOf(
32
+ MlDsa44.fromMnemonic(mnemonic, { passphrase: 'qwerty' }),
33
+ ).toEqualTypeOf<Hex.Hex>()
34
+ expectTypeOf(
35
+ MlDsa44.fromMnemonic(mnemonic, { as: 'Bytes' }),
36
+ ).toEqualTypeOf<Bytes.Bytes>()
37
+ })
38
+
39
+ test('fromSeed', () => {
40
+ const seed =
41
+ '0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'
42
+
43
+ expectTypeOf(MlDsa44.fromSeed(seed)).toEqualTypeOf<Hex.Hex>()
44
+ expectTypeOf(MlDsa44.fromSeed(new Uint8Array(32))).toEqualTypeOf<Hex.Hex>()
45
+ expectTypeOf(MlDsa44.fromSeed(seed, { as: 'Hex' })).toEqualTypeOf<Hex.Hex>()
46
+ expectTypeOf(
47
+ MlDsa44.fromSeed(seed, { as: 'Bytes' }),
48
+ ).toEqualTypeOf<Bytes.Bytes>()
49
+ })
50
+
27
51
  test('getPublicKey', () => {
28
52
  expectTypeOf(
29
53
  MlDsa44.getPublicKey({ privateKey: '0x' }),
@@ -1,4 +1,4 @@
1
- import { Bytes, Engine, Hash, Hex, MlDsa44 } from 'ox'
1
+ import { Bytes, Engine, Hash, Hex, MlDsa44, Mnemonic } from 'ox'
2
2
  import { describe, expect, test } from 'vp/test'
3
3
 
4
4
  const privateKey = Hex.fromBytes(new Uint8Array(32).fill(7))
@@ -139,6 +139,74 @@ describe('fromPrf', () => {
139
139
  })
140
140
  })
141
141
 
142
+ describe('fromMnemonic', () => {
143
+ const mnemonic = 'test test test test test test test test test test test junk'
144
+
145
+ test('default', () => {
146
+ const privateKey = MlDsa44.fromMnemonic(mnemonic)
147
+
148
+ expect(privateKey).toBe(MlDsa44.fromSeed(Mnemonic.toSeed(mnemonic)))
149
+ expect(privateKey).toMatchInlineSnapshot(
150
+ `"0x787f747b571a3511deac4d045a7876ae2349f45b9f9f4e3a6ee40342313a963f"`,
151
+ )
152
+ })
153
+
154
+ test('options: passphrase', () => {
155
+ expect(
156
+ MlDsa44.fromMnemonic(mnemonic, { passphrase: 'qwerty' }),
157
+ ).toMatchInlineSnapshot(
158
+ `"0x51202c4aaf8b4090f94c43ae43234b133a2646642fbc4ca39d8883a0b39d2b44"`,
159
+ )
160
+ })
161
+
162
+ test('options: as', () => {
163
+ const privateKey = MlDsa44.fromMnemonic(mnemonic, { as: 'Bytes' })
164
+
165
+ expect(privateKey).toBeInstanceOf(Uint8Array)
166
+ expect(privateKey).toHaveLength(32)
167
+ })
168
+ })
169
+
170
+ describe('fromSeed', () => {
171
+ const seed =
172
+ '0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'
173
+
174
+ test('vector', () => {
175
+ expect(MlDsa44.fromSeed(seed)).toMatchInlineSnapshot(
176
+ `"0xbee74160aef452ec0e90a4c347bbb0ac37e93fb94855f7a98dfe9afabebbdad6"`,
177
+ )
178
+ })
179
+
180
+ test('value: Bytes', () => {
181
+ expect(MlDsa44.fromSeed(Bytes.fromHex(seed))).toMatchInlineSnapshot(
182
+ `"0xbee74160aef452ec0e90a4c347bbb0ac37e93fb94855f7a98dfe9afabebbdad6"`,
183
+ )
184
+ })
185
+
186
+ test('options: as', () => {
187
+ expect(MlDsa44.fromSeed(seed, { as: 'Bytes' })).toEqual(
188
+ Bytes.fromHex(
189
+ '0xbee74160aef452ec0e90a4c347bbb0ac37e93fb94855f7a98dfe9afabebbdad6',
190
+ ),
191
+ )
192
+ })
193
+
194
+ test('behavior: output signs and verifies', () => {
195
+ const privateKey = MlDsa44.fromSeed(new Uint8Array(64))
196
+ const publicKey = MlDsa44.getPublicKey({ privateKey })
197
+ const signature = MlDsa44.sign({ payload, privateKey })
198
+
199
+ expect(MlDsa44.verify({ payload, publicKey, signature })).toBe(true)
200
+ })
201
+
202
+ test('error: seed is too short', () => {
203
+ expect(() => MlDsa44.fromSeed(new Uint8Array(31)))
204
+ .toThrowErrorMatchingInlineSnapshot(`
205
+ [MlDsa44.InvalidSeedSizeError: Seed must contain at least 32 bytes. Received 31 bytes.]
206
+ `)
207
+ })
208
+ })
209
+
142
210
  describe('getPublicKey', () => {
143
211
  test('default', () => {
144
212
  const publicKey = MlDsa44.getPublicKey({ privateKey })
@@ -0,0 +1,26 @@
1
+ import { type Bytes, type Hex, P256 } from 'ox'
2
+ import { expectTypeOf, test } from 'vp/test'
3
+
4
+ test('fromMnemonic', () => {
5
+ const mnemonic = 'test test test test test test test test test test test junk'
6
+
7
+ expectTypeOf(P256.fromMnemonic(mnemonic)).toEqualTypeOf<Hex.Hex>()
8
+ expectTypeOf(
9
+ P256.fromMnemonic(mnemonic, { passphrase: 'qwerty' }),
10
+ ).toEqualTypeOf<Hex.Hex>()
11
+ expectTypeOf(
12
+ P256.fromMnemonic(mnemonic, { as: 'Bytes' }),
13
+ ).toEqualTypeOf<Bytes.Bytes>()
14
+ })
15
+
16
+ test('fromSeed', () => {
17
+ const seed =
18
+ '0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'
19
+
20
+ expectTypeOf(P256.fromSeed(seed)).toEqualTypeOf<Hex.Hex>()
21
+ expectTypeOf(P256.fromSeed(new Uint8Array(32))).toEqualTypeOf<Hex.Hex>()
22
+ expectTypeOf(P256.fromSeed(seed, { as: 'Hex' })).toEqualTypeOf<Hex.Hex>()
23
+ expectTypeOf(
24
+ P256.fromSeed(seed, { as: 'Bytes' }),
25
+ ).toEqualTypeOf<Bytes.Bytes>()
26
+ })
@@ -1,4 +1,4 @@
1
- import { Bytes, Hex, P256 } from 'ox'
1
+ import { Bytes, Engine, Hex, Mnemonic, P256 } from 'ox'
2
2
  import { describe, expect, test } from 'vp/test'
3
3
  import { accounts } from '../../../test/constants/accounts.js'
4
4
 
@@ -145,6 +145,100 @@ describe('createKeyPair', () => {
145
145
  })
146
146
  })
147
147
 
148
+ describe('fromMnemonic', () => {
149
+ const mnemonic = 'test test test test test test test test test test test junk'
150
+
151
+ test('default', () => {
152
+ const privateKey = P256.fromMnemonic(mnemonic)
153
+
154
+ expect(privateKey).toBe(P256.fromSeed(Mnemonic.toSeed(mnemonic)))
155
+ expect(privateKey).toMatchInlineSnapshot(
156
+ `"0x882f5b02a84c96bceeebf9c868bec73041d51b041ae05380e66a41508648ebc3"`,
157
+ )
158
+ })
159
+
160
+ test('options: passphrase', () => {
161
+ expect(
162
+ P256.fromMnemonic(mnemonic, { passphrase: 'qwerty' }),
163
+ ).toMatchInlineSnapshot(
164
+ `"0x364ff539f37ce477d49a35476c9ad32a2a3fed1d1237eaee7abdfd07655ad9f5"`,
165
+ )
166
+ })
167
+
168
+ test('options: as', () => {
169
+ const privateKey = P256.fromMnemonic(mnemonic, { as: 'Bytes' })
170
+
171
+ expect(privateKey).toBeInstanceOf(Uint8Array)
172
+ expect(privateKey).toHaveLength(32)
173
+ })
174
+ })
175
+
176
+ describe('fromSeed', () => {
177
+ const seed =
178
+ '0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'
179
+
180
+ test('vector', () => {
181
+ expect(P256.fromSeed(seed)).toMatchInlineSnapshot(
182
+ `"0x724009e76eb27e34eb77b93db354b9015ecd4296deb57561f5214bab68b94a6f"`,
183
+ )
184
+ })
185
+
186
+ test('value: Bytes', () => {
187
+ expect(P256.fromSeed(Bytes.fromHex(seed))).toMatchInlineSnapshot(
188
+ `"0x724009e76eb27e34eb77b93db354b9015ecd4296deb57561f5214bab68b94a6f"`,
189
+ )
190
+ })
191
+
192
+ test('options: as', () => {
193
+ expect(P256.fromSeed(seed, { as: 'Bytes' })).toEqual(
194
+ Bytes.fromHex(
195
+ '0x724009e76eb27e34eb77b93db354b9015ecd4296deb57561f5214bab68b94a6f',
196
+ ),
197
+ )
198
+ })
199
+
200
+ test('behavior: output is a valid P256 private key', () => {
201
+ const privateKey = P256.fromSeed(new Uint8Array(64), { as: 'Bytes' })
202
+
203
+ expect(P256.noble.utils.isValidSecretKey(privateKey)).toBe(true)
204
+ })
205
+
206
+ test('behavior: skips invalid scalar candidates', () => {
207
+ const messages: Hex.Hex[] = []
208
+ const privateKey = Engine.with(
209
+ {
210
+ Hash: {
211
+ hmacSha256: (_key, message) => {
212
+ messages.push(Hex.fromBytes(message))
213
+ if (messages.length === 1) return new Uint8Array(32)
214
+ return Bytes.fromHex(
215
+ '0xef3f982137910a4c362017e69ba173fbb342325802ec827767aa6942027af35b',
216
+ )
217
+ },
218
+ },
219
+ },
220
+ () => P256.fromSeed(new Uint8Array(32)),
221
+ )
222
+
223
+ expect(privateKey).toBe(
224
+ '0xef3f982137910a4c362017e69ba173fbb342325802ec827767aa6942027af35b',
225
+ )
226
+ expect(messages).toMatchInlineSnapshot(`
227
+ [
228
+ "0x6f782e703235362e66726f6d536565642e763100000000",
229
+ "0x6f782e703235362e66726f6d536565642e763100000001",
230
+ ]
231
+ `)
232
+ })
233
+
234
+ test('error: seed is too short', () => {
235
+ expect(() => P256.fromSeed(new Uint8Array(31)))
236
+ .toThrowErrorMatchingInlineSnapshot(`
237
+ [P256.InvalidSeedSizeError: Seed must contain at least 32 bytes. Received 31 bytes.]
238
+ `)
239
+ })
240
+ })
241
+
148
242
  describe('getSharedSecret', () => {
149
243
  test('default', () => {
150
244
  const { privateKey: privateKeyA, publicKey: publicKeyA } =
@@ -546,12 +640,15 @@ test('exports', () => {
546
640
  [
547
641
  "noble",
548
642
  "createKeyPair",
643
+ "fromMnemonic",
644
+ "fromSeed",
549
645
  "getPublicKey",
550
646
  "getSharedSecret",
551
647
  "randomPrivateKey",
552
648
  "recoverPublicKey",
553
649
  "sign",
554
650
  "verify",
651
+ "InvalidSeedSizeError",
555
652
  ]
556
653
  `)
557
654
  })
@@ -12,3 +12,30 @@ test('fromPrf', () => {
12
12
  Secp256k1.fromPrf(prf, { as: 'Bytes' }),
13
13
  ).toEqualTypeOf<Bytes.Bytes>()
14
14
  })
15
+
16
+ test('fromMnemonic', () => {
17
+ const mnemonic = 'test test test test test test test test test test test junk'
18
+
19
+ expectTypeOf(Secp256k1.fromMnemonic(mnemonic)).toEqualTypeOf<Hex.Hex>()
20
+ expectTypeOf(
21
+ Secp256k1.fromMnemonic(mnemonic, { passphrase: 'qwerty' }),
22
+ ).toEqualTypeOf<Hex.Hex>()
23
+ expectTypeOf(
24
+ Secp256k1.fromMnemonic(mnemonic, { as: 'Bytes' }),
25
+ ).toEqualTypeOf<Bytes.Bytes>()
26
+ expectTypeOf(
27
+ Secp256k1.fromMnemonic(mnemonic, { path: "m/44'/60'/0'/0/1" }),
28
+ ).toEqualTypeOf<Hex.Hex>()
29
+ })
30
+
31
+ test('fromSeed', () => {
32
+ const seed =
33
+ '0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'
34
+
35
+ expectTypeOf(Secp256k1.fromSeed(seed)).toEqualTypeOf<Hex.Hex>()
36
+ expectTypeOf(Secp256k1.fromSeed(new Uint8Array(32))).toEqualTypeOf<Hex.Hex>()
37
+ expectTypeOf(Secp256k1.fromSeed(seed, { as: 'Hex' })).toEqualTypeOf<Hex.Hex>()
38
+ expectTypeOf(
39
+ Secp256k1.fromSeed(seed, { as: 'Bytes' }),
40
+ ).toEqualTypeOf<Bytes.Bytes>()
41
+ })