ox 1.1.0 → 1.1.2

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 (184) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/core/Engine.d.ts +43 -11
  3. package/dist/core/Engine.d.ts.map +1 -1
  4. package/dist/core/Engine.js +40 -10
  5. package/dist/core/Engine.js.map +1 -1
  6. package/dist/core/internal/engine.d.ts +8 -8
  7. package/dist/core/internal/engine.d.ts.map +1 -1
  8. package/dist/index.d.ts +5 -2
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +5 -2
  11. package/dist/index.js.map +1 -1
  12. package/dist/node/Ed25519.d.ts +33 -0
  13. package/dist/node/Ed25519.d.ts.map +1 -0
  14. package/dist/node/Ed25519.js +67 -0
  15. package/dist/node/Ed25519.js.map +1 -0
  16. package/dist/node/Engine.d.ts +29 -16
  17. package/dist/node/Engine.d.ts.map +1 -1
  18. package/dist/node/Engine.js +38 -14
  19. package/dist/node/Engine.js.map +1 -1
  20. package/dist/node/Hash.d.ts +14 -16
  21. package/dist/node/Hash.d.ts.map +1 -1
  22. package/dist/node/Hash.js +14 -16
  23. package/dist/node/Hash.js.map +1 -1
  24. package/dist/node/Keystore.d.ts +36 -0
  25. package/dist/node/Keystore.d.ts.map +1 -0
  26. package/dist/node/Keystore.js +72 -0
  27. package/dist/node/Keystore.js.map +1 -0
  28. package/dist/node/Mnemonic.d.ts +35 -0
  29. package/dist/node/Mnemonic.d.ts.map +1 -0
  30. package/dist/node/Mnemonic.js +46 -0
  31. package/dist/node/Mnemonic.js.map +1 -0
  32. package/dist/node/P256.d.ts +32 -0
  33. package/dist/node/P256.d.ts.map +1 -0
  34. package/dist/node/P256.js +34 -0
  35. package/dist/node/P256.js.map +1 -0
  36. package/dist/node/X25519.d.ts +31 -0
  37. package/dist/node/X25519.d.ts.map +1 -0
  38. package/dist/node/X25519.js +71 -0
  39. package/dist/node/X25519.js.map +1 -0
  40. package/dist/node/index.d.ts +95 -8
  41. package/dist/node/index.d.ts.map +1 -1
  42. package/dist/node/index.js +95 -8
  43. package/dist/node/index.js.map +1 -1
  44. package/dist/wasm/Ed25519.d.ts +35 -0
  45. package/dist/wasm/Ed25519.d.ts.map +1 -0
  46. package/dist/wasm/Ed25519.js +88 -0
  47. package/dist/wasm/Ed25519.js.map +1 -0
  48. package/dist/wasm/Engine.d.ts +35 -26
  49. package/dist/wasm/Engine.d.ts.map +1 -1
  50. package/dist/wasm/Engine.js +44 -25
  51. package/dist/wasm/Engine.js.map +1 -1
  52. package/dist/wasm/Hash.d.ts +13 -19
  53. package/dist/wasm/Hash.d.ts.map +1 -1
  54. package/dist/wasm/Hash.js +21 -27
  55. package/dist/wasm/Hash.js.map +1 -1
  56. package/dist/wasm/Keystore.d.ts +37 -0
  57. package/dist/wasm/Keystore.d.ts.map +1 -0
  58. package/dist/wasm/Keystore.js +34 -0
  59. package/dist/wasm/Keystore.js.map +1 -0
  60. package/dist/wasm/Mnemonic.d.ts +33 -0
  61. package/dist/wasm/Mnemonic.d.ts.map +1 -0
  62. package/dist/wasm/Mnemonic.js +30 -0
  63. package/dist/wasm/Mnemonic.js.map +1 -0
  64. package/dist/wasm/X25519.d.ts +33 -0
  65. package/dist/wasm/X25519.d.ts.map +1 -0
  66. package/dist/wasm/X25519.js +53 -0
  67. package/dist/wasm/X25519.js.map +1 -0
  68. package/dist/wasm/index.d.ts +81 -12
  69. package/dist/wasm/index.d.ts.map +1 -1
  70. package/dist/wasm/index.js +81 -12
  71. package/dist/wasm/index.js.map +1 -1
  72. package/dist/wasm/internal/blake3.d.ts +11 -0
  73. package/dist/wasm/internal/blake3.d.ts.map +1 -0
  74. package/dist/wasm/internal/blake3.js +31 -0
  75. package/dist/wasm/internal/blake3.js.map +1 -0
  76. package/dist/wasm/internal/blake3.wasm.d.ts +3 -0
  77. package/dist/wasm/internal/blake3.wasm.d.ts.map +1 -0
  78. package/dist/wasm/internal/blake3.wasm.js +10 -0
  79. package/dist/wasm/internal/blake3.wasm.js.map +1 -0
  80. package/dist/wasm/internal/crypto25519.d.ts +16 -0
  81. package/dist/wasm/internal/crypto25519.d.ts.map +1 -0
  82. package/dist/wasm/internal/crypto25519.js +5 -0
  83. package/dist/wasm/internal/crypto25519.js.map +1 -0
  84. package/dist/wasm/internal/crypto25519.wasm.d.ts +3 -0
  85. package/dist/wasm/internal/crypto25519.wasm.d.ts.map +1 -0
  86. package/dist/wasm/internal/crypto25519.wasm.js +10 -0
  87. package/dist/wasm/internal/crypto25519.wasm.js.map +1 -0
  88. package/dist/wasm/internal/ed25519.d.ts +4 -0
  89. package/dist/wasm/internal/ed25519.d.ts.map +1 -0
  90. package/dist/wasm/internal/ed25519.js +21 -0
  91. package/dist/wasm/internal/ed25519.js.map +1 -0
  92. package/dist/wasm/internal/hashes.d.ts +12 -0
  93. package/dist/wasm/internal/hashes.d.ts.map +1 -0
  94. package/dist/wasm/internal/hashes.js +8 -0
  95. package/dist/wasm/internal/hashes.js.map +1 -0
  96. package/dist/wasm/internal/hashes.wasm.d.ts +4 -2
  97. package/dist/wasm/internal/hashes.wasm.d.ts.map +1 -1
  98. package/dist/wasm/internal/hashes.wasm.js +4 -2
  99. package/dist/wasm/internal/hashes.wasm.js.map +1 -1
  100. package/dist/wasm/internal/mnemonic.d.ts +4 -0
  101. package/dist/wasm/internal/mnemonic.d.ts.map +1 -0
  102. package/dist/wasm/internal/mnemonic.js +45 -0
  103. package/dist/wasm/internal/mnemonic.js.map +1 -0
  104. package/dist/wasm/internal/pbkdf2.d.ts +12 -0
  105. package/dist/wasm/internal/pbkdf2.d.ts.map +1 -0
  106. package/dist/wasm/internal/pbkdf2.js +51 -0
  107. package/dist/wasm/internal/pbkdf2.js.map +1 -0
  108. package/dist/wasm/internal/x25519.d.ts +4 -0
  109. package/dist/wasm/internal/x25519.d.ts.map +1 -0
  110. package/dist/wasm/internal/x25519.js +21 -0
  111. package/dist/wasm/internal/x25519.js.map +1 -0
  112. package/package.json +46 -1
  113. package/src/core/Engine.ts +84 -11
  114. package/src/core/_test/Engine.test-d.ts +56 -0
  115. package/src/core/_test/Engine.test.ts +95 -0
  116. package/src/core/_test/Hash.vectors.test.ts +8 -0
  117. package/src/core/internal/engine.ts +8 -8
  118. package/src/index.ts +5 -2
  119. package/src/node/Ed25519.ts +87 -0
  120. package/src/node/Engine.ts +79 -23
  121. package/src/node/Hash.ts +22 -27
  122. package/src/node/Keystore.ts +103 -0
  123. package/src/node/Mnemonic.ts +68 -0
  124. package/src/node/P256.ts +48 -0
  125. package/src/node/X25519.ts +93 -0
  126. package/src/node/_test/Curves.fuzz.ts +76 -0
  127. package/src/node/_test/Ed25519.test-d.ts +35 -0
  128. package/src/node/_test/Ed25519.test.ts +220 -0
  129. package/src/node/_test/Engine.test-d.ts +28 -0
  130. package/src/node/_test/Engine.test.ts +62 -22
  131. package/src/node/_test/Hash.test-d.ts +15 -9
  132. package/src/node/_test/Hash.test.ts +10 -11
  133. package/src/node/_test/Keystore.fuzz.ts +58 -0
  134. package/src/node/_test/Keystore.test-d.ts +43 -0
  135. package/src/node/_test/Keystore.test.ts +198 -0
  136. package/src/node/_test/Mnemonic.fuzz.ts +31 -0
  137. package/src/node/_test/Mnemonic.test-d.ts +21 -0
  138. package/src/node/_test/Mnemonic.test.ts +98 -0
  139. package/src/node/_test/P256.test-d.ts +29 -0
  140. package/src/node/_test/P256.test.ts +102 -0
  141. package/src/node/_test/X25519.test-d.ts +28 -0
  142. package/src/node/_test/X25519.test.ts +239 -0
  143. package/src/node/index.ts +100 -8
  144. package/src/version.ts +1 -1
  145. package/src/wasm/Ed25519.ts +114 -0
  146. package/src/wasm/Engine.ts +76 -31
  147. package/src/wasm/Hash.bench.ts +2 -2
  148. package/src/wasm/Hash.ts +29 -50
  149. package/src/wasm/Keystore.ts +48 -0
  150. package/src/wasm/Mnemonic.ts +44 -0
  151. package/src/wasm/X25519.ts +70 -0
  152. package/src/wasm/_test/Crypto25519.conformance.ts +57 -0
  153. package/src/wasm/_test/Crypto25519.fuzz.ts +187 -0
  154. package/src/wasm/_test/Ed25519.test-d.ts +28 -0
  155. package/src/wasm/_test/Ed25519.test.ts +168 -0
  156. package/src/wasm/_test/Ed25519.vectors.test.ts +73 -0
  157. package/src/wasm/_test/Engine.test-d.ts +28 -0
  158. package/src/wasm/_test/Engine.test.ts +59 -25
  159. package/src/wasm/_test/Hash.browser.test.ts +11 -7
  160. package/src/wasm/_test/Hash.fuzz.ts +58 -23
  161. package/src/wasm/_test/Hash.test-d.ts +15 -10
  162. package/src/wasm/_test/Hash.test.ts +148 -48
  163. package/src/wasm/_test/Hash.vectors.test.ts +15 -8
  164. package/src/wasm/_test/Keystore.browser.test.ts +42 -0
  165. package/src/wasm/_test/Keystore.fuzz.ts +113 -0
  166. package/src/wasm/_test/Keystore.test-d.ts +33 -0
  167. package/src/wasm/_test/Keystore.test.ts +192 -0
  168. package/src/wasm/_test/Keystore.vectors.test.ts +21 -0
  169. package/src/wasm/_test/Mnemonic.test-d.ts +17 -0
  170. package/src/wasm/_test/Mnemonic.test.ts +110 -0
  171. package/src/wasm/_test/X25519.test-d.ts +21 -0
  172. package/src/wasm/_test/X25519.test.ts +194 -0
  173. package/src/wasm/_test/fixtures.ts +4 -0
  174. package/src/wasm/index.ts +85 -12
  175. package/src/wasm/internal/blake3.ts +45 -0
  176. package/src/wasm/internal/blake3.wasm.ts +11 -0
  177. package/src/wasm/internal/crypto25519.ts +43 -0
  178. package/src/wasm/internal/crypto25519.wasm.ts +11 -0
  179. package/src/wasm/internal/ed25519.ts +31 -0
  180. package/src/wasm/internal/hashes.ts +29 -0
  181. package/src/wasm/internal/hashes.wasm.ts +5 -2
  182. package/src/wasm/internal/mnemonic.ts +56 -0
  183. package/src/wasm/internal/pbkdf2.ts +86 -0
  184. package/src/wasm/internal/x25519.ts +29 -0
