hd-wallet-wasm 2.0.3 → 2.0.5
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.
- package/package.json +3 -1
- package/src/sdn-plugin-manifest-source.mjs +307 -0
- package/src/sdn-plugin.mjs +482 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hd-wallet-wasm",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
4
4
|
"description": "Comprehensive HD Wallet implementation in WebAssembly - BIP-32/39/44, multi-curve, multi-chain support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.mjs",
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
"files": [
|
|
28
28
|
"src/index.mjs",
|
|
29
29
|
"src/index.d.ts",
|
|
30
|
+
"src/sdn-plugin.mjs",
|
|
31
|
+
"src/sdn-plugin-manifest-source.mjs",
|
|
30
32
|
"src/epm-attestation.mjs",
|
|
31
33
|
"src/aligned.mjs",
|
|
32
34
|
"src/aligned.d.ts",
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
export const HD_WALLET_SDN_PLUGIN_ID =
|
|
2
|
+
'com.digitalarsenal.infrastructure.hd-wallet-wasm';
|
|
3
|
+
|
|
4
|
+
export const HD_WALLET_SDN_PLUGIN_MANIFEST = Object.freeze({
|
|
5
|
+
pluginId: HD_WALLET_SDN_PLUGIN_ID,
|
|
6
|
+
name: 'HD Wallet Crypto',
|
|
7
|
+
version: '2.0.1',
|
|
8
|
+
pluginFamily: 'infrastructure',
|
|
9
|
+
description:
|
|
10
|
+
'Native-crypto infrastructure plugin surface for detached signing and field-level encryption inside an sdn-flow runtime.',
|
|
11
|
+
capabilities: [
|
|
12
|
+
{
|
|
13
|
+
capabilityId: 'random',
|
|
14
|
+
required: true,
|
|
15
|
+
description:
|
|
16
|
+
'Explicit host entropy for IVs, salts, and ephemeral encryption material.',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
capabilityId: 'wallet_sign',
|
|
20
|
+
required: false,
|
|
21
|
+
description:
|
|
22
|
+
'Optional host-resident key surface for signing when private key bytes are not carried in the request payload.',
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
externalInterfaces: [
|
|
26
|
+
{
|
|
27
|
+
interfaceId: 'wallet-active-key',
|
|
28
|
+
kind: 'host-service',
|
|
29
|
+
direction: 'bidirectional',
|
|
30
|
+
capability: 'wallet_sign',
|
|
31
|
+
resource: 'wallet://active-key',
|
|
32
|
+
required: false,
|
|
33
|
+
description:
|
|
34
|
+
'Optional host-provided signing and key-agreement surface for resident key material.',
|
|
35
|
+
properties: {
|
|
36
|
+
curves: ['secp256k1', 'ed25519', 'x25519'],
|
|
37
|
+
residentKeyMaterial: true,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
interfaceId: 'host-rng',
|
|
42
|
+
kind: 'host-service',
|
|
43
|
+
direction: 'input',
|
|
44
|
+
capability: 'random',
|
|
45
|
+
resource: 'host-rng://default',
|
|
46
|
+
required: true,
|
|
47
|
+
description:
|
|
48
|
+
'Explicit entropy source injected by the host for salts, IVs, and ephemeral sender keys.',
|
|
49
|
+
properties: {
|
|
50
|
+
provides: ['salt', 'iv', 'ephemeral-private-key'],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
methods: [
|
|
55
|
+
{
|
|
56
|
+
methodId: 'encrypt_fields',
|
|
57
|
+
displayName: 'Encrypt Fields',
|
|
58
|
+
inputPorts: [
|
|
59
|
+
{
|
|
60
|
+
portId: 'field_set',
|
|
61
|
+
acceptedTypeSets: [
|
|
62
|
+
{
|
|
63
|
+
setId: 'field-selection-bundle',
|
|
64
|
+
allowedTypes: [
|
|
65
|
+
{
|
|
66
|
+
schemaName: 'FieldSelectionBundle.fbs',
|
|
67
|
+
fileIdentifier: 'FSLB',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
description:
|
|
71
|
+
'Selected plaintext fields plus key agreement material.',
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
minStreams: 1,
|
|
75
|
+
maxStreams: 1,
|
|
76
|
+
required: true,
|
|
77
|
+
description:
|
|
78
|
+
'Field bundle containing plaintext values to encrypt and recipient key material.',
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
outputPorts: [
|
|
82
|
+
{
|
|
83
|
+
portId: 'encrypted_fields',
|
|
84
|
+
acceptedTypeSets: [
|
|
85
|
+
{
|
|
86
|
+
setId: 'encrypted-field-set',
|
|
87
|
+
allowedTypes: [
|
|
88
|
+
{
|
|
89
|
+
schemaName: 'EncryptedFieldSet.fbs',
|
|
90
|
+
fileIdentifier: 'EFLD',
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
description:
|
|
94
|
+
'Encrypted field envelopes emitted by hd-wallet-wasm.',
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
minStreams: 1,
|
|
98
|
+
maxStreams: 1,
|
|
99
|
+
required: true,
|
|
100
|
+
description: 'Encrypted field envelopes with salts, IVs, and tags.',
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
maxBatch: 64,
|
|
104
|
+
drainPolicy: 'drain-until-yield',
|
|
105
|
+
description:
|
|
106
|
+
'Encrypts selected field payloads without leaving the plugin runtime contract.',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
methodId: 'decrypt_fields',
|
|
110
|
+
displayName: 'Decrypt Fields',
|
|
111
|
+
inputPorts: [
|
|
112
|
+
{
|
|
113
|
+
portId: 'encrypted_fields',
|
|
114
|
+
acceptedTypeSets: [
|
|
115
|
+
{
|
|
116
|
+
setId: 'encrypted-field-set',
|
|
117
|
+
allowedTypes: [
|
|
118
|
+
{
|
|
119
|
+
schemaName: 'EncryptedFieldSet.fbs',
|
|
120
|
+
fileIdentifier: 'EFLD',
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
description:
|
|
124
|
+
'Encrypted field envelopes emitted by encrypt_fields.',
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
minStreams: 1,
|
|
128
|
+
maxStreams: 1,
|
|
129
|
+
required: true,
|
|
130
|
+
description:
|
|
131
|
+
'Encrypted field envelopes plus recipient private key material.',
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
outputPorts: [
|
|
135
|
+
{
|
|
136
|
+
portId: 'field_set',
|
|
137
|
+
acceptedTypeSets: [
|
|
138
|
+
{
|
|
139
|
+
setId: 'field-selection-bundle',
|
|
140
|
+
allowedTypes: [
|
|
141
|
+
{
|
|
142
|
+
schemaName: 'FieldSelectionBundle.fbs',
|
|
143
|
+
fileIdentifier: 'FSLB',
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
description:
|
|
147
|
+
'Recovered plaintext field bundle after authenticated decryption.',
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
minStreams: 1,
|
|
151
|
+
maxStreams: 1,
|
|
152
|
+
required: true,
|
|
153
|
+
description: 'Recovered plaintext fields.',
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
maxBatch: 64,
|
|
157
|
+
drainPolicy: 'drain-until-yield',
|
|
158
|
+
description:
|
|
159
|
+
'Performs authenticated field decryption using the manifest-defined plugin surface.',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
methodId: 'sign_detached',
|
|
163
|
+
displayName: 'Sign Detached',
|
|
164
|
+
inputPorts: [
|
|
165
|
+
{
|
|
166
|
+
portId: 'message',
|
|
167
|
+
acceptedTypeSets: [
|
|
168
|
+
{
|
|
169
|
+
setId: 'detached-signing-request',
|
|
170
|
+
allowedTypes: [
|
|
171
|
+
{
|
|
172
|
+
schemaName: 'DetachedSigningRequest.fbs',
|
|
173
|
+
fileIdentifier: 'SGRQ',
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
description:
|
|
177
|
+
'Message payload and signing material for detached signatures.',
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
minStreams: 1,
|
|
181
|
+
maxStreams: 1,
|
|
182
|
+
required: true,
|
|
183
|
+
description: 'Signing request payload.',
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
outputPorts: [
|
|
187
|
+
{
|
|
188
|
+
portId: 'signature',
|
|
189
|
+
acceptedTypeSets: [
|
|
190
|
+
{
|
|
191
|
+
setId: 'detached-signature',
|
|
192
|
+
allowedTypes: [
|
|
193
|
+
{
|
|
194
|
+
schemaName: 'DetachedSignature.fbs',
|
|
195
|
+
fileIdentifier: 'SIGD',
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
description: 'Detached signature envelope.',
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
minStreams: 1,
|
|
202
|
+
maxStreams: 1,
|
|
203
|
+
required: true,
|
|
204
|
+
description: 'Detached signature plus digest and public key metadata.',
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
maxBatch: 64,
|
|
208
|
+
drainPolicy: 'drain-until-yield',
|
|
209
|
+
description:
|
|
210
|
+
'Signs payloads through hd-wallet-wasm primitives instead of ad hoc host helpers.',
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
methodId: 'verify_detached',
|
|
214
|
+
displayName: 'Verify Detached',
|
|
215
|
+
inputPorts: [
|
|
216
|
+
{
|
|
217
|
+
portId: 'signature',
|
|
218
|
+
acceptedTypeSets: [
|
|
219
|
+
{
|
|
220
|
+
setId: 'detached-signature',
|
|
221
|
+
allowedTypes: [
|
|
222
|
+
{
|
|
223
|
+
schemaName: 'DetachedSignature.fbs',
|
|
224
|
+
fileIdentifier: 'SIGD',
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
description:
|
|
228
|
+
'Detached signature envelope emitted by sign_detached.',
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
minStreams: 1,
|
|
232
|
+
maxStreams: 1,
|
|
233
|
+
required: true,
|
|
234
|
+
description: 'Detached signature envelope to verify.',
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
outputPorts: [
|
|
238
|
+
{
|
|
239
|
+
portId: 'verification',
|
|
240
|
+
acceptedTypeSets: [
|
|
241
|
+
{
|
|
242
|
+
setId: 'detached-verification-result',
|
|
243
|
+
allowedTypes: [
|
|
244
|
+
{
|
|
245
|
+
schemaName: 'DetachedVerificationResult.fbs',
|
|
246
|
+
fileIdentifier: 'SIGV',
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
description: 'Detached signature verification result.',
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
minStreams: 1,
|
|
253
|
+
maxStreams: 1,
|
|
254
|
+
required: true,
|
|
255
|
+
description: 'Verification outcome and normalized signature metadata.',
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
maxBatch: 64,
|
|
259
|
+
drainPolicy: 'drain-until-yield',
|
|
260
|
+
description:
|
|
261
|
+
'Verifies detached signatures through the plugin contract using WASM-backed crypto only.',
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
schemasUsed: [
|
|
265
|
+
{
|
|
266
|
+
schemaName: 'FieldSelectionBundle.fbs',
|
|
267
|
+
fileIdentifier: 'FSLB',
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
schemaName: 'EncryptedFieldSet.fbs',
|
|
271
|
+
fileIdentifier: 'EFLD',
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
schemaName: 'DetachedSigningRequest.fbs',
|
|
275
|
+
fileIdentifier: 'SGRQ',
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
schemaName: 'DetachedSignature.fbs',
|
|
279
|
+
fileIdentifier: 'SIGD',
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
schemaName: 'DetachedVerificationResult.fbs',
|
|
283
|
+
fileIdentifier: 'SIGV',
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
buildArtifacts: [
|
|
287
|
+
{
|
|
288
|
+
artifactId: 'hd-wallet-wasm-browser',
|
|
289
|
+
kind: 'wasm-module',
|
|
290
|
+
path: 'wasm/dist/hd-wallet.wasm',
|
|
291
|
+
target: 'browser',
|
|
292
|
+
entrySymbol: 'plugin_get_manifest_flatbuffer',
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
artifactId: 'hd-wallet-wasm-loader',
|
|
296
|
+
kind: 'javascript-loader',
|
|
297
|
+
path: 'wasm/dist/hd-wallet.js',
|
|
298
|
+
target: 'browser',
|
|
299
|
+
entrySymbol: 'HDWalletWasm',
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
abiVersion: 1,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
export function cloneSdnPluginManifest() {
|
|
306
|
+
return JSON.parse(JSON.stringify(HD_WALLET_SDN_PLUGIN_MANIFEST));
|
|
307
|
+
}
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cloneSdnPluginManifest,
|
|
3
|
+
HD_WALLET_SDN_PLUGIN_MANIFEST,
|
|
4
|
+
} from './sdn-plugin-manifest-source.mjs';
|
|
5
|
+
|
|
6
|
+
const MANIFEST_EXPORTS = Object.freeze({
|
|
7
|
+
bytesSymbol: 'plugin_get_manifest_flatbuffer',
|
|
8
|
+
sizeSymbol: 'plugin_get_manifest_flatbuffer_size',
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const textEncoder = new TextEncoder();
|
|
12
|
+
|
|
13
|
+
function toUint8Array(value, fieldName) {
|
|
14
|
+
if (value instanceof Uint8Array) {
|
|
15
|
+
return new Uint8Array(value);
|
|
16
|
+
}
|
|
17
|
+
if (ArrayBuffer.isView(value)) {
|
|
18
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
19
|
+
}
|
|
20
|
+
if (value instanceof ArrayBuffer) {
|
|
21
|
+
return new Uint8Array(value);
|
|
22
|
+
}
|
|
23
|
+
throw new TypeError(`${fieldName} must be a Uint8Array, ArrayBufferView, or ArrayBuffer.`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function optionalUint8Array(value, fieldName) {
|
|
27
|
+
if (value === null || value === undefined) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return toUint8Array(value, fieldName);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function cloneFrame(frame, payload) {
|
|
34
|
+
return {
|
|
35
|
+
portId: frame.portId,
|
|
36
|
+
typeRef: frame.typeRef ? { ...frame.typeRef } : null,
|
|
37
|
+
alignment: frame.alignment ?? 8,
|
|
38
|
+
offset: frame.offset ?? 0,
|
|
39
|
+
size: frame.size ?? 0,
|
|
40
|
+
ownership: frame.ownership ?? 'shared',
|
|
41
|
+
generation: frame.generation ?? 0,
|
|
42
|
+
mutability: frame.mutability ?? 'immutable',
|
|
43
|
+
traceId: frame.traceId ?? null,
|
|
44
|
+
streamId: frame.streamId ?? 1,
|
|
45
|
+
sequence: frame.sequence ?? 1,
|
|
46
|
+
payload,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function buildOutputFrame(sourceFrame, portId, schemaName, fileIdentifier, payload) {
|
|
51
|
+
return cloneFrame(
|
|
52
|
+
{
|
|
53
|
+
portId,
|
|
54
|
+
typeRef: {
|
|
55
|
+
schemaName,
|
|
56
|
+
fileIdentifier,
|
|
57
|
+
schemaHash: [],
|
|
58
|
+
acceptsAnyFlatbuffer: false,
|
|
59
|
+
},
|
|
60
|
+
alignment: 8,
|
|
61
|
+
offset: 0,
|
|
62
|
+
size: 0,
|
|
63
|
+
ownership: 'shared',
|
|
64
|
+
generation: 0,
|
|
65
|
+
mutability: 'immutable',
|
|
66
|
+
traceId:
|
|
67
|
+
sourceFrame?.traceId ??
|
|
68
|
+
`${HD_WALLET_SDN_PLUGIN_MANIFEST.pluginId}:${portId}`,
|
|
69
|
+
streamId: sourceFrame?.streamId ?? 1,
|
|
70
|
+
sequence: sourceFrame?.sequence ?? 1,
|
|
71
|
+
},
|
|
72
|
+
payload
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function resolveLastInput(request) {
|
|
77
|
+
if (!Array.isArray(request?.inputs) || request.inputs.length === 0) {
|
|
78
|
+
throw new Error('Plugin invocation requires at least one input frame.');
|
|
79
|
+
}
|
|
80
|
+
return request.inputs[request.inputs.length - 1];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolveRandomBytes(randomBytes, length, context) {
|
|
84
|
+
if (typeof randomBytes !== 'function') {
|
|
85
|
+
throw new Error(
|
|
86
|
+
'encrypt_fields requires an explicit randomBytes capability callback.'
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
const bytes = toUint8Array(randomBytes(length, context), 'randomBytes result');
|
|
90
|
+
if (bytes.length !== length) {
|
|
91
|
+
throw new Error(`randomBytes capability must return exactly ${length} bytes.`);
|
|
92
|
+
}
|
|
93
|
+
return bytes;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function detectSigningCurve(payload) {
|
|
97
|
+
const curve = String(payload.curve ?? '').trim();
|
|
98
|
+
if (curve) {
|
|
99
|
+
return curve;
|
|
100
|
+
}
|
|
101
|
+
const algorithm = String(payload.algorithm ?? '').trim();
|
|
102
|
+
if (algorithm.startsWith('ed25519')) {
|
|
103
|
+
return 'ed25519';
|
|
104
|
+
}
|
|
105
|
+
return 'secp256k1';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function resolveSigningInput(wallet, payload, curve) {
|
|
109
|
+
const messageBytes = toUint8Array(
|
|
110
|
+
payload.messageBytes ?? payload.message ?? payload.protectedRecordBytes,
|
|
111
|
+
'message'
|
|
112
|
+
);
|
|
113
|
+
if (curve === 'secp256k1') {
|
|
114
|
+
return {
|
|
115
|
+
messageBytes,
|
|
116
|
+
digest: optionalUint8Array(payload.digest, 'digest') ?? wallet.utils.sha256(messageBytes),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
messageBytes,
|
|
121
|
+
digest: optionalUint8Array(payload.digest, 'digest'),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function resolveSignatureEnvelope(wallet, payload, walletSign) {
|
|
126
|
+
const curve = detectSigningCurve(payload);
|
|
127
|
+
const { messageBytes, digest } = resolveSigningInput(wallet, payload, curve);
|
|
128
|
+
|
|
129
|
+
if (!payload.signerPrivateKey && typeof walletSign !== 'function') {
|
|
130
|
+
throw new Error(
|
|
131
|
+
'sign_detached requires signerPrivateKey bytes or a walletSign capability callback.'
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (typeof walletSign === 'function' && !payload.signerPrivateKey) {
|
|
136
|
+
const response = walletSign({
|
|
137
|
+
curve,
|
|
138
|
+
payload,
|
|
139
|
+
messageBytes,
|
|
140
|
+
digest,
|
|
141
|
+
});
|
|
142
|
+
if (!response || typeof response !== 'object') {
|
|
143
|
+
throw new Error('walletSign capability must return a signature envelope.');
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
curve,
|
|
147
|
+
algorithm:
|
|
148
|
+
response.algorithm ??
|
|
149
|
+
(curve === 'ed25519'
|
|
150
|
+
? digest
|
|
151
|
+
? 'ed25519-prehash-sha256'
|
|
152
|
+
: 'ed25519'
|
|
153
|
+
: 'secp256k1-sha256'),
|
|
154
|
+
messageBytes,
|
|
155
|
+
digest,
|
|
156
|
+
signature: toUint8Array(response.signature, 'walletSign signature'),
|
|
157
|
+
publicKey: toUint8Array(response.publicKey, 'walletSign publicKey'),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const privateKey = toUint8Array(payload.signerPrivateKey, 'signerPrivateKey');
|
|
162
|
+
|
|
163
|
+
if (curve === 'ed25519') {
|
|
164
|
+
const signatureInput = digest ?? messageBytes;
|
|
165
|
+
return {
|
|
166
|
+
curve,
|
|
167
|
+
algorithm:
|
|
168
|
+
payload.algorithm ??
|
|
169
|
+
(digest ? 'ed25519-prehash-sha256' : 'ed25519'),
|
|
170
|
+
messageBytes,
|
|
171
|
+
digest,
|
|
172
|
+
signature: wallet.curves.ed25519.sign(signatureInput, privateKey),
|
|
173
|
+
publicKey:
|
|
174
|
+
optionalUint8Array(payload.publicKey, 'publicKey') ??
|
|
175
|
+
wallet.curves.ed25519.publicKeyFromSeed(privateKey),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
curve: 'secp256k1',
|
|
181
|
+
algorithm: payload.algorithm ?? 'secp256k1-sha256',
|
|
182
|
+
messageBytes,
|
|
183
|
+
digest,
|
|
184
|
+
signature: wallet.curves.secp256k1.sign(digest, privateKey),
|
|
185
|
+
publicKey:
|
|
186
|
+
optionalUint8Array(payload.publicKey, 'publicKey') ??
|
|
187
|
+
wallet.curves.publicKeyFromPrivate(privateKey, 0),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function resolveVerificationEnvelope(wallet, payload) {
|
|
192
|
+
const curve = detectSigningCurve(payload);
|
|
193
|
+
const signature = toUint8Array(payload.signature, 'signature');
|
|
194
|
+
const publicKey = toUint8Array(payload.publicKey, 'publicKey');
|
|
195
|
+
const { messageBytes, digest } = resolveSigningInput(wallet, payload, curve);
|
|
196
|
+
const verificationInput = curve === 'secp256k1' ? digest : digest ?? messageBytes;
|
|
197
|
+
const valid =
|
|
198
|
+
curve === 'ed25519'
|
|
199
|
+
? wallet.curves.ed25519.verify(verificationInput, signature, publicKey)
|
|
200
|
+
: wallet.curves.secp256k1.verify(verificationInput, signature, publicKey);
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
curve,
|
|
204
|
+
algorithm:
|
|
205
|
+
payload.algorithm ??
|
|
206
|
+
(curve === 'ed25519'
|
|
207
|
+
? digest
|
|
208
|
+
? 'ed25519-prehash-sha256'
|
|
209
|
+
: 'ed25519'
|
|
210
|
+
: 'secp256k1-sha256'),
|
|
211
|
+
messageBytes,
|
|
212
|
+
digest,
|
|
213
|
+
signature,
|
|
214
|
+
publicKey,
|
|
215
|
+
valid,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function resolveFieldCurve(payload, field) {
|
|
220
|
+
return String(field?.curve ?? payload.curve ?? 'x25519').trim() || 'x25519';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function ecdhForCurve(wallet, curve, privateKey, publicKey) {
|
|
224
|
+
if (curve === 'secp256k1') {
|
|
225
|
+
return wallet.curves.secp256k1.ecdh(privateKey, publicKey);
|
|
226
|
+
}
|
|
227
|
+
if (curve === 'x25519') {
|
|
228
|
+
return wallet.curves.x25519.ecdh(privateKey, publicKey);
|
|
229
|
+
}
|
|
230
|
+
throw new Error(`encrypt_fields does not support curve "${curve}".`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function publicKeyForCurve(wallet, curve, privateKey) {
|
|
234
|
+
if (curve === 'secp256k1') {
|
|
235
|
+
return wallet.curves.publicKeyFromPrivate(privateKey, 0);
|
|
236
|
+
}
|
|
237
|
+
if (curve === 'x25519') {
|
|
238
|
+
return wallet.curves.x25519.publicKey(privateKey);
|
|
239
|
+
}
|
|
240
|
+
throw new Error(`encrypt_fields does not support curve "${curve}".`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function algorithmForCurve(curve) {
|
|
244
|
+
if (curve === 'secp256k1') {
|
|
245
|
+
return 'secp256k1-hkdf-aes-256-gcm';
|
|
246
|
+
}
|
|
247
|
+
return 'x25519-hkdf-aes-256-gcm';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function resolveSenderPrivateKey(randomBytes, payload, curve) {
|
|
251
|
+
if (payload.senderPrivateKey) {
|
|
252
|
+
return toUint8Array(payload.senderPrivateKey, 'senderPrivateKey');
|
|
253
|
+
}
|
|
254
|
+
if (curve === 'x25519') {
|
|
255
|
+
return resolveRandomBytes(randomBytes, 32, {
|
|
256
|
+
methodId: 'encrypt_fields',
|
|
257
|
+
purpose: 'senderPrivateKey',
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
throw new Error(
|
|
261
|
+
'encrypt_fields requires senderPrivateKey when using secp256k1 field encryption.'
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function normalizeAad(field, payload) {
|
|
266
|
+
return (
|
|
267
|
+
optionalUint8Array(field?.aad, 'aad') ??
|
|
268
|
+
optionalUint8Array(payload?.aad, 'aad') ??
|
|
269
|
+
new Uint8Array()
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function encryptFieldsPayload(wallet, payload, randomBytes) {
|
|
274
|
+
const fields = Array.isArray(payload.fields) ? payload.fields : [];
|
|
275
|
+
if (fields.length === 0) {
|
|
276
|
+
throw new Error('encrypt_fields requires at least one field entry.');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const encryptedFields = [];
|
|
280
|
+
for (const field of fields) {
|
|
281
|
+
const curve = resolveFieldCurve(payload, field);
|
|
282
|
+
const recipientPublicKey = toUint8Array(
|
|
283
|
+
payload.recipientPublicKey ?? field.recipientPublicKey,
|
|
284
|
+
'recipientPublicKey'
|
|
285
|
+
);
|
|
286
|
+
const senderPrivateKey = resolveSenderPrivateKey(randomBytes, payload, curve);
|
|
287
|
+
const senderPublicKey =
|
|
288
|
+
optionalUint8Array(field.senderPublicKey, 'senderPublicKey') ??
|
|
289
|
+
publicKeyForCurve(wallet, curve, senderPrivateKey);
|
|
290
|
+
const salt =
|
|
291
|
+
optionalUint8Array(field.salt, 'salt') ??
|
|
292
|
+
resolveRandomBytes(randomBytes, 32, {
|
|
293
|
+
methodId: 'encrypt_fields',
|
|
294
|
+
fieldPath: field.fieldPath,
|
|
295
|
+
purpose: 'salt',
|
|
296
|
+
});
|
|
297
|
+
const iv =
|
|
298
|
+
optionalUint8Array(field.iv, 'iv') ??
|
|
299
|
+
resolveRandomBytes(randomBytes, 12, {
|
|
300
|
+
methodId: 'encrypt_fields',
|
|
301
|
+
fieldPath: field.fieldPath,
|
|
302
|
+
purpose: 'iv',
|
|
303
|
+
});
|
|
304
|
+
const plaintext = toUint8Array(field.plaintext, 'plaintext');
|
|
305
|
+
const aad = normalizeAad(field, payload);
|
|
306
|
+
const sharedSecret = ecdhForCurve(wallet, curve, senderPrivateKey, recipientPublicKey);
|
|
307
|
+
const hkdfInfo = textEncoder.encode(`field:${field.fieldPath}`);
|
|
308
|
+
const aesKey = wallet.utils.hkdf(sharedSecret, salt, hkdfInfo, 32);
|
|
309
|
+
const { ciphertext, tag } = wallet.utils.aesGcm.encrypt(
|
|
310
|
+
aesKey,
|
|
311
|
+
plaintext,
|
|
312
|
+
iv,
|
|
313
|
+
aad
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
encryptedFields.push({
|
|
317
|
+
fieldPath: String(field.fieldPath ?? ''),
|
|
318
|
+
curve,
|
|
319
|
+
algorithm: algorithmForCurve(curve),
|
|
320
|
+
salt,
|
|
321
|
+
iv,
|
|
322
|
+
tag,
|
|
323
|
+
ciphertext,
|
|
324
|
+
senderPublicKey,
|
|
325
|
+
aad,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return { fields: encryptedFields };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function decryptFieldsPayload(wallet, payload) {
|
|
333
|
+
const fields = Array.isArray(payload.fields) ? payload.fields : [];
|
|
334
|
+
if (fields.length === 0) {
|
|
335
|
+
throw new Error('decrypt_fields requires at least one encrypted field entry.');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const recipientPrivateKey = toUint8Array(
|
|
339
|
+
payload.recipientPrivateKey,
|
|
340
|
+
'recipientPrivateKey'
|
|
341
|
+
);
|
|
342
|
+
const decryptedFields = [];
|
|
343
|
+
|
|
344
|
+
for (const field of fields) {
|
|
345
|
+
const curve = resolveFieldCurve(payload, field);
|
|
346
|
+
const senderPublicKey = toUint8Array(field.senderPublicKey, 'senderPublicKey');
|
|
347
|
+
const salt = toUint8Array(field.salt, 'salt');
|
|
348
|
+
const iv = toUint8Array(field.iv, 'iv');
|
|
349
|
+
const tag = toUint8Array(field.tag, 'tag');
|
|
350
|
+
const ciphertext = toUint8Array(field.ciphertext, 'ciphertext');
|
|
351
|
+
const aad = normalizeAad(field, payload);
|
|
352
|
+
const sharedSecret = ecdhForCurve(wallet, curve, recipientPrivateKey, senderPublicKey);
|
|
353
|
+
const hkdfInfo = textEncoder.encode(`field:${field.fieldPath}`);
|
|
354
|
+
const aesKey = wallet.utils.hkdf(sharedSecret, salt, hkdfInfo, 32);
|
|
355
|
+
const plaintext = wallet.utils.aesGcm.decrypt(aesKey, ciphertext, tag, iv, aad);
|
|
356
|
+
|
|
357
|
+
decryptedFields.push({
|
|
358
|
+
fieldPath: String(field.fieldPath ?? ''),
|
|
359
|
+
curve,
|
|
360
|
+
algorithm: field.algorithm ?? algorithmForCurve(curve),
|
|
361
|
+
plaintext,
|
|
362
|
+
aad,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return { fields: decryptedFields };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function readEmbeddedManifestBytes(wasm) {
|
|
370
|
+
const getBytes = wasm._plugin_get_manifest_flatbuffer;
|
|
371
|
+
const getSize = wasm._plugin_get_manifest_flatbuffer_size;
|
|
372
|
+
if (typeof getBytes !== 'function' || typeof getSize !== 'function') {
|
|
373
|
+
throw new Error('Embedded plugin manifest exports are not available in this build.');
|
|
374
|
+
}
|
|
375
|
+
const pointer = Number(getBytes());
|
|
376
|
+
const size = Number(getSize());
|
|
377
|
+
if (!Number.isFinite(pointer) || !Number.isFinite(size) || size <= 0) {
|
|
378
|
+
throw new Error('Embedded plugin manifest exports returned invalid values.');
|
|
379
|
+
}
|
|
380
|
+
return wasm.HEAPU8.slice(pointer, pointer + size);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function buildInvocationResult(outputs) {
|
|
384
|
+
return {
|
|
385
|
+
outputs,
|
|
386
|
+
backlogRemaining: 0,
|
|
387
|
+
yielded: false,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function createSdnPluginContract({ wallet, wasm, randomBytes = null, walletSign = null }) {
|
|
392
|
+
function invoke(methodId, request = {}) {
|
|
393
|
+
const inputFrame = resolveLastInput(request);
|
|
394
|
+
const payload = inputFrame.payload ?? {};
|
|
395
|
+
|
|
396
|
+
switch (methodId) {
|
|
397
|
+
case 'encrypt_fields':
|
|
398
|
+
return buildInvocationResult([
|
|
399
|
+
buildOutputFrame(
|
|
400
|
+
inputFrame,
|
|
401
|
+
'encrypted_fields',
|
|
402
|
+
'EncryptedFieldSet.fbs',
|
|
403
|
+
'EFLD',
|
|
404
|
+
encryptFieldsPayload(wallet, payload, randomBytes)
|
|
405
|
+
),
|
|
406
|
+
]);
|
|
407
|
+
case 'decrypt_fields':
|
|
408
|
+
return buildInvocationResult([
|
|
409
|
+
buildOutputFrame(
|
|
410
|
+
inputFrame,
|
|
411
|
+
'field_set',
|
|
412
|
+
'FieldSelectionBundle.fbs',
|
|
413
|
+
'FSLB',
|
|
414
|
+
decryptFieldsPayload(wallet, payload)
|
|
415
|
+
),
|
|
416
|
+
]);
|
|
417
|
+
case 'sign_detached':
|
|
418
|
+
return buildInvocationResult([
|
|
419
|
+
buildOutputFrame(
|
|
420
|
+
inputFrame,
|
|
421
|
+
'signature',
|
|
422
|
+
'DetachedSignature.fbs',
|
|
423
|
+
'SIGD',
|
|
424
|
+
resolveSignatureEnvelope(wallet, payload, walletSign)
|
|
425
|
+
),
|
|
426
|
+
]);
|
|
427
|
+
case 'verify_detached':
|
|
428
|
+
return buildInvocationResult([
|
|
429
|
+
buildOutputFrame(
|
|
430
|
+
inputFrame,
|
|
431
|
+
'verification',
|
|
432
|
+
'DetachedVerificationResult.fbs',
|
|
433
|
+
'SIGV',
|
|
434
|
+
resolveVerificationEnvelope(wallet, payload)
|
|
435
|
+
),
|
|
436
|
+
]);
|
|
437
|
+
default:
|
|
438
|
+
throw new Error(`Unknown SDN plugin method "${methodId}".`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return {
|
|
443
|
+
manifest: cloneSdnPluginManifest(),
|
|
444
|
+
manifestExports: MANIFEST_EXPORTS,
|
|
445
|
+
getManifest() {
|
|
446
|
+
return cloneSdnPluginManifest();
|
|
447
|
+
},
|
|
448
|
+
getManifestBytes() {
|
|
449
|
+
return readEmbeddedManifestBytes(wasm);
|
|
450
|
+
},
|
|
451
|
+
withCapabilities(capabilities = {}) {
|
|
452
|
+
return createSdnPluginContract({
|
|
453
|
+
wallet,
|
|
454
|
+
wasm,
|
|
455
|
+
randomBytes:
|
|
456
|
+
capabilities.randomBytes !== undefined
|
|
457
|
+
? capabilities.randomBytes
|
|
458
|
+
: randomBytes,
|
|
459
|
+
walletSign:
|
|
460
|
+
capabilities.walletSign !== undefined
|
|
461
|
+
? capabilities.walletSign
|
|
462
|
+
: walletSign,
|
|
463
|
+
});
|
|
464
|
+
},
|
|
465
|
+
invoke,
|
|
466
|
+
encrypt_fields(request) {
|
|
467
|
+
return invoke('encrypt_fields', request);
|
|
468
|
+
},
|
|
469
|
+
decrypt_fields(request) {
|
|
470
|
+
return invoke('decrypt_fields', request);
|
|
471
|
+
},
|
|
472
|
+
sign_detached(request) {
|
|
473
|
+
return invoke('sign_detached', request);
|
|
474
|
+
},
|
|
475
|
+
verify_detached(request) {
|
|
476
|
+
return invoke('verify_detached', request);
|
|
477
|
+
},
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export { MANIFEST_EXPORTS as SDN_PLUGIN_MANIFEST_EXPORTS };
|
|
482
|
+
|