@aikdna/kdna-core 0.6.0 → 0.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.
- package/package.json +1 -1
- package/schema/kdna-manifest-v1rc.json +107 -2
- package/src/asset-reader.js +3 -1
- package/src/crypto-profile.js +278 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikdna/kdna-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "KDNA core library — pure logic for loading, validating, linting, and rendering KDNA domain cognition packages. Zero Node.js dependencies.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -60,6 +60,40 @@
|
|
|
60
60
|
"examples": ["2026.05"]
|
|
61
61
|
},
|
|
62
62
|
|
|
63
|
+
"domain_id": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"pattern": "^[a-z][a-z0-9_]*$",
|
|
66
|
+
"description": "Stable semantic domain slug generated by the Studio-compatible authoring pipeline."
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
"registry_name": {
|
|
70
|
+
"type": "string",
|
|
71
|
+
"pattern": "^@[a-z][a-z0-9-]*/[a-z][a-z0-9_]*$",
|
|
72
|
+
"description": "Scoped registry distribution name for this asset."
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
"asset_uid": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"description": "Unique identifier for this exported .kdna asset instance. uuidv7 is recommended."
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
"project_uid": {
|
|
81
|
+
"type": "string",
|
|
82
|
+
"description": "Stable identifier for the Studio project that produced this asset. uuidv7 is recommended."
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
"build_id": {
|
|
86
|
+
"type": "string",
|
|
87
|
+
"pattern": "^build_[A-Za-z0-9_.:-]+$",
|
|
88
|
+
"description": "Unique identifier for the Studio compile/export run."
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
"content_digest": {
|
|
92
|
+
"type": "string",
|
|
93
|
+
"pattern": "^sha256[-:][0-9a-f]{64}$",
|
|
94
|
+
"description": "Canonical SHA-256 digest of the internal content tree, excluding self-referential digest and signature fields."
|
|
95
|
+
},
|
|
96
|
+
|
|
63
97
|
"description": {
|
|
64
98
|
"type": "string",
|
|
65
99
|
"minLength": 20,
|
|
@@ -193,6 +227,56 @@
|
|
|
193
227
|
"additionalProperties": false
|
|
194
228
|
},
|
|
195
229
|
|
|
230
|
+
"authoring": {
|
|
231
|
+
"type": "object",
|
|
232
|
+
"description": "Studio-compatible authoring provenance. Required for trusted quality claims.",
|
|
233
|
+
"required": ["created_by"],
|
|
234
|
+
"properties": {
|
|
235
|
+
"created_by": {
|
|
236
|
+
"type": "string",
|
|
237
|
+
"enum": [
|
|
238
|
+
"kdna-studio",
|
|
239
|
+
"kdna-studio-cli",
|
|
240
|
+
"kdna-studio-sdk",
|
|
241
|
+
"third-party-studio-compatible",
|
|
242
|
+
"manual-dev-source"
|
|
243
|
+
]
|
|
244
|
+
},
|
|
245
|
+
"authoring_tool": { "type": "string" },
|
|
246
|
+
"authoring_tool_version": { "type": "string" },
|
|
247
|
+
"compiler": { "type": "string" },
|
|
248
|
+
"compiler_version": { "type": "string" },
|
|
249
|
+
"asset_uid": { "type": "string" },
|
|
250
|
+
"project_uid": { "type": "string" },
|
|
251
|
+
"build_id": {
|
|
252
|
+
"type": "string",
|
|
253
|
+
"pattern": "^build_[A-Za-z0-9_.:-]+$"
|
|
254
|
+
},
|
|
255
|
+
"domain_id": {
|
|
256
|
+
"type": "string",
|
|
257
|
+
"pattern": "^[a-z][a-z0-9_]*$"
|
|
258
|
+
},
|
|
259
|
+
"registry_name": {
|
|
260
|
+
"type": "string",
|
|
261
|
+
"pattern": "^@[a-z][a-z0-9-]*/[a-z][a-z0-9_]*$"
|
|
262
|
+
},
|
|
263
|
+
"content_digest": {
|
|
264
|
+
"type": "string",
|
|
265
|
+
"pattern": "^sha256[-:][0-9a-f]{64}$"
|
|
266
|
+
},
|
|
267
|
+
"studio_project_digest": {
|
|
268
|
+
"type": "string",
|
|
269
|
+
"pattern": "^sha256[-:][0-9a-f]{64}$"
|
|
270
|
+
},
|
|
271
|
+
"human_lock_required": { "type": "boolean" },
|
|
272
|
+
"human_lock_count": { "type": "integer", "minimum": 0 },
|
|
273
|
+
"ai_assisted": { "type": "boolean" },
|
|
274
|
+
"human_confirmed": { "type": "boolean" },
|
|
275
|
+
"compiled_at": { "type": "string", "format": "date-time" }
|
|
276
|
+
},
|
|
277
|
+
"additionalProperties": false
|
|
278
|
+
},
|
|
279
|
+
|
|
196
280
|
"signature": {
|
|
197
281
|
"type": "string",
|
|
198
282
|
"pattern": "^ed25519:[0-9a-f]{128}$",
|
|
@@ -229,9 +313,30 @@
|
|
|
229
313
|
}
|
|
230
314
|
},
|
|
231
315
|
"then": {
|
|
232
|
-
"required": ["signature"],
|
|
316
|
+
"required": ["signature", "authoring"],
|
|
233
317
|
"properties": {
|
|
234
|
-
"author": { "required": ["pubkey"] }
|
|
318
|
+
"author": { "required": ["pubkey"] },
|
|
319
|
+
"authoring": {
|
|
320
|
+
"required": [
|
|
321
|
+
"compiler",
|
|
322
|
+
"compiler_version",
|
|
323
|
+
"compiled_at",
|
|
324
|
+
"human_confirmed",
|
|
325
|
+
"human_lock_count"
|
|
326
|
+
],
|
|
327
|
+
"properties": {
|
|
328
|
+
"created_by": {
|
|
329
|
+
"enum": [
|
|
330
|
+
"kdna-studio",
|
|
331
|
+
"kdna-studio-cli",
|
|
332
|
+
"kdna-studio-sdk",
|
|
333
|
+
"third-party-studio-compatible"
|
|
334
|
+
]
|
|
335
|
+
},
|
|
336
|
+
"human_confirmed": { "const": true },
|
|
337
|
+
"human_lock_count": { "minimum": 1 }
|
|
338
|
+
}
|
|
339
|
+
}
|
|
235
340
|
}
|
|
236
341
|
}
|
|
237
342
|
}
|
package/src/asset-reader.js
CHANGED
|
@@ -178,8 +178,10 @@ function manifestForDigest(manifest) {
|
|
|
178
178
|
|
|
179
179
|
function buildContentDigest(asset) {
|
|
180
180
|
const parts = [];
|
|
181
|
+
const excluded = new Set(['.DS_Store', 'signature.json', 'build-receipt.json']);
|
|
181
182
|
for (const entryName of [...asset.entries.keys()].sort()) {
|
|
182
|
-
if (
|
|
183
|
+
if (excluded.has(entryName)) continue;
|
|
184
|
+
if (entryName.startsWith('reports/')) continue;
|
|
183
185
|
const entryBuf = asset.readEntry(entryName);
|
|
184
186
|
let digestBuf = entryBuf;
|
|
185
187
|
if (JSON_ENTRY_RE.test(entryName)) {
|
package/src/crypto-profile.js
CHANGED
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
const crypto = require('crypto');
|
|
2
2
|
|
|
3
|
+
// ── Profile constants ──────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* RFC-0008 compliant profile.
|
|
7
|
+
* - HKDF-SHA256 key derivation
|
|
8
|
+
* - AES-256-KW (RFC 3394) content encryption key wrapping
|
|
9
|
+
* - AES-256-GCM content encryption
|
|
10
|
+
* - Random CEK per asset; license key only unwraps CEK, never touches content
|
|
11
|
+
*/
|
|
3
12
|
const LICENSED_ENTRY_PROFILE = 'kdna-licensed-entry-v1';
|
|
4
|
-
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Pre-RFC legacy profile (now experimental).
|
|
16
|
+
* Uses scrypt-sha256 with concatenated secret (`licenseKey|machineFingerprint`).
|
|
17
|
+
* Retained for backward compatibility only.
|
|
18
|
+
*/
|
|
19
|
+
const LICENSED_EXPERIMENTAL_PROFILE = 'kdna-licensed-entry-experimental';
|
|
20
|
+
|
|
21
|
+
const RFC_KDF = 'HKDF-SHA256';
|
|
22
|
+
const RFC_KEY_WRAPPING = 'AES-256-KW';
|
|
23
|
+
const LEGACY_KDF = 'scrypt-sha256';
|
|
5
24
|
const ALG = 'AES-256-GCM';
|
|
6
25
|
|
|
26
|
+
// ── Helpers ───────────────────────────────────────────────────────
|
|
27
|
+
|
|
7
28
|
function toBuffer(value, label) {
|
|
8
29
|
if (Buffer.isBuffer(value)) return value;
|
|
9
30
|
if (value instanceof Uint8Array) return Buffer.from(value);
|
|
@@ -25,21 +46,10 @@ function normalizeEnvelope(value) {
|
|
|
25
46
|
throw new Error('encrypted entry envelope must be JSON');
|
|
26
47
|
}
|
|
27
48
|
|
|
28
|
-
function
|
|
29
|
-
const { licenseKey, machineFingerprint, salt, keyLength = 32 } = options;
|
|
30
|
-
if (!licenseKey) throw new Error('licenseKey is required');
|
|
31
|
-
if (!machineFingerprint) throw new Error('machineFingerprint is required');
|
|
32
|
-
const saltBuffer = Buffer.isBuffer(salt) || salt instanceof Uint8Array
|
|
33
|
-
? Buffer.from(salt)
|
|
34
|
-
: decodeBase64(salt, 'salt');
|
|
35
|
-
const secret = `${licenseKey}|${machineFingerprint}`;
|
|
36
|
-
return crypto.scryptSync(secret, saltBuffer, keyLength);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function encryptedEntryAad(entryName, manifest = {}) {
|
|
49
|
+
function encryptedEntryAad(entryName, manifest = {}, profile = LICENSED_ENTRY_PROFILE) {
|
|
40
50
|
return Buffer.from(
|
|
41
51
|
[
|
|
42
|
-
|
|
52
|
+
profile,
|
|
43
53
|
manifest.name || manifest.asset_id || '',
|
|
44
54
|
manifest.version || '',
|
|
45
55
|
entryName,
|
|
@@ -48,19 +58,220 @@ function encryptedEntryAad(entryName, manifest = {}) {
|
|
|
48
58
|
);
|
|
49
59
|
}
|
|
50
60
|
|
|
51
|
-
|
|
61
|
+
// ── HKDF-SHA256 (RFC 5869) ────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* HKDF-SHA256 extract-then-expand.
|
|
65
|
+
* @param {Buffer|string} ikm — input keying material (license key)
|
|
66
|
+
* @param {Buffer} [salt] — optional salt (defaults to 32 zero bytes)
|
|
67
|
+
* @param {Buffer|string} [info] — context info
|
|
68
|
+
* @param {number} [length] — output length (default 32)
|
|
69
|
+
*/
|
|
70
|
+
function hkdfSha256(ikm, salt = null, info = '', length = 32) {
|
|
71
|
+
const ikmBuf = toBuffer(ikm, 'ikm');
|
|
72
|
+
// Extract
|
|
73
|
+
const saltBuf = salt || Buffer.alloc(32, 0);
|
|
74
|
+
const prk = crypto.createHmac('sha256', saltBuf).update(ikmBuf).digest();
|
|
75
|
+
// Expand (single-block expansion for length ≤ 32)
|
|
76
|
+
const t = crypto.createHmac('sha256', prk)
|
|
77
|
+
.update(Buffer.concat([
|
|
78
|
+
toBuffer(info, 'info'),
|
|
79
|
+
// Counter byte 0x01
|
|
80
|
+
Buffer.from([1]),
|
|
81
|
+
]))
|
|
82
|
+
.digest();
|
|
83
|
+
return t.subarray(0, length);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── AES-256-KW (RFC 3394) ─────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
const AES_BLOCK = 16;
|
|
89
|
+
const KW_IV = Buffer.from('a6a6a6a6a6a6a6a6', 'hex'); // RFC 3394 default IV
|
|
90
|
+
|
|
91
|
+
function aesWrap(key, plaintext) {
|
|
92
|
+
// key = 32 bytes, plaintext = 32 bytes (single CEK)
|
|
93
|
+
if (key.length !== 32) throw new Error('AES-256-KW requires 32-byte key');
|
|
94
|
+
if (plaintext.length !== 32) throw new Error('AES-256-KW requires 32-byte plaintext');
|
|
95
|
+
|
|
96
|
+
const n = plaintext.length / 8; // = 4 for 256-bit CEK
|
|
97
|
+
const r = new Array(n + 1);
|
|
98
|
+
r[0] = KW_IV;
|
|
99
|
+
for (let i = 0; i < n; i++) r[i + 1] = plaintext.subarray(i * 8, (i + 1) * 8);
|
|
100
|
+
|
|
101
|
+
for (let j = 0; j <= 5; j++) {
|
|
102
|
+
for (let i = 1; i <= n; i++) {
|
|
103
|
+
const input = Buffer.concat([r[0], r[i]]); // 16 bytes
|
|
104
|
+
const cipher = crypto.createCipheriv('aes-256-ecb', key, null);
|
|
105
|
+
cipher.setAutoPadding(false);
|
|
106
|
+
const b = Buffer.concat([cipher.update(input), cipher.final()]);
|
|
107
|
+
r[0] = b.subarray(0, 8);
|
|
108
|
+
// XOR t = (n * j) + i as big-endian 64-bit
|
|
109
|
+
const t = BigInt(n) * BigInt(j) + BigInt(i);
|
|
110
|
+
const tBuf = Buffer.alloc(8);
|
|
111
|
+
tBuf.writeBigUInt64BE(t);
|
|
112
|
+
for (let k = 0; k < 8; k++) r[0][k] ^= tBuf[k];
|
|
113
|
+
r[i] = b.subarray(8, 16);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const result = Buffer.alloc((n + 1) * 8);
|
|
118
|
+
for (let i = 0; i <= n; i++) r[i].copy(result, i * 8);
|
|
119
|
+
return result; // 40 bytes
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function aesUnwrap(key, ciphertext) {
|
|
123
|
+
if (key.length !== 32) throw new Error('AES-256-KW requires 32-byte key');
|
|
124
|
+
if (ciphertext.length !== 40) throw new Error('AES-256-KW ciphertext must be 40 bytes');
|
|
125
|
+
|
|
126
|
+
const n = (ciphertext.length / 8) - 1; // = 4
|
|
127
|
+
const r = new Array(n + 1);
|
|
128
|
+
for (let i = 0; i <= n; i++) r[i] = ciphertext.subarray(i * 8, (i + 1) * 8);
|
|
129
|
+
|
|
130
|
+
for (let j = 5; j >= 0; j--) {
|
|
131
|
+
for (let i = n; i >= 1; i--) {
|
|
132
|
+
const t = BigInt(n) * BigInt(j) + BigInt(i);
|
|
133
|
+
const tBuf = Buffer.alloc(8);
|
|
134
|
+
tBuf.writeBigUInt64BE(t);
|
|
135
|
+
for (let k = 0; k < 8; k++) r[0][k] ^= tBuf[k];
|
|
136
|
+
|
|
137
|
+
const input = Buffer.concat([r[0], r[i]]);
|
|
138
|
+
const decipher = crypto.createDecipheriv('aes-256-ecb', key, null);
|
|
139
|
+
decipher.setAutoPadding(false);
|
|
140
|
+
const b = Buffer.concat([decipher.update(input), decipher.final()]);
|
|
141
|
+
r[0] = b.subarray(0, 8);
|
|
142
|
+
r[i] = b.subarray(8, 16);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!r[0].equals(KW_IV)) throw new Error('AES-256-KW unwrap: integrity check failed');
|
|
147
|
+
const result = Buffer.alloc(n * 8);
|
|
148
|
+
for (let i = 1; i <= n; i++) r[i].copy(result, (i - 1) * 8);
|
|
149
|
+
return result; // 32 bytes
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── RFC-0008 compliant encryption ─────────────────────────────────
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Derive a key-wrapping key (KWK) from a license key via HKDF-SHA256.
|
|
156
|
+
*/
|
|
157
|
+
function deriveWrappingKey(licenseKey, info = 'kdna-licensed-entry-v1-kwk') {
|
|
158
|
+
return hkdfSha256(licenseKey, null, info, 32);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Generate a random content encryption key (CEK).
|
|
163
|
+
*/
|
|
164
|
+
function generateCEK() {
|
|
165
|
+
return crypto.randomBytes(32);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Wrap a CEK with a key-wrapping key.
|
|
170
|
+
*/
|
|
171
|
+
function wrapCEK(cek, wrappingKey) {
|
|
172
|
+
return aesWrap(wrappingKey, cek);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Unwrap a CEK from its wrapped form.
|
|
177
|
+
*/
|
|
178
|
+
function unwrapCEK(wrappedCek, wrappingKey) {
|
|
179
|
+
return aesUnwrap(wrappingKey, wrappedCek);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Encrypt a licensed entry using the RFC-0008 compliant model.
|
|
184
|
+
*
|
|
185
|
+
* 1. Generate random CEK
|
|
186
|
+
* 2. Derive KWK from license key + machine fingerprint via HKDF
|
|
187
|
+
* 3. Encrypt content with CEK (AES-256-GCM)
|
|
188
|
+
* 4. Wrap CEK with KWK (AES-256-KW)
|
|
189
|
+
* 5. Store wrapped_key in envelope
|
|
190
|
+
*/
|
|
191
|
+
function encryptLicensedEntryV1(plaintext, options = {}) {
|
|
192
|
+
const { entryName, manifest = {}, licenseKey } = options;
|
|
193
|
+
if (!entryName) throw new Error('entryName is required');
|
|
194
|
+
if (!licenseKey) throw new Error('licenseKey is required for RFC-0008 encryption');
|
|
195
|
+
|
|
196
|
+
const cek = generateCEK();
|
|
197
|
+
const wrappingKey = deriveWrappingKey(licenseKey);
|
|
198
|
+
const wrappedKey = wrapCEK(cek, wrappingKey);
|
|
199
|
+
|
|
200
|
+
const iv = crypto.randomBytes(12);
|
|
201
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', cek, iv);
|
|
202
|
+
cipher.setAAD(encryptedEntryAad(entryName, manifest));
|
|
203
|
+
const ciphertext = Buffer.concat([cipher.update(toBuffer(plaintext, 'plaintext')), cipher.final()]);
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
profile: LICENSED_ENTRY_PROFILE,
|
|
207
|
+
alg: ALG,
|
|
208
|
+
kdf: RFC_KDF,
|
|
209
|
+
key_wrapping: RFC_KEY_WRAPPING,
|
|
210
|
+
wrapped_key: wrappedKey.toString('base64'),
|
|
211
|
+
iv: iv.toString('base64'),
|
|
212
|
+
tag: cipher.getAuthTag().toString('base64'),
|
|
213
|
+
ciphertext: ciphertext.toString('base64'),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Decrypt a licensed entry encoded with the RFC-0008 compliant profile.
|
|
219
|
+
*
|
|
220
|
+
* 1. Derive KWK from license key via HKDF
|
|
221
|
+
* 2. Unwrap CEK from wrapped_key (AES-256-KW)
|
|
222
|
+
* 3. Decrypt content with CEK (AES-256-GCM)
|
|
223
|
+
*/
|
|
224
|
+
function decryptLicensedEntryV1(envelopeValue, options = {}) {
|
|
225
|
+
const { entryName, manifest = {}, licenseKey } = options;
|
|
226
|
+
if (!entryName) throw new Error('entryName is required');
|
|
227
|
+
if (!licenseKey) throw new Error('licenseKey is required for RFC-0008 decryption');
|
|
228
|
+
|
|
229
|
+
const envelope = normalizeEnvelope(envelopeValue);
|
|
230
|
+
if (envelope.profile !== LICENSED_ENTRY_PROFILE) {
|
|
231
|
+
throw new Error(`unsupported encrypted entry profile: ${envelope.profile || 'unknown'} (expected ${LICENSED_ENTRY_PROFILE})`);
|
|
232
|
+
}
|
|
233
|
+
if (envelope.alg !== ALG) throw new Error(`unsupported encrypted entry alg: ${envelope.alg}`);
|
|
234
|
+
if (envelope.kdf !== RFC_KDF) throw new Error(`unsupported encrypted entry kdf: ${envelope.kdf}`);
|
|
235
|
+
if (envelope.key_wrapping !== RFC_KEY_WRAPPING) throw new Error(`unsupported encrypted entry key_wrapping: ${envelope.key_wrapping}`);
|
|
236
|
+
|
|
237
|
+
const wrappingKey = deriveWrappingKey(licenseKey);
|
|
238
|
+
const cek = unwrapCEK(decodeBase64(envelope.wrapped_key, 'wrapped_key'), wrappingKey);
|
|
239
|
+
|
|
240
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', cek, decodeBase64(envelope.iv, 'iv'));
|
|
241
|
+
decipher.setAAD(encryptedEntryAad(entryName, manifest));
|
|
242
|
+
decipher.setAuthTag(decodeBase64(envelope.tag, 'tag'));
|
|
243
|
+
return Buffer.concat([
|
|
244
|
+
decipher.update(decodeBase64(envelope.ciphertext, 'ciphertext')),
|
|
245
|
+
decipher.final(),
|
|
246
|
+
]);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── Legacy / experimental profile (pre-RFC, backward compat) ───────
|
|
250
|
+
|
|
251
|
+
function deriveLicensedEntryKeyLegacy(options = {}) {
|
|
252
|
+
const { licenseKey, machineFingerprint, salt, keyLength = 32 } = options;
|
|
253
|
+
if (!licenseKey) throw new Error('licenseKey is required');
|
|
254
|
+
if (!machineFingerprint) throw new Error('machineFingerprint is required');
|
|
255
|
+
const saltBuffer = Buffer.isBuffer(salt) || salt instanceof Uint8Array
|
|
256
|
+
? Buffer.from(salt)
|
|
257
|
+
: decodeBase64(salt, 'salt');
|
|
258
|
+
const secret = `${licenseKey}|${machineFingerprint}`;
|
|
259
|
+
return crypto.scryptSync(secret, saltBuffer, keyLength);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function encryptLicensedEntryLegacy(plaintext, options = {}) {
|
|
52
263
|
const { entryName, manifest = {}, licenseKey, machineFingerprint } = options;
|
|
53
264
|
if (!entryName) throw new Error('entryName is required');
|
|
54
265
|
const salt = crypto.randomBytes(16);
|
|
55
266
|
const iv = crypto.randomBytes(12);
|
|
56
|
-
const key =
|
|
267
|
+
const key = deriveLicensedEntryKeyLegacy({ licenseKey, machineFingerprint, salt });
|
|
57
268
|
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
58
|
-
cipher.setAAD(encryptedEntryAad(entryName, manifest));
|
|
269
|
+
cipher.setAAD(encryptedEntryAad(entryName, manifest, LICENSED_EXPERIMENTAL_PROFILE));
|
|
59
270
|
const ciphertext = Buffer.concat([cipher.update(toBuffer(plaintext, 'plaintext')), cipher.final()]);
|
|
60
271
|
return {
|
|
61
|
-
profile:
|
|
272
|
+
profile: LICENSED_EXPERIMENTAL_PROFILE,
|
|
62
273
|
alg: ALG,
|
|
63
|
-
kdf:
|
|
274
|
+
kdf: LEGACY_KDF,
|
|
64
275
|
salt: salt.toString('base64'),
|
|
65
276
|
iv: iv.toString('base64'),
|
|
66
277
|
tag: cipher.getAuthTag().toString('base64'),
|
|
@@ -68,22 +279,22 @@ function encryptLicensedEntry(plaintext, options = {}) {
|
|
|
68
279
|
};
|
|
69
280
|
}
|
|
70
281
|
|
|
71
|
-
function
|
|
282
|
+
function decryptLicensedEntryLegacy(envelopeValue, options = {}) {
|
|
72
283
|
const { entryName, manifest = {}, licenseKey, machineFingerprint } = options;
|
|
73
284
|
if (!entryName) throw new Error('entryName is required');
|
|
74
285
|
const envelope = normalizeEnvelope(envelopeValue);
|
|
75
|
-
if (envelope.profile !==
|
|
286
|
+
if (envelope.profile !== LICENSED_EXPERIMENTAL_PROFILE) {
|
|
76
287
|
throw new Error(`unsupported encrypted entry profile: ${envelope.profile || 'unknown'}`);
|
|
77
288
|
}
|
|
78
289
|
if (envelope.alg !== ALG) throw new Error(`unsupported encrypted entry alg: ${envelope.alg}`);
|
|
79
|
-
if (envelope.kdf !==
|
|
80
|
-
const key =
|
|
290
|
+
if (envelope.kdf !== LEGACY_KDF) throw new Error(`unsupported encrypted entry kdf: ${envelope.kdf}`);
|
|
291
|
+
const key = deriveLicensedEntryKeyLegacy({
|
|
81
292
|
licenseKey,
|
|
82
293
|
machineFingerprint,
|
|
83
294
|
salt: envelope.salt,
|
|
84
295
|
});
|
|
85
296
|
const decipher = crypto.createDecipheriv('aes-256-gcm', key, decodeBase64(envelope.iv, 'iv'));
|
|
86
|
-
decipher.setAAD(encryptedEntryAad(entryName, manifest));
|
|
297
|
+
decipher.setAAD(encryptedEntryAad(entryName, manifest, LICENSED_EXPERIMENTAL_PROFILE));
|
|
87
298
|
decipher.setAuthTag(decodeBase64(envelope.tag, 'tag'));
|
|
88
299
|
return Buffer.concat([
|
|
89
300
|
decipher.update(decodeBase64(envelope.ciphertext, 'ciphertext')),
|
|
@@ -91,6 +302,23 @@ function decryptLicensedEntry(envelopeValue, options = {}) {
|
|
|
91
302
|
]);
|
|
92
303
|
}
|
|
93
304
|
|
|
305
|
+
// ── Unified entry points (auto-detect profile) ────────────────────
|
|
306
|
+
|
|
307
|
+
function encryptLicensedEntry(plaintext, options = {}) {
|
|
308
|
+
return encryptLicensedEntryV1(plaintext, options);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function decryptLicensedEntry(envelopeValue, options = {}) {
|
|
312
|
+
const envelope = normalizeEnvelope(envelopeValue);
|
|
313
|
+
if (envelope.profile === LICENSED_ENTRY_PROFILE) {
|
|
314
|
+
return decryptLicensedEntryV1(envelopeValue, options);
|
|
315
|
+
}
|
|
316
|
+
if (envelope.profile === LICENSED_EXPERIMENTAL_PROFILE) {
|
|
317
|
+
return decryptLicensedEntryLegacy(envelopeValue, options);
|
|
318
|
+
}
|
|
319
|
+
throw new Error(`unsupported encrypted entry profile: ${envelope.profile || 'unknown'}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
94
322
|
function createLicensedDecryptEntry(options = {}) {
|
|
95
323
|
const { licenseKey, machineFingerprint } = options;
|
|
96
324
|
return ({ entryName, ciphertext, manifest }) =>
|
|
@@ -98,9 +326,34 @@ function createLicensedDecryptEntry(options = {}) {
|
|
|
98
326
|
}
|
|
99
327
|
|
|
100
328
|
module.exports = {
|
|
329
|
+
// Profiles
|
|
101
330
|
LICENSED_ENTRY_PROFILE,
|
|
102
|
-
|
|
331
|
+
LICENSED_EXPERIMENTAL_PROFILE,
|
|
332
|
+
ALG,
|
|
333
|
+
RFC_KDF,
|
|
334
|
+
RFC_KEY_WRAPPING,
|
|
335
|
+
LEGACY_KDF,
|
|
336
|
+
|
|
337
|
+
// RFC-0008 compliant
|
|
338
|
+
deriveWrappingKey,
|
|
339
|
+
generateCEK,
|
|
340
|
+
wrapCEK,
|
|
341
|
+
unwrapCEK,
|
|
342
|
+
encryptLicensedEntryV1,
|
|
343
|
+
decryptLicensedEntryV1,
|
|
344
|
+
|
|
345
|
+
// Legacy
|
|
346
|
+
deriveLicensedEntryKey: deriveLicensedEntryKeyLegacy,
|
|
347
|
+
encryptLicensedEntryLegacy,
|
|
348
|
+
decryptLicensedEntryLegacy,
|
|
349
|
+
|
|
350
|
+
// Unified
|
|
103
351
|
encryptLicensedEntry,
|
|
104
352
|
decryptLicensedEntry,
|
|
105
353
|
createLicensedDecryptEntry,
|
|
354
|
+
|
|
355
|
+
// Low-level
|
|
356
|
+
hkdfSha256,
|
|
357
|
+
aesWrap,
|
|
358
|
+
aesUnwrap,
|
|
106
359
|
};
|