@@ -0,0 +1,68 @@
1
+ import * as crypto from 'node:crypto'
2
+ import type * as Engine from '../core/Engine.js'
3
+ import type * as Errors from '../core/Errors.js'
4
+
5
+ export * from '../core/Mnemonic.js'
6
+
7
+ /**
8
+ * Creates a Node.js implementation of the [`Mnemonic`](/api/Mnemonic) engine
9
+ * slot, without installing it.
10
+ *
11
+ * Most callers want [`Engine.install`](/node/crypto/Engine/install) instead,
12
+ * which installs every implementation this entrypoint provides. Reach for this
13
+ * to install or hold the `Mnemonic` slot on its own.
14
+ *
15
+ * @example
16
+ * ```ts twoslash
17
+ * // @noErrors
18
+ * import { Engine } from 'ox'
19
+ * import { Mnemonic } from 'ox/node'
20
+ *
21
+ * await Engine.install({ Mnemonic: Mnemonic.engine() })
22
+ *
23
+ * Mnemonic.toSeed(
24
+ * 'test test test test test test test test test test test junk'
25
+ * )
26
+ * ```
27
+ *
28
+ * @returns The raw `Mnemonic` engine slot.
29
+ */
30
+ export function engine(): Promise<engine.ReturnType> {
31
+ return Promise.resolve({
32
+ toSeed: (mnemonic, passphrase = '') => {
33
+ if (typeof mnemonic !== 'string')
34
+ throw new TypeError(`invalid mnemonic type: ${typeof mnemonic}`)
35
+
36
+ const normalized = mnemonic.normalize('NFKD')
37
+ if (![12, 15, 18, 21, 24].includes(normalized.split(' ').length))
38
+ throw new Error('Invalid mnemonic')
39
+
40
+ return copyAndClear(
41
+ crypto.pbkdf2Sync(
42
+ normalized,
43
+ `mnemonic${passphrase}`.normalize('NFKD'),
44
+ 2048,
45
+ 64,
46
+ 'sha512',
47
+ ),
48
+ )
49
+ },
50
+ })
51
+ }
52
+
53
+ export declare namespace engine {
54
+ /** Every `Mnemonic` primitive this module implements. */
55
+ type ReturnType = {
56
+ [key in 'toSeed']-?: NonNullable<Engine.Mnemonic[key]>
57
+ }
58
+
59
+ type ErrorType = Errors.GlobalErrorType
60
+ }
61
+
62
+ function copyAndClear(value: Buffer): Uint8Array {
63
+ try {
64
+ return Uint8Array.from(value)
65
+ } finally {
66
+ value.fill(0)
67
+ }
68
+ }
@@ -0,0 +1,48 @@
1
+ import * as crypto from 'node:crypto'
2
+ import type * as Engine from '../core/Engine.js'
3
+ import type * as Errors from '../core/Errors.js'
4
+
5
+ export * from '../core/P256.js'
6
+
7
+ /**
8
+ * Creates a Node.js implementation of the [`P256`](/api/P256) engine slot,
9
+ * without installing it.
10
+ *
11
+ * Node's other P256 operations do not reproduce Ox's recovered-signature and
12
+ * compressed-shared-point contracts, so they remain on Ox's defaults.
13
+ *
14
+ * @example
15
+ * ```ts twoslash
16
+ * // @noErrors
17
+ * import { Engine } from 'ox'
18
+ * import { P256 } from 'ox/node'
19
+ *
20
+ * await Engine.install({ P256: P256.engine() })
21
+ *
22
+ * P256.getPublicKey({ privateKey: '0x...' })
23
+ * ```
24
+ *
25
+ * @returns The raw `P256` engine slot.
26
+ */
27
+ export function engine(): Promise<engine.ReturnType> {
28
+ return Promise.resolve({
29
+ getPublicKey: (privateKey) => {
30
+ if (privateKey.length !== 32)
31
+ throw new RangeError(
32
+ `P256 private key must be 32 bytes, got ${privateKey.length}`,
33
+ )
34
+ const ecdh = crypto.createECDH('prime256v1')
35
+ ecdh.setPrivateKey(privateKey)
36
+ return new Uint8Array(ecdh.getPublicKey(undefined, 'uncompressed'))
37
+ },
38
+ })
39
+ }
40
+
41
+ export declare namespace engine {
42
+ /** Every `P256` primitive this module implements. */
43
+ type ReturnType = {
44
+ getPublicKey: NonNullable<Engine.Ecdsa['getPublicKey']>
45
+ }
46
+
47
+ type ErrorType = Errors.GlobalErrorType
48
+ }
@@ -0,0 +1,93 @@
1
+ import * as crypto from 'node:crypto'
2
+ import type * as Engine from '../core/Engine.js'
3
+ import type * as Errors from '../core/Errors.js'
4
+
5
+ export * from '../core/X25519.js'
6
+
7
+ const privateKeyPrefix = Buffer.from('302e020100300506032b656e04220420', 'hex')
8
+ const publicKeyPrefix = Buffer.from('302a300506032b656e032100', 'hex')
9
+
10
+ /**
11
+ * Creates a Node.js implementation of the [`X25519`](/api/X25519) engine slot,
12
+ * without installing it.
13
+ *
14
+ * Random key generation remains on Ox's default implementation.
15
+ *
16
+ * @example
17
+ * ```ts twoslash
18
+ * // @noErrors
19
+ * import { Engine } from 'ox'
20
+ * import { X25519 } from 'ox/node'
21
+ *
22
+ * await Engine.install({ X25519: X25519.engine() })
23
+ *
24
+ * X25519.getPublicKey({ privateKey: '0x...' })
25
+ * ```
26
+ *
27
+ * @returns The raw `X25519` engine slot.
28
+ */
29
+ export function engine(): Promise<engine.ReturnType> {
30
+ return Promise.resolve({
31
+ getPublicKey: (privateKey) =>
32
+ rawPublicKey(crypto.createPublicKey(toPrivateKey(privateKey))),
33
+ getSharedSecret: (privateKey, publicKey) =>
34
+ copyAndClear(
35
+ crypto.diffieHellman({
36
+ privateKey: toPrivateKey(privateKey),
37
+ publicKey: toPublicKey(publicKey),
38
+ }),
39
+ ),
40
+ })
41
+ }
42
+
43
+ export declare namespace engine {
44
+ /** Every `X25519` primitive this module implements. */
45
+ type ReturnType = {
46
+ [key in 'getPublicKey' | 'getSharedSecret']-?: NonNullable<Engine.Ecdh[key]>
47
+ }
48
+
49
+ type ErrorType = Errors.GlobalErrorType
50
+ }
51
+
52
+ function copyAndClear(value: Buffer): Uint8Array {
53
+ try {
54
+ return Uint8Array.from(value)
55
+ } finally {
56
+ value.fill(0)
57
+ }
58
+ }
59
+
60
+ function assertLength(key: Uint8Array, type: 'private' | 'public'): void {
61
+ if (key.length !== 32)
62
+ throw new RangeError(
63
+ `X25519 ${type} key must be 32 bytes, got ${key.length}`,
64
+ )
65
+ }
66
+
67
+ function rawPublicKey(publicKey: crypto.KeyObject): Uint8Array {
68
+ const der = publicKey.export({ format: 'der', type: 'spki' })
69
+ return Uint8Array.from(der.subarray(-32))
70
+ }
71
+
72
+ function toPrivateKey(privateKey: Uint8Array): crypto.KeyObject {
73
+ assertLength(privateKey, 'private')
74
+ const der = Buffer.concat([privateKeyPrefix, privateKey])
75
+ try {
76
+ return crypto.createPrivateKey({
77
+ format: 'der',
78
+ key: der,
79
+ type: 'pkcs8',
80
+ })
81
+ } finally {
82
+ der.fill(0)
83
+ }
84
+ }
85
+
86
+ function toPublicKey(publicKey: Uint8Array): crypto.KeyObject {
87
+ assertLength(publicKey, 'public')
88
+ return crypto.createPublicKey({
89
+ format: 'der',
90
+ key: Buffer.concat([publicKeyPrefix, publicKey]),
91
+ type: 'spki',
92
+ })
93
+ }
@@ -0,0 +1,76 @@
1
+ import { fc, test } from '@fast-check/vitest'
2
+ import { ed25519, x25519 } from '@noble/curves/ed25519.js'
3
+ import { p256 } from '@noble/curves/nist.js'
4
+ import { describe, expect } from 'vp/test'
5
+ import { numRuns } from '../../../test/fuzz/numRuns.js'
6
+ import * as Ed25519 from '../Ed25519.js'
7
+ import * as P256 from '../P256.js'
8
+ import * as X25519 from '../X25519.js'
9
+
10
+ const arbitraryBytes = fc.uint8Array({ maxLength: 512 })
11
+ const arbitraryKey = fc.uint8Array({ maxLength: 32, minLength: 32 })
12
+ const arbitraryP256PrivateKey = fc
13
+ .tuple(
14
+ fc.integer({ max: 0xfe, min: 1 }),
15
+ fc.uint8Array({ maxLength: 31, minLength: 31 }),
16
+ )
17
+ .map(([first, rest]) => Uint8Array.of(first, ...rest))
18
+
19
+ describe('Ed25519', () => {
20
+ test.prop({ payload: arbitraryBytes, privateKey: arbitraryKey }, { numRuns })(
21
+ 'matches the default for arbitrary offset views',
22
+ async (options) => {
23
+ const engine = await Ed25519.engine()
24
+ const payload = offsetView(options.payload)
25
+ const privateKey = offsetView(options.privateKey)
26
+
27
+ expect(engine.getPublicKey(privateKey)).toEqual(
28
+ ed25519.getPublicKey(privateKey),
29
+ )
30
+ expect(engine.sign(payload, privateKey)).toEqual(
31
+ ed25519.sign(payload, privateKey),
32
+ )
33
+ expect(engine.toMontgomerySecret(privateKey)).toEqual(
34
+ ed25519.utils.toMontgomerySecret(privateKey),
35
+ )
36
+ },
37
+ )
38
+ })
39
+
40
+ describe('X25519', () => {
41
+ test.prop(
42
+ { privateKey: arbitraryKey, publicKeySeed: arbitraryKey },
43
+ { numRuns },
44
+ )('matches the default for arbitrary offset views', async (options) => {
45
+ const engine = await X25519.engine()
46
+ const privateKey = offsetView(options.privateKey)
47
+ const publicKey = offsetView(x25519.getPublicKey(options.publicKeySeed))
48
+
49
+ expect(engine.getPublicKey(privateKey)).toEqual(
50
+ x25519.getPublicKey(privateKey),
51
+ )
52
+ expect(engine.getSharedSecret(privateKey, publicKey)).toEqual(
53
+ x25519.getSharedSecret(privateKey, publicKey),
54
+ )
55
+ })
56
+ })
57
+
58
+ describe('P256', () => {
59
+ test.prop({ privateKey: arbitraryP256PrivateKey }, { numRuns })(
60
+ 'matches the default for arbitrary offset views',
61
+ async (options) => {
62
+ const engine = await P256.engine()
63
+ const privateKey = offsetView(options.privateKey)
64
+
65
+ expect(engine.getPublicKey(privateKey)).toEqual(
66
+ p256.getPublicKey(privateKey, false),
67
+ )
68
+ },
69
+ )
70
+ })
71
+
72
+ function offsetView(value: Uint8Array): Uint8Array {
73
+ const bytes = new Uint8Array(value.length + 4).fill(0xff)
74
+ bytes.set(value, 2)
75
+ return bytes.subarray(2, -2)
76
+ }
@@ -0,0 +1,35 @@
1
+ import { expectTypeOf, test } from 'vp/test'
2
+ import type * as Engine from '../../core/Engine.js'
3
+ import * as core_Ed25519 from '../../core/Ed25519.js'
4
+ import * as NodeEd25519 from '../Ed25519.js'
5
+
6
+ type Slot = Awaited<ReturnType<typeof NodeEd25519.engine>>
7
+
8
+ test('every implemented primitive is present', () => {
9
+ expectTypeOf<Slot['getPublicKey']>().toEqualTypeOf<
10
+ (privateKey: Uint8Array) => Uint8Array
11
+ >()
12
+ expectTypeOf<Slot['sign']>().toEqualTypeOf<
13
+ (payload: Uint8Array, privateKey: Uint8Array) => Uint8Array
14
+ >()
15
+ expectTypeOf<Slot['toMontgomerySecret']>().toEqualTypeOf<
16
+ (privateKey: Uint8Array) => Uint8Array
17
+ >()
18
+ })
19
+
20
+ test('unsupported primitives are absent', () => {
21
+ expectTypeOf<Slot>().not.toHaveProperty('randomSecretKey')
22
+ expectTypeOf<Slot>().not.toHaveProperty('toMontgomery')
23
+ expectTypeOf<Slot>().not.toHaveProperty('verify')
24
+ })
25
+
26
+ test('the result is the raw slot', () => {
27
+ expectTypeOf<{ Ed25519: Slot }>().toExtend<Engine.Engine>()
28
+ })
29
+
30
+ test('the Node namespace exposes the public Ed25519 API', () => {
31
+ expectTypeOf(NodeEd25519.getPublicKey).toEqualTypeOf(
32
+ core_Ed25519.getPublicKey,
33
+ )
34
+ expectTypeOf<typeof NodeEd25519>().not.toHaveProperty('create')
35
+ })
@@ -0,0 +1,220 @@
1
+ import * as crypto from 'node:crypto'
2
+ import * as fs from 'node:fs'
3
+ import { ed25519 } from '@noble/curves/ed25519.js'
4
+ import { Hex } from 'ox'
5
+ import { describe, expect, test } from 'vp/test'
6
+ import * as Ed25519 from '../Ed25519.js'
7
+
8
+ type Zip215Vector = {
9
+ sig_bytes: string
10
+ valid_legacy: boolean
11
+ valid_zip215: boolean
12
+ vk_bytes: string
13
+ }
14
+
15
+ const publicKeyPrefix = Buffer.from('302a300506032b6570032100', 'hex')
16
+ const zip215Text = fs.readFileSync(
17
+ new URL('../../../test/vectors/ed25519/zip215.json', import.meta.url),
18
+ 'utf8',
19
+ )
20
+ const zip215 = JSON.parse(zip215Text) as readonly Zip215Vector[]
21
+
22
+ const vectors = [
23
+ {
24
+ payload: '',
25
+ privateKey:
26
+ '9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60',
27
+ },
28
+ {
29
+ payload: '72',
30
+ privateKey:
31
+ '4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb',
32
+ },
33
+ ] as const
34
+
35
+ describe('engine', () => {
36
+ test('behavior: exposes only primitives compatible with Ox semantics', async () => {
37
+ const engine = await Ed25519.engine()
38
+
39
+ // Node verification is deliberately absent because it does not implement
40
+ // the ZIP-215 rules used by Ox's default verifier.
41
+ expect(Object.keys(engine).sort()).toMatchInlineSnapshot(`
42
+ [
43
+ "getPublicKey",
44
+ "sign",
45
+ "toMontgomerySecret",
46
+ ]
47
+ `)
48
+ })
49
+
50
+ test('behavior: matches RFC 8032 test vectors', async () => {
51
+ const engine = await Ed25519.engine()
52
+
53
+ expect(
54
+ vectors.map(({ payload, privateKey }) => ({
55
+ publicKey: Hex.fromBytes(engine.getPublicKey(fromHex(privateKey))),
56
+ signature: Hex.fromBytes(
57
+ engine.sign(fromHex(payload), fromHex(privateKey)),
58
+ ),
59
+ })),
60
+ ).toMatchInlineSnapshot(`
61
+ [
62
+ {
63
+ "publicKey": "0xd75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",
64
+ "signature": "0xe5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b",
65
+ },
66
+ {
67
+ "publicKey": "0x3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c",
68
+ "signature": "0x92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00",
69
+ },
70
+ ]
71
+ `)
72
+ })
73
+
74
+ test('behavior: omits verification that disagrees with ZIP-215', async () => {
75
+ const engine = await Ed25519.engine()
76
+ const digest = crypto
77
+ .createHash('sha256')
78
+ .update(JSON.stringify(zip215))
79
+ .digest('hex')
80
+ const message = new TextEncoder().encode('Zcash')
81
+ let defaultAccepted = 0
82
+ let nodeAccepted = 0
83
+
84
+ for (const vector of zip215) {
85
+ const publicKey = fromHex(vector.vk_bytes)
86
+ const signature = fromHex(vector.sig_bytes)
87
+ if (ed25519.verify(signature, message, publicKey)) defaultAccepted++
88
+ if (nodeVerify(vector, message)) nodeAccepted++
89
+ }
90
+
91
+ expect({
92
+ defaultAccepted,
93
+ digest,
94
+ legacyAccepted: zip215.filter((vector) => vector.valid_legacy).length,
95
+ nodeDisagrees: nodeAccepted < defaultAccepted,
96
+ vectors: zip215.length,
97
+ }).toMatchInlineSnapshot(`
98
+ {
99
+ "defaultAccepted": 196,
100
+ "digest": "e99ef30c602df87b8d5970dea07f35bcd5993b003e5682ca2c8dc4dab9de4335",
101
+ "legacyAccepted": 3,
102
+ "nodeDisagrees": true,
103
+ "vectors": 196,
104
+ }
105
+ `)
106
+ expect('verify' in engine).toMatchInlineSnapshot('false')
107
+ })
108
+
109
+ test('behavior: matches the default across messages and private keys', async () => {
110
+ const engine = await Ed25519.engine()
111
+
112
+ for (const [index, { privateKey }] of vectors.entries()) {
113
+ const key = fromHex(privateKey)
114
+ for (const size of [0, 1, 31, 32, 63, 64, 65, 255]) {
115
+ const payload = Uint8Array.from(
116
+ { length: size },
117
+ (_, offset) => (offset * 29 + index) % 251,
118
+ )
119
+ expect(engine.sign(payload, key)).toEqual(ed25519.sign(payload, key))
120
+ }
121
+ expect(engine.getPublicKey(key)).toEqual(ed25519.getPublicKey(key))
122
+ expect(engine.toMontgomerySecret(key)).toEqual(
123
+ ed25519.utils.toMontgomerySecret(key),
124
+ )
125
+ }
126
+ })
127
+
128
+ test('behavior: respects typed-array offsets without mutating inputs', async () => {
129
+ const engine = await Ed25519.engine()
130
+ const privateKey = offsetView(fromHex(vectors[0].privateKey))
131
+ const payload = offsetView(
132
+ Uint8Array.from({ length: 65 }, (_, index) => (index * 17) % 251),
133
+ )
134
+ const privateKeyBefore = privateKey.slice()
135
+ const payloadBefore = payload.slice()
136
+
137
+ expect(engine.getPublicKey(privateKey)).toEqual(
138
+ ed25519.getPublicKey(privateKey),
139
+ )
140
+ expect(engine.sign(payload, privateKey)).toEqual(
141
+ ed25519.sign(payload, privateKey),
142
+ )
143
+ expect(engine.toMontgomerySecret(privateKey)).toEqual(
144
+ ed25519.utils.toMontgomerySecret(privateKey),
145
+ )
146
+ expect({ payload, privateKey }).toEqual({
147
+ payload: payloadBefore,
148
+ privateKey: privateKeyBefore,
149
+ })
150
+ })
151
+
152
+ test('behavior: rejects malformed private-key lengths', async () => {
153
+ const engine = await Ed25519.engine()
154
+
155
+ for (const size of [0, 1, 31, 33, 64]) {
156
+ const privateKey = new Uint8Array(size)
157
+ expect(() => engine.getPublicKey(privateKey)).toThrowError(
158
+ `Ed25519 private key must be 32 bytes, got ${size}`,
159
+ )
160
+ expect(() => engine.sign(new Uint8Array(), privateKey)).toThrowError(
161
+ `Ed25519 private key must be 32 bytes, got ${size}`,
162
+ )
163
+ expect(() => engine.toMontgomerySecret(privateKey)).toThrowError(
164
+ `Ed25519 private key must be 32 bytes, got ${size}`,
165
+ )
166
+ }
167
+ })
168
+
169
+ test('behavior: returns owned Uint8Array values', async () => {
170
+ const engine = await Ed25519.engine()
171
+ const privateKey = fromHex(vectors[0].privateKey)
172
+ const outputs = [
173
+ engine.getPublicKey(privateKey),
174
+ engine.sign(new Uint8Array(), privateKey),
175
+ engine.toMontgomerySecret(privateKey),
176
+ ]
177
+
178
+ expect(outputs.map((output) => output.constructor === Uint8Array))
179
+ .toMatchInlineSnapshot(`
180
+ [
181
+ true,
182
+ true,
183
+ true,
184
+ ]
185
+ `)
186
+ expect(
187
+ engine.getPublicKey(privateKey) === engine.getPublicKey(privateKey),
188
+ ).toMatchInlineSnapshot('false')
189
+ })
190
+
191
+ test('behavior: returns a fresh engine', async () => {
192
+ const first = await Ed25519.engine()
193
+ const second = await Ed25519.engine()
194
+
195
+ expect(first === second).toMatchInlineSnapshot('false')
196
+ })
197
+ })
198
+
199
+ function fromHex(value: string): Uint8Array {
200
+ return Hex.toBytes(`0x${value}`)
201
+ }
202
+
203
+ function offsetView(value: Uint8Array): Uint8Array {
204
+ const bytes = new Uint8Array(value.length + 4).fill(0xff)
205
+ bytes.set(value, 2)
206
+ return bytes.subarray(2, -2)
207
+ }
208
+
209
+ function nodeVerify(vector: Zip215Vector, message: Uint8Array): boolean {
210
+ try {
211
+ const publicKey = crypto.createPublicKey({
212
+ format: 'der',
213
+ key: Buffer.concat([publicKeyPrefix, fromHex(vector.vk_bytes)]),
214
+ type: 'spki',
215
+ })
216
+ return crypto.verify(null, message, publicKey, fromHex(vector.sig_bytes))
217
+ } catch {
218
+ return false
219
+ }
220
+ }
@@ -0,0 +1,28 @@
1
+ import { Engine } from 'ox/node'
2
+ import { expectTypeOf, test } from 'vp/test'
3
+ import * as CoreEngine from '../../core/Engine.js'
4
+
5
+ type Installed = Awaited<ReturnType<typeof Engine.install>>
6
+ type Uninstalled = Awaited<ReturnType<typeof Engine.engine>>
7
+
8
+ test('install and engine expose the same precise engine', () => {
9
+ expectTypeOf<Installed>().toEqualTypeOf<Uninstalled>()
10
+ expectTypeOf<Installed['Hash']['sha256']>().toEqualTypeOf<
11
+ (input: Uint8Array) => Uint8Array
12
+ >()
13
+ })
14
+
15
+ test('the Node Engine namespace exposes synchronous core operations', () => {
16
+ expectTypeOf(Engine.get).toEqualTypeOf(CoreEngine.get)
17
+ expectTypeOf(Engine.InvalidSlotValueError).toEqualTypeOf(
18
+ CoreEngine.InvalidSlotValueError,
19
+ )
20
+ expectTypeOf(Engine.reset).toEqualTypeOf(CoreEngine.reset)
21
+ expectTypeOf(Engine.set).toEqualTypeOf(CoreEngine.set)
22
+ expectTypeOf(Engine.with).toEqualTypeOf(CoreEngine.with)
23
+ })
24
+
25
+ test('the replaced aliases are absent', () => {
26
+ expectTypeOf<typeof Engine>().not.toHaveProperty('create')
27
+ expectTypeOf<typeof Engine>().not.toHaveProperty('load')
28
+ })
@@ -1,22 +1,27 @@
1
- import { Engine as CoreEngine, Hash } from 'ox'
2
- import { Engine } from 'ox/node'
1
+ import { Engine as CoreEngine } from 'ox'
2
+ import { Engine, Hash } from 'ox/node'
3
3
  import { describe, expect, test } from 'vp/test'
