@zcloak/ai-agent 1.0.41 → 1.0.42
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/SKILL.md +57 -18
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -4
- package/dist/cli.js.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/crypto.d.ts +24 -0
- package/dist/crypto.js +45 -0
- package/dist/crypto.js.map +1 -1
- package/dist/openclaw.js +12 -0
- package/dist/openclaw.js.map +1 -1
- package/dist/pre-check.d.ts +1 -1
- package/dist/pre-check.js +7 -3
- package/dist/pre-check.js.map +1 -1
- package/dist/vetkey.d.ts +15 -2
- package/dist/vetkey.js +309 -77
- package/dist/vetkey.js.map +1 -1
- package/dist/zmail.d.ts +11 -0
- package/dist/zmail.js +264 -0
- package/dist/zmail.js.map +1 -1
- package/package.json +1 -1
package/dist/vetkey.js
CHANGED
|
@@ -31,7 +31,7 @@ import { daemonLogPath } from './paths.js';
|
|
|
31
31
|
import { join } from 'path';
|
|
32
32
|
import { fileURLToPath } from 'url';
|
|
33
33
|
import { createInterface } from 'readline';
|
|
34
|
-
import { createHash } from 'crypto';
|
|
34
|
+
import { createHash, randomBytes } from 'crypto';
|
|
35
35
|
import { Principal } from '@dfinity/principal';
|
|
36
36
|
import { schnorr } from '@noble/curves/secp256k1';
|
|
37
37
|
import { hexToBytes, bytesToHex } from '@noble/hashes/utils';
|
|
@@ -133,7 +133,7 @@ function showHelp() {
|
|
|
133
133
|
console.log(' grants-in List grants you received (as grantee)');
|
|
134
134
|
console.log('');
|
|
135
135
|
console.log('Encrypted Messaging:');
|
|
136
|
-
console.log(' send-msg Encrypt + auto-deliver via zMail (
|
|
136
|
+
console.log(' send-msg Encrypt + auto-deliver via zMail (Kind17 v2 by default)');
|
|
137
137
|
console.log(' recv-msg Decrypt a received message via Mail daemon');
|
|
138
138
|
console.log('');
|
|
139
139
|
console.log('Options:');
|
|
@@ -154,6 +154,7 @@ function showHelp() {
|
|
|
154
154
|
console.log(' --reply=<msg_id> Reply to a parent message (for send-msg)');
|
|
155
155
|
console.log(' --data=<json> Encrypted message JSON envelope (for recv-msg)');
|
|
156
156
|
console.log(' --msg-id=<id> Message ID to auto-fetch and decrypt (for recv-msg)');
|
|
157
|
+
console.log(' --kind17-version=<v> Mail content version: v2 (default) or v1');
|
|
157
158
|
console.log(' --no-zmail Skip auto-POST to zMail (send-msg only)');
|
|
158
159
|
console.log(' --zmail-url=<url> Override zMail server URL');
|
|
159
160
|
}
|
|
@@ -1112,6 +1113,16 @@ function computeEnvelopeId(envelope) {
|
|
|
1112
1113
|
];
|
|
1113
1114
|
return createHash('sha256').update(JSON.stringify(payload), 'utf8').digest('hex');
|
|
1114
1115
|
}
|
|
1116
|
+
function kind17VersionFlag(session) {
|
|
1117
|
+
const raw = session.args['kind17-version'];
|
|
1118
|
+
if (raw === undefined) {
|
|
1119
|
+
return 'v2';
|
|
1120
|
+
}
|
|
1121
|
+
if (raw !== 'v1' && raw !== 'v2') {
|
|
1122
|
+
throw new Error('Invalid --kind17-version value: expected "v1" or "v2"');
|
|
1123
|
+
}
|
|
1124
|
+
return raw;
|
|
1125
|
+
}
|
|
1115
1126
|
/**
|
|
1116
1127
|
* Extract the raw 32-byte secp256k1 private key from the session identity.
|
|
1117
1128
|
* The Secp256k1KeyIdentity.toJSON() returns [publicKeyHex, privateKeyHex].
|
|
@@ -1135,6 +1146,33 @@ const SPKI_X_LENGTH = 64;
|
|
|
1135
1146
|
export function schnorrPubkeyFromSpki(spkiHex) {
|
|
1136
1147
|
return spkiHex.slice(SPKI_X_OFFSET, SPKI_X_OFFSET + SPKI_X_LENGTH);
|
|
1137
1148
|
}
|
|
1149
|
+
function withSenderPubkeyTag(session, tags) {
|
|
1150
|
+
if (getTagValue(tags, 'from_pubkey')) {
|
|
1151
|
+
return tags;
|
|
1152
|
+
}
|
|
1153
|
+
const senderSpkiHex = Buffer.from(session.getIdentity().getPublicKey().toDer()).toString('hex');
|
|
1154
|
+
return [...tags, ['from_pubkey', senderSpkiHex]];
|
|
1155
|
+
}
|
|
1156
|
+
function canonicalSerializeEnvelopeCryptoContext(context) {
|
|
1157
|
+
return JSON.stringify(canonicalize({
|
|
1158
|
+
kind: context.kind,
|
|
1159
|
+
ai_id: context.ai_id,
|
|
1160
|
+
created_at: context.created_at,
|
|
1161
|
+
tags: context.tags,
|
|
1162
|
+
}));
|
|
1163
|
+
}
|
|
1164
|
+
function buildKind17V2BodyAad(context, payloadType) {
|
|
1165
|
+
return Buffer.from(`${canonicalSerializeEnvelopeCryptoContext(context)}|body|${payloadType}|v2`, 'utf8');
|
|
1166
|
+
}
|
|
1167
|
+
function splitCiphertextAndTag(raw) {
|
|
1168
|
+
if (raw.length < 16) {
|
|
1169
|
+
throw new Error('Invalid Kind17 v2 ciphertext: missing authentication tag');
|
|
1170
|
+
}
|
|
1171
|
+
return {
|
|
1172
|
+
ciphertext: raw.slice(0, raw.length - 16),
|
|
1173
|
+
tag: raw.slice(raw.length - 16),
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1138
1176
|
/**
|
|
1139
1177
|
* Build, sign, and return a complete Kind17 envelope.
|
|
1140
1178
|
*
|
|
@@ -1147,14 +1185,9 @@ export function schnorrPubkeyFromSpki(spkiHex) {
|
|
|
1147
1185
|
* @param content - Encrypted content JSON string: {"v":1,"type":"text"|"file","ct":"<base64>"}
|
|
1148
1186
|
* @returns Signed Kind17Envelope ready for JSON serialization
|
|
1149
1187
|
*/
|
|
1150
|
-
function buildSignedEnvelope(session, tags, content) {
|
|
1188
|
+
function buildSignedEnvelope(session, tags, content, createdAt = Math.floor(Date.now() / 1000)) {
|
|
1151
1189
|
const senderPrincipal = session.getPrincipal();
|
|
1152
|
-
const
|
|
1153
|
-
// Include the sender's SPKI public key for receiver-side verification.
|
|
1154
|
-
// This enables binding ai_id ↔ pubkey ↔ sig without registry lookups.
|
|
1155
|
-
const senderIdentity = session.getIdentity();
|
|
1156
|
-
const senderSpkiHex = Buffer.from(senderIdentity.getPublicKey().toDer()).toString('hex');
|
|
1157
|
-
const allTags = [...tags, ['from_pubkey', senderSpkiHex]];
|
|
1190
|
+
const allTags = withSenderPubkeyTag(session, tags);
|
|
1158
1191
|
// Compute the envelope ID from canonical serialization
|
|
1159
1192
|
const partial = { kind: 17, ai_id: senderPrincipal, created_at: createdAt, tags: allTags, content };
|
|
1160
1193
|
const id = computeEnvelopeId(partial);
|
|
@@ -1196,6 +1229,11 @@ async function cmdSendMsg(session) {
|
|
|
1196
1229
|
throw new Error('--to=<AI-ID or principal> is required');
|
|
1197
1230
|
}
|
|
1198
1231
|
const { plaintext, payloadType, filename } = readMessageInput(text, file);
|
|
1232
|
+
const kind17Version = kind17VersionFlag(session);
|
|
1233
|
+
// Legacy Kind17 v1 generation is kept only for compatibility/debug comparison.
|
|
1234
|
+
// The default zMail backend is the V2 endpoint, which is expected to reject
|
|
1235
|
+
// newly sent v1 content with unsupported_content_version. Do not treat that
|
|
1236
|
+
// as a regression in the upgraded path — v1 is on the deprecation path.
|
|
1199
1237
|
// Resolve recipient principal from AI-ID (readable) or raw principal text.
|
|
1200
1238
|
let recipientPrincipal;
|
|
1201
1239
|
const registryActor = await session.getAnonymousRegistryActor();
|
|
@@ -1239,28 +1277,51 @@ async function cmdSendMsg(session) {
|
|
|
1239
1277
|
catch (e) {
|
|
1240
1278
|
throw canisterCallError(`get_ibe_public_key failed: ${e instanceof Error ? e.message : String(e)}`, e);
|
|
1241
1279
|
}
|
|
1242
|
-
// IBE-encrypt the plaintext for the recipient's Mail identity
|
|
1243
|
-
const ciphertext = cryptoOps.ibeEncrypt(dpkBytes, ibeIdentity, plaintext);
|
|
1244
|
-
const contentBase64 = Buffer.from(ciphertext).toString('base64');
|
|
1245
|
-
// Wrap ciphertext in the standardized message composition format per zmail-skill spec.
|
|
1246
|
-
// Shape: {"v":1,"type":"text"|"file","ct":"<base64-ciphertext>"}
|
|
1247
|
-
// The outer Kind17 envelope carries routing metadata; only the body is encrypted.
|
|
1248
|
-
const content = JSON.stringify({ v: 1, type: payloadType, ct: contentBase64 });
|
|
1249
1280
|
// Build tags: recipient + metadata
|
|
1250
|
-
const
|
|
1281
|
+
const baseTags = [
|
|
1251
1282
|
['to', recipientPrincipal],
|
|
1252
|
-
['payload_type', payloadType],
|
|
1253
|
-
['ibe_id', ibeIdentity],
|
|
1254
1283
|
];
|
|
1284
|
+
if (kind17Version === 'v1') {
|
|
1285
|
+
baseTags.push(['payload_type', payloadType], ['ibe_id', ibeIdentity]);
|
|
1286
|
+
}
|
|
1255
1287
|
if (filename) {
|
|
1256
|
-
|
|
1288
|
+
baseTags.push(['filename', filename]);
|
|
1257
1289
|
}
|
|
1258
1290
|
// Include reply tag when responding to an existing message
|
|
1259
1291
|
if (replyMsgId) {
|
|
1260
|
-
|
|
1292
|
+
baseTags.push(['reply', replyMsgId]);
|
|
1261
1293
|
}
|
|
1294
|
+
const createdAt = Math.floor(Date.now() / 1000);
|
|
1295
|
+
const envelopeTags = withSenderPubkeyTag(session, baseTags);
|
|
1296
|
+
const senderPrincipal = session.getPrincipal();
|
|
1297
|
+
const content = kind17Version === 'v1'
|
|
1298
|
+
? buildKind17V1Content(dpkBytes, ibeIdentity, plaintext, payloadType)
|
|
1299
|
+
: buildKind17V2Content(dpkBytes, senderPrincipal, recipientPrincipal, plaintext, payloadType, {
|
|
1300
|
+
kind: 17,
|
|
1301
|
+
ai_id: senderPrincipal,
|
|
1302
|
+
created_at: createdAt,
|
|
1303
|
+
tags: envelopeTags,
|
|
1304
|
+
});
|
|
1305
|
+
log.debug('send-msg envelope prepared', {
|
|
1306
|
+
kind17Version,
|
|
1307
|
+
senderPrincipal,
|
|
1308
|
+
recipientPrincipal,
|
|
1309
|
+
payloadType,
|
|
1310
|
+
plaintextSize: plaintext.length,
|
|
1311
|
+
hasFilename: Boolean(filename),
|
|
1312
|
+
hasReply: Boolean(replyMsgId),
|
|
1313
|
+
contentType: typeof content,
|
|
1314
|
+
});
|
|
1262
1315
|
// Build and sign the Kind17 envelope
|
|
1263
|
-
const envelope = buildSignedEnvelope(session,
|
|
1316
|
+
const envelope = buildSignedEnvelope(session, envelopeTags, content, createdAt);
|
|
1317
|
+
log.debug('send-msg envelope signed', {
|
|
1318
|
+
msgId: envelope.id,
|
|
1319
|
+
createdAt,
|
|
1320
|
+
tagKeys: envelope.tags.map(([key]) => key),
|
|
1321
|
+
contentVersion: typeof envelope.content === 'string'
|
|
1322
|
+
? 'v1-string'
|
|
1323
|
+
: `v${String(envelope.content.v ?? 'unknown')}-object`,
|
|
1324
|
+
});
|
|
1264
1325
|
// Output the envelope as JSON (always JSON for machine consumption)
|
|
1265
1326
|
console.log(JSON.stringify(envelope));
|
|
1266
1327
|
// Auto-POST to zMail if not explicitly disabled with --no-zmail.
|
|
@@ -1274,6 +1335,12 @@ async function cmdSendMsg(session) {
|
|
|
1274
1335
|
: (zmailUrlEnv && zmailUrlEnv.length > 0)
|
|
1275
1336
|
? zmailUrlEnv.replace(/\/+$/, '')
|
|
1276
1337
|
: (await import('./config.js')).default.zmail_url;
|
|
1338
|
+
log.debug('send-msg auto-deliver start', {
|
|
1339
|
+
zmailUrl,
|
|
1340
|
+
msgId: envelope.id,
|
|
1341
|
+
recipientPrincipal,
|
|
1342
|
+
kind17Version,
|
|
1343
|
+
});
|
|
1277
1344
|
const { postEnvelopeToZmail } = await import('./zmail.js');
|
|
1278
1345
|
const result = await postEnvelopeToZmail(zmailUrl, envelope);
|
|
1279
1346
|
log.info(`zMail: delivered (msg_id=${result.msg_id}, to=${result.delivered_to})`);
|
|
@@ -1283,6 +1350,39 @@ async function cmdSendMsg(session) {
|
|
|
1283
1350
|
}
|
|
1284
1351
|
}
|
|
1285
1352
|
}
|
|
1353
|
+
function buildKind17V1Content(dpkBytes, ibeIdentity, plaintext, payloadType) {
|
|
1354
|
+
// Deprecated compatibility format:
|
|
1355
|
+
// v1 wraps a single IBE ciphertext in a JSON string body. It is retained so
|
|
1356
|
+
// recv-msg can continue to read historical mail and so send-side output can
|
|
1357
|
+
// still be compared during migration work, but new delivery is expected to
|
|
1358
|
+
// move to Kind17 v2 only.
|
|
1359
|
+
const ciphertext = cryptoOps.ibeEncrypt(dpkBytes, ibeIdentity, plaintext);
|
|
1360
|
+
const content = {
|
|
1361
|
+
v: 1,
|
|
1362
|
+
type: payloadType,
|
|
1363
|
+
ct: Buffer.from(ciphertext).toString('base64'),
|
|
1364
|
+
};
|
|
1365
|
+
return JSON.stringify(content);
|
|
1366
|
+
}
|
|
1367
|
+
function buildKind17V2Content(dpkBytes, senderPrincipal, recipientPrincipal, plaintext, payloadType, context) {
|
|
1368
|
+
const bodyKey = randomBytes(32);
|
|
1369
|
+
const body = cryptoOps.aes256GcmEncryptRaw(bodyKey, plaintext, { aad: buildKind17V2BodyAad(context, payloadType) });
|
|
1370
|
+
const readers = [...new Set([senderPrincipal, recipientPrincipal])];
|
|
1371
|
+
const keys = {};
|
|
1372
|
+
for (const reader of readers) {
|
|
1373
|
+
const wrappedKeyCiphertext = cryptoOps.ibeEncrypt(dpkBytes, `${reader}:Mail`, bodyKey);
|
|
1374
|
+
keys[reader] = Buffer.from(wrappedKeyCiphertext).toString('base64');
|
|
1375
|
+
}
|
|
1376
|
+
return {
|
|
1377
|
+
v: 2,
|
|
1378
|
+
type: payloadType,
|
|
1379
|
+
alg: 'aes-256-gcm',
|
|
1380
|
+
key_alg: 'vetkey-ibe',
|
|
1381
|
+
ciphertext: Buffer.concat([body.ciphertext, body.tag]).toString('base64'),
|
|
1382
|
+
iv: body.iv.toString('base64'),
|
|
1383
|
+
keys,
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1286
1386
|
/**
|
|
1287
1387
|
* recv-msg: Decrypt a received Kind17 encrypted message via the Mail daemon.
|
|
1288
1388
|
*
|
|
@@ -1354,11 +1454,12 @@ async function cmdRecvMsg(session) {
|
|
|
1354
1454
|
// Parse and validate the Kind17 envelope
|
|
1355
1455
|
const envelope = parseKind17Envelope(dataStr);
|
|
1356
1456
|
const principal = session.getPrincipal();
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1457
|
+
log.debug('recv-msg envelope parsed', {
|
|
1458
|
+
msgId: envelope.id,
|
|
1459
|
+
from: envelope.ai_id,
|
|
1460
|
+
createdAt: envelope.created_at,
|
|
1461
|
+
contentType: typeof envelope.content,
|
|
1462
|
+
});
|
|
1362
1463
|
// Verify this envelope is addressed to us
|
|
1363
1464
|
const recipients = envelope.tags.filter(t => t[0] === 'to').map(t => t[1]);
|
|
1364
1465
|
if (!recipients.includes(principal)) {
|
|
@@ -1367,53 +1468,26 @@ async function cmdRecvMsg(session) {
|
|
|
1367
1468
|
// Verify envelope integrity and sender authentication.
|
|
1368
1469
|
// Returns true only if full Schnorr signature + principal binding was verified.
|
|
1369
1470
|
const verifiedSender = verifyKind17Signature(envelope);
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
if (parsed.type !== 'text' && parsed.type !== 'file') {
|
|
1381
|
-
throw new Error(`Invalid message composition format: type must be "text" or "file", got "${String(parsed.type)}"`);
|
|
1382
|
-
}
|
|
1383
|
-
// Verify consistency between the composition type and the envelope payload_type tag
|
|
1384
|
-
if (parsed.type !== payloadType) {
|
|
1385
|
-
throw new Error(`Message composition type "${parsed.type}" does not match envelope payload_type tag "${payloadType}"`);
|
|
1386
|
-
}
|
|
1387
|
-
ciphertextBase64 = parsed.ct;
|
|
1388
|
-
}
|
|
1389
|
-
catch (e) {
|
|
1390
|
-
if (e instanceof SyntaxError) {
|
|
1391
|
-
throw new Error('Invalid message composition format: content is not valid JSON');
|
|
1392
|
-
}
|
|
1393
|
-
throw e;
|
|
1394
|
-
}
|
|
1395
|
-
const mailDerivationId = `${principal}:Mail`;
|
|
1471
|
+
const parsedContent = parseKind17MessageContent(envelope, principal);
|
|
1472
|
+
const payloadType = parsedContent.payloadType;
|
|
1473
|
+
const filename = parsedContent.filename;
|
|
1474
|
+
log.debug('recv-msg content parsed', {
|
|
1475
|
+
msgId: envelope.id,
|
|
1476
|
+
version: parsedContent.version,
|
|
1477
|
+
payloadType,
|
|
1478
|
+
hasFilename: Boolean(filename),
|
|
1479
|
+
verifiedSender,
|
|
1480
|
+
});
|
|
1396
1481
|
await ensureDaemonReadyForRecvMsg(principal);
|
|
1397
|
-
const
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1482
|
+
const plaintextBytes = parsedContent.version === 'v1'
|
|
1483
|
+
? await decryptKind17V1Content(principal, parsedContent)
|
|
1484
|
+
: await decryptKind17V2Content(principal, envelope, parsedContent);
|
|
1485
|
+
log.debug('recv-msg decrypt complete', {
|
|
1486
|
+
msgId: envelope.id,
|
|
1487
|
+
version: parsedContent.version,
|
|
1488
|
+
plaintextSize: plaintextBytes.length,
|
|
1489
|
+
outputMode: output ? 'file' : payloadType === 'file' ? 'auto-file' : 'stdout',
|
|
1405
1490
|
});
|
|
1406
|
-
if (response.error) {
|
|
1407
|
-
throw new Error(`Daemon decryption failed: ${response.error}`);
|
|
1408
|
-
}
|
|
1409
|
-
const result = response.result;
|
|
1410
|
-
if (!result || typeof result.data_base64 !== 'string' || typeof result.plaintext_size !== 'number') {
|
|
1411
|
-
throw new Error('Daemon returned an invalid decrypt result');
|
|
1412
|
-
}
|
|
1413
|
-
const plaintextBytes = decodeBase64Strict(result.data_base64, 'daemon data_base64');
|
|
1414
|
-
if (plaintextBytes.length !== result.plaintext_size) {
|
|
1415
|
-
throw new Error(`Daemon returned mismatched plaintext size: expected ${result.plaintext_size}, got ${plaintextBytes.length}`);
|
|
1416
|
-
}
|
|
1417
1491
|
// Determine output target
|
|
1418
1492
|
const shouldWriteFile = payloadType === 'file' || !!output;
|
|
1419
1493
|
const resolvedOutput = shouldWriteFile
|
|
@@ -1430,7 +1504,7 @@ async function cmdRecvMsg(session) {
|
|
|
1430
1504
|
payload_type: payloadType,
|
|
1431
1505
|
filename,
|
|
1432
1506
|
verified_sender: verifiedSender,
|
|
1433
|
-
plaintext_size:
|
|
1507
|
+
plaintext_size: plaintextBytes.length,
|
|
1434
1508
|
timestamp: envelope.created_at,
|
|
1435
1509
|
};
|
|
1436
1510
|
if (resolvedOutput) {
|
|
@@ -1449,7 +1523,7 @@ async function cmdRecvMsg(session) {
|
|
|
1449
1523
|
if (filename)
|
|
1450
1524
|
console.log(` File Name: ${filename}`);
|
|
1451
1525
|
console.log(` Time: ${new Date(envelope.created_at * 1000).toISOString()}`);
|
|
1452
|
-
console.log(` Size: ${
|
|
1526
|
+
console.log(` Size: ${plaintextBytes.length} bytes`);
|
|
1453
1527
|
console.log(` Output File: ${resolvedOutput}`);
|
|
1454
1528
|
}
|
|
1455
1529
|
else {
|
|
@@ -1459,11 +1533,167 @@ async function cmdRecvMsg(session) {
|
|
|
1459
1533
|
console.log(` Verified: ${verifiedSender ? 'yes' : 'id-only'}`);
|
|
1460
1534
|
console.log(` Payload Type: ${payloadType}`);
|
|
1461
1535
|
console.log(` Time: ${new Date(envelope.created_at * 1000).toISOString()}`);
|
|
1462
|
-
console.log(` Size: ${
|
|
1536
|
+
console.log(` Size: ${plaintextBytes.length} bytes`);
|
|
1463
1537
|
console.log(' Content:');
|
|
1464
1538
|
console.log(plaintextBytes.toString('utf-8'));
|
|
1465
1539
|
}
|
|
1466
1540
|
}
|
|
1541
|
+
function parseKind17MessageContent(envelope, readerPrincipal) {
|
|
1542
|
+
const filename = getTagValue(envelope.tags, 'filename');
|
|
1543
|
+
const legacyPayloadType = getTagValue(envelope.tags, 'payload_type');
|
|
1544
|
+
const legacyIbeIdentity = getTagValue(envelope.tags, 'ibe_id');
|
|
1545
|
+
if (typeof envelope.content === 'string') {
|
|
1546
|
+
log.debug('recv-msg parsing legacy string content', {
|
|
1547
|
+
msgId: envelope.id,
|
|
1548
|
+
readerPrincipal,
|
|
1549
|
+
contentLength: envelope.content.length,
|
|
1550
|
+
});
|
|
1551
|
+
let parsed;
|
|
1552
|
+
try {
|
|
1553
|
+
parsed = JSON.parse(envelope.content);
|
|
1554
|
+
}
|
|
1555
|
+
catch {
|
|
1556
|
+
throw new Error('Invalid message composition format: content is not valid JSON');
|
|
1557
|
+
}
|
|
1558
|
+
if (!parsed || typeof parsed !== 'object' || parsed.v !== 1 || typeof parsed.ct !== 'string') {
|
|
1559
|
+
throw new Error('Invalid message composition format: missing v=1 or ct field');
|
|
1560
|
+
}
|
|
1561
|
+
if (parsed.type !== 'text' && parsed.type !== 'file') {
|
|
1562
|
+
throw new Error(`Invalid message composition format: type must be "text" or "file", got "${String(parsed.type)}"`);
|
|
1563
|
+
}
|
|
1564
|
+
if (legacyPayloadType && parsed.type !== legacyPayloadType) {
|
|
1565
|
+
throw new Error(`Message composition type "${parsed.type}" does not match envelope payload_type tag "${legacyPayloadType}"`);
|
|
1566
|
+
}
|
|
1567
|
+
if (!legacyIbeIdentity) {
|
|
1568
|
+
throw new Error('Legacy Kind17 v1 message is missing required ibe_id tag');
|
|
1569
|
+
}
|
|
1570
|
+
return {
|
|
1571
|
+
version: 'v1',
|
|
1572
|
+
payloadType: parsed.type,
|
|
1573
|
+
ciphertextBase64: parsed.ct,
|
|
1574
|
+
ibeIdentity: legacyIbeIdentity,
|
|
1575
|
+
filename,
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
const parsed = envelope.content;
|
|
1579
|
+
log.debug('recv-msg parsing structured object content', {
|
|
1580
|
+
msgId: envelope.id,
|
|
1581
|
+
readerPrincipal,
|
|
1582
|
+
contentVersion: parsed.v ?? null,
|
|
1583
|
+
});
|
|
1584
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1585
|
+
throw new Error('Invalid Kind17 content: expected a string or object');
|
|
1586
|
+
}
|
|
1587
|
+
if (parsed.v !== 2) {
|
|
1588
|
+
throw new Error(`Unsupported Kind17 content version: ${String(parsed.v)}`);
|
|
1589
|
+
}
|
|
1590
|
+
if (parsed.type !== 'text' && parsed.type !== 'file') {
|
|
1591
|
+
throw new Error(`Invalid Kind17 v2 content type: expected "text" or "file", got "${String(parsed.type)}"`);
|
|
1592
|
+
}
|
|
1593
|
+
if (parsed.alg !== 'aes-256-gcm') {
|
|
1594
|
+
throw new Error(`Unsupported Kind17 v2 body algorithm: ${String(parsed.alg)}`);
|
|
1595
|
+
}
|
|
1596
|
+
if (parsed.key_alg !== 'vetkey-ibe') {
|
|
1597
|
+
throw new Error(`Unsupported Kind17 v2 key algorithm: ${String(parsed.key_alg)}`);
|
|
1598
|
+
}
|
|
1599
|
+
if (typeof parsed.iv !== 'string' || typeof parsed.ciphertext !== 'string') {
|
|
1600
|
+
throw new Error('Invalid Kind17 v2 content: missing iv or ciphertext');
|
|
1601
|
+
}
|
|
1602
|
+
if (!parsed.keys || typeof parsed.keys !== 'object' || Array.isArray(parsed.keys)) {
|
|
1603
|
+
throw new Error('Invalid Kind17 v2 content: keys must be an object');
|
|
1604
|
+
}
|
|
1605
|
+
const wrappedKeyBase64 = parsed.keys[readerPrincipal];
|
|
1606
|
+
if (typeof wrappedKeyBase64 !== 'string' || wrappedKeyBase64.length === 0) {
|
|
1607
|
+
throw new Error(`Kind17 v2 content does not contain a wrapped key for reader "${readerPrincipal}"`);
|
|
1608
|
+
}
|
|
1609
|
+
return {
|
|
1610
|
+
version: 'v2',
|
|
1611
|
+
payloadType: parsed.type,
|
|
1612
|
+
content: parsed,
|
|
1613
|
+
wrappedKeyBase64,
|
|
1614
|
+
filename,
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
async function decryptKind17V1Content(principal, parsed) {
|
|
1618
|
+
// Legacy receive path for historical inbox content. This stays read-only
|
|
1619
|
+
// compatible while v1 send support is being phased out.
|
|
1620
|
+
const response = await sendRpcToSocket(socketPath(principal), {
|
|
1621
|
+
id: 1,
|
|
1622
|
+
method: 'ibe-decrypt',
|
|
1623
|
+
params: {
|
|
1624
|
+
ibe_identity: parsed.ibeIdentity,
|
|
1625
|
+
ciphertext_base64: parsed.ciphertextBase64,
|
|
1626
|
+
},
|
|
1627
|
+
});
|
|
1628
|
+
if (response.error) {
|
|
1629
|
+
throw new Error(`Daemon decryption failed: ${response.error}`);
|
|
1630
|
+
}
|
|
1631
|
+
const result = response.result;
|
|
1632
|
+
if (!result || typeof result.data_base64 !== 'string' || typeof result.plaintext_size !== 'number') {
|
|
1633
|
+
throw new Error('Daemon returned an invalid decrypt result');
|
|
1634
|
+
}
|
|
1635
|
+
const plaintextBytes = decodeBase64Strict(result.data_base64, 'daemon data_base64');
|
|
1636
|
+
if (plaintextBytes.length !== result.plaintext_size) {
|
|
1637
|
+
throw new Error(`Daemon returned mismatched plaintext size: expected ${result.plaintext_size}, got ${plaintextBytes.length}`);
|
|
1638
|
+
}
|
|
1639
|
+
log.debug('recv-msg v1 decrypt result', {
|
|
1640
|
+
principal,
|
|
1641
|
+
plaintextSize: plaintextBytes.length,
|
|
1642
|
+
ibeIdentity: parsed.ibeIdentity,
|
|
1643
|
+
});
|
|
1644
|
+
return plaintextBytes;
|
|
1645
|
+
}
|
|
1646
|
+
async function decryptKind17V2Content(principal, envelope, parsed) {
|
|
1647
|
+
log.debug('recv-msg v2 wrapped-key decrypt start', {
|
|
1648
|
+
msgId: envelope.id,
|
|
1649
|
+
principal,
|
|
1650
|
+
});
|
|
1651
|
+
const wrappedKeyResponse = await sendRpcToSocket(socketPath(principal), {
|
|
1652
|
+
id: 1,
|
|
1653
|
+
method: 'ibe-decrypt',
|
|
1654
|
+
params: {
|
|
1655
|
+
ibe_identity: `${principal}:Mail`,
|
|
1656
|
+
ciphertext_base64: parsed.wrappedKeyBase64,
|
|
1657
|
+
},
|
|
1658
|
+
});
|
|
1659
|
+
if (wrappedKeyResponse.error) {
|
|
1660
|
+
throw new Error(`Daemon decryption failed: ${wrappedKeyResponse.error}`);
|
|
1661
|
+
}
|
|
1662
|
+
const wrappedKeyResult = wrappedKeyResponse.result;
|
|
1663
|
+
if (!wrappedKeyResult
|
|
1664
|
+
|| typeof wrappedKeyResult.data_base64 !== 'string'
|
|
1665
|
+
|| typeof wrappedKeyResult.plaintext_size !== 'number') {
|
|
1666
|
+
throw new Error('Daemon returned an invalid wrapped-key decrypt result');
|
|
1667
|
+
}
|
|
1668
|
+
const bodyKey = decodeBase64Strict(wrappedKeyResult.data_base64, 'daemon data_base64');
|
|
1669
|
+
if (bodyKey.length !== wrappedKeyResult.plaintext_size) {
|
|
1670
|
+
throw new Error(`Daemon returned mismatched wrapped-key size: expected ${wrappedKeyResult.plaintext_size}, got ${bodyKey.length}`);
|
|
1671
|
+
}
|
|
1672
|
+
log.debug('recv-msg v2 wrapped-key decrypt result', {
|
|
1673
|
+
msgId: envelope.id,
|
|
1674
|
+
principal,
|
|
1675
|
+
wrappedKeySize: bodyKey.length,
|
|
1676
|
+
});
|
|
1677
|
+
const iv = decodeBase64Strict(parsed.content.iv, 'content.iv');
|
|
1678
|
+
const rawCiphertext = decodeBase64Strict(parsed.content.ciphertext, 'content.ciphertext');
|
|
1679
|
+
const { ciphertext, tag } = splitCiphertextAndTag(rawCiphertext);
|
|
1680
|
+
const plaintext = cryptoOps.aes256GcmDecryptRaw(bodyKey, ciphertext, iv, tag, {
|
|
1681
|
+
aad: buildKind17V2BodyAad({
|
|
1682
|
+
kind: 17,
|
|
1683
|
+
ai_id: envelope.ai_id,
|
|
1684
|
+
created_at: envelope.created_at,
|
|
1685
|
+
tags: envelope.tags,
|
|
1686
|
+
}, parsed.payloadType),
|
|
1687
|
+
});
|
|
1688
|
+
log.debug('recv-msg v2 body decrypt result', {
|
|
1689
|
+
msgId: envelope.id,
|
|
1690
|
+
principal,
|
|
1691
|
+
ivSize: iv.length,
|
|
1692
|
+
ciphertextSize: ciphertext.length,
|
|
1693
|
+
plaintextSize: plaintext.length,
|
|
1694
|
+
});
|
|
1695
|
+
return plaintext;
|
|
1696
|
+
}
|
|
1467
1697
|
// ── Message Input Parsing ────────────────────────────────────────────────────
|
|
1468
1698
|
function readMessageInput(text, file) {
|
|
1469
1699
|
if (text && file)
|
|
@@ -1526,8 +1756,10 @@ function parseKind17Envelope(dataStr) {
|
|
|
1526
1756
|
if (!Array.isArray(e.tags)) {
|
|
1527
1757
|
throw new Error('Invalid envelope: tags must be an array');
|
|
1528
1758
|
}
|
|
1529
|
-
|
|
1530
|
-
|
|
1759
|
+
const contentValid = ((typeof e.content === 'string' && e.content.length > 0)
|
|
1760
|
+
|| (e.content !== null && typeof e.content === 'object' && !Array.isArray(e.content)));
|
|
1761
|
+
if (!contentValid) {
|
|
1762
|
+
throw new Error('Invalid envelope: content must be a non-empty string or object');
|
|
1531
1763
|
}
|
|
1532
1764
|
if (typeof e.sig !== 'string' || e.sig.length === 0) {
|
|
1533
1765
|
throw new Error('Invalid envelope: sig must be a non-empty string');
|