4
4
 
5
- describe('create', () => {
5
+ describe('engine', () => {
6
6
  test('behavior: does not install', async () => {
7
- const engine = await Engine.create()
7
+ const engine = await Engine.engine()
8
8
 
9
9
  expect(Object.keys(engine)).toMatchInlineSnapshot(`
10
10
  [
11
+ "Ed25519",
11
12
  "Hash",
13
+ "Keystore",
14
+ "Mnemonic",
15
+ "P256",
16
+ "X25519",
12
17
  ]
13
18
  `)
14
- expect(CoreEngine.get()).toMatchInlineSnapshot('{}')
19
+ expect(Engine.get()).toMatchInlineSnapshot('{}')
15
20
  })
16
21
 
17
22
  test('behavior: installs for the duration of a call', async () => {
18
- const node = await Engine.create()
19
- const digest = CoreEngine.with(node, () => Hash.sha256('0xdeadbeef'))
23
+ const node = await Engine.engine()
24
+ const digest = Engine.with(node, () => Hash.sha256('0xdeadbeef'))
20
25
 
21
26
  expect(digest).toMatchInlineSnapshot(
22
27
  `"0x5f78c33274e43fa9de5659265c1d917e25c03722dcb0b8d27db8d5feaa813953"`,
@@ -25,23 +30,53 @@ describe('create', () => {
25
30
  })
26
31
  })
27
32
 
28
- describe('load', () => {
33
+ describe('install', () => {
29
34
  test('behavior: installs and returns what it installed', async () => {
30
- const engine = await Engine.load()
35
+ const engine = await Engine.install()
31
36
 
32
- expect(Object.keys(CoreEngine.get())).toEqual(Object.keys(engine))
33
- expect(Object.keys(CoreEngine.get().Hash ?? {}).sort())
34
- .toMatchInlineSnapshot(`
35
- [
36
- "hmacSha256",
37
- "ripemd160",
38
- "sha256",
39
- ]
40
- `)
37
+ expect(Engine.get()).toEqual(CoreEngine.get())
38
+ expect(Object.keys(Engine.get())).toEqual(Object.keys(engine))
39
+ expect(
40
+ Object.fromEntries(
41
+ Object.entries(Engine.get()).map(([slot, value]) => [
42
+ slot,
43
+ Object.keys(value ?? {}).sort(),
44
+ ]),
45
+ ),
46
+ ).toMatchInlineSnapshot(`
47
+ {
48
+ "Ed25519": [
49
+ "getPublicKey",
50
+ "sign",
51
+ "toMontgomerySecret",
52
+ ],
53
+ "Hash": [
54
+ "hmacSha256",
55
+ "ripemd160",
56
+ "sha256",
57
+ ],
58
+ "Keystore": [
59
+ "aesCtrDecrypt",
60
+ "aesCtrEncrypt",
61
+ "pbkdf2Sha256",
62
+ "pbkdf2Sha256Async",
63
+ ],
64
+ "Mnemonic": [
65
+ "toSeed",
66
+ ],
67
+ "P256": [
68
+ "getPublicKey",
69
+ ],
70
+ "X25519": [
71
+ "getPublicKey",
72
+ "getSharedSecret",
73
+ ],
74
+ }
75
+ `)
41
76
  })
42
77
 
43
78
  test('behavior: leaves unsupported primitives on the default', async () => {
44
- await Engine.load()
79
+ await Engine.install()
45
80
 
46
81
  expect(Hash.keccak256('0xdeadbeef')).toMatchInlineSnapshot(
47
82
  `"0xd4fd4e189132273036449fc9e11198c739161b4c0116a9a2dccdfa1c492006f1"`,
@@ -49,13 +84,18 @@ describe('load', () => {
49
84
  })
50
85
 
51
86
  test('behavior: merges with later engines', async () => {
52
- await Engine.load()
53
- CoreEngine.set({ Mnemonic: { toSeed: () => new Uint8Array(64) } })
87
+ await Engine.install()
88
+ Engine.set({ Bls: { randomSecretKey: () => new Uint8Array(32) } })
54
89
 
55
- expect(Object.keys(CoreEngine.get()).sort()).toMatchInlineSnapshot(`
90
+ expect(Object.keys(Engine.get()).sort()).toMatchInlineSnapshot(`
56
91
  [
92
+ "Bls",
93
+ "Ed25519",
57
94
  "Hash",
95
+ "Keystore",
58
96
  "Mnemonic",
97
+ "P256",
98
+ "X25519",
59
99
  ]
60
100
  `)
61
101
  })