@zcloak/ai-agent 1.0.41 → 1.0.43
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.d.ts +1 -2
- package/dist/openclaw.js +15 -38
- package/dist/openclaw.js.map +1 -1
- package/dist/pre-check.d.ts +1 -1
- package/dist/pre-check.js +8 -4
- package/dist/pre-check.js.map +1 -1
- package/dist/vetkey.d.ts +15 -2
- package/dist/vetkey.js +311 -84
- 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
|
}
|
|
@@ -439,7 +440,7 @@ async function cmdServe(session) {
|
|
|
439
440
|
}
|
|
440
441
|
log.info("Key derivation complete. Starting JSON-RPC daemon...");
|
|
441
442
|
const { syncMailbox } = await import('./zmail.js');
|
|
442
|
-
const {
|
|
443
|
+
const { notifyOpenClawMainAgentOfNewMail } = await import('./openclaw.js');
|
|
443
444
|
await runDaemonUds(activeKeyStore, mailKeyStore, principal, daemonId, [
|
|
444
445
|
() => startPeriodicAsyncTask({
|
|
445
446
|
name: 'zmail-sync',
|
|
@@ -447,12 +448,7 @@ async function cmdServe(session) {
|
|
|
447
448
|
task: async () => {
|
|
448
449
|
const summary = await syncMailbox(session, { fullSync: false, logProgress: false });
|
|
449
450
|
if (summary.inbox_new > 0) {
|
|
450
|
-
const
|
|
451
|
-
if (!channelContext) {
|
|
452
|
-
log.warn(`Daemon zMail sync: ${summary.inbox_new} new inbox, ${summary.sent_new} new sent (openclaw status unavailable)`);
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
const notified = await notifyOpenClawMainAgentOfNewMail(summary, channelContext);
|
|
451
|
+
const notified = await notifyOpenClawMainAgentOfNewMail(summary);
|
|
456
452
|
if (notified) {
|
|
457
453
|
log.info(`Daemon zMail sync: ${summary.inbox_new} new inbox, ${summary.sent_new} new sent (notified openclaw main)`);
|
|
458
454
|
return;
|
|
@@ -1112,6 +1108,16 @@ function computeEnvelopeId(envelope) {
|
|
|
1112
1108
|
];
|
|
1113
1109
|
return createHash('sha256').update(JSON.stringify(payload), 'utf8').digest('hex');
|
|
1114
1110
|
}
|
|
1111
|
+
function kind17VersionFlag(session) {
|
|
1112
|
+
const raw = session.args['kind17-version'];
|
|
1113
|
+
if (raw === undefined) {
|
|
1114
|
+
return 'v2';
|
|
1115
|
+
}
|
|
1116
|
+
if (raw !== 'v1' && raw !== 'v2') {
|
|
1117
|
+
throw new Error('Invalid --kind17-version value: expected "v1" or "v2"');
|
|
1118
|
+
}
|
|
1119
|
+
return raw;
|
|
1120
|
+
}
|
|
1115
1121
|
/**
|
|
1116
1122
|
* Extract the raw 32-byte secp256k1 private key from the session identity.
|
|
1117
1123
|
* The Secp256k1KeyIdentity.toJSON() returns [publicKeyHex, privateKeyHex].
|
|
@@ -1135,6 +1141,33 @@ const SPKI_X_LENGTH = 64;
|
|
|
1135
1141
|
export function schnorrPubkeyFromSpki(spkiHex) {
|
|
1136
1142
|
return spkiHex.slice(SPKI_X_OFFSET, SPKI_X_OFFSET + SPKI_X_LENGTH);
|
|
1137
1143
|
}
|
|
1144
|
+
function withSenderPubkeyTag(session, tags) {
|
|
1145
|
+
if (getTagValue(tags, 'from_pubkey')) {
|
|
1146
|
+
return tags;
|
|
1147
|
+
}
|
|
1148
|
+
const senderSpkiHex = Buffer.from(session.getIdentity().getPublicKey().toDer()).toString('hex');
|
|
1149
|
+
return [...tags, ['from_pubkey', senderSpkiHex]];
|
|
1150
|
+
}
|
|
1151
|
+
function canonicalSerializeEnvelopeCryptoContext(context) {
|
|
1152
|
+
return JSON.stringify(canonicalize({
|
|
1153
|
+
kind: context.kind,
|
|
1154
|
+
ai_id: context.ai_id,
|
|
1155
|
+
created_at: context.created_at,
|
|
1156
|
+
tags: context.tags,
|
|
1157
|
+
}));
|
|
1158
|
+
}
|
|
1159
|
+
function buildKind17V2BodyAad(context, payloadType) {
|
|
1160
|
+
return Buffer.from(`${canonicalSerializeEnvelopeCryptoContext(context)}|body|${payloadType}|v2`, 'utf8');
|
|
1161
|
+
}
|
|
1162
|
+
function splitCiphertextAndTag(raw) {
|
|
1163
|
+
if (raw.length < 16) {
|
|
1164
|
+
throw new Error('Invalid Kind17 v2 ciphertext: missing authentication tag');
|
|
1165
|
+
}
|
|
1166
|
+
return {
|
|
1167
|
+
ciphertext: raw.slice(0, raw.length - 16),
|
|
1168
|
+
tag: raw.slice(raw.length - 16),
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1138
1171
|
/**
|
|
1139
1172
|
* Build, sign, and return a complete Kind17 envelope.
|
|
1140
1173
|
*
|
|
@@ -1147,14 +1180,9 @@ export function schnorrPubkeyFromSpki(spkiHex) {
|
|
|
1147
1180
|
* @param content - Encrypted content JSON string: {"v":1,"type":"text"|"file","ct":"<base64>"}
|
|
1148
1181
|
* @returns Signed Kind17Envelope ready for JSON serialization
|
|
1149
1182
|
*/
|
|
1150
|
-
function buildSignedEnvelope(session, tags, content) {
|
|
1183
|
+
function buildSignedEnvelope(session, tags, content, createdAt = Math.floor(Date.now() / 1000)) {
|
|
1151
1184
|
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]];
|
|
1185
|
+
const allTags = withSenderPubkeyTag(session, tags);
|
|
1158
1186
|
// Compute the envelope ID from canonical serialization
|
|
1159
1187
|
const partial = { kind: 17, ai_id: senderPrincipal, created_at: createdAt, tags: allTags, content };
|
|
1160
1188
|
const id = computeEnvelopeId(partial);
|
|
@@ -1196,6 +1224,11 @@ async function cmdSendMsg(session) {
|
|
|
1196
1224
|
throw new Error('--to=<AI-ID or principal> is required');
|
|
1197
1225
|
}
|
|
1198
1226
|
const { plaintext, payloadType, filename } = readMessageInput(text, file);
|
|
1227
|
+
const kind17Version = kind17VersionFlag(session);
|
|
1228
|
+
// Legacy Kind17 v1 generation is kept only for compatibility/debug comparison.
|
|
1229
|
+
// The default zMail backend is the V2 endpoint, which is expected to reject
|
|
1230
|
+
// newly sent v1 content with unsupported_content_version. Do not treat that
|
|
1231
|
+
// as a regression in the upgraded path — v1 is on the deprecation path.
|
|
1199
1232
|
// Resolve recipient principal from AI-ID (readable) or raw principal text.
|
|
1200
1233
|
let recipientPrincipal;
|
|
1201
1234
|
const registryActor = await session.getAnonymousRegistryActor();
|
|
@@ -1239,28 +1272,51 @@ async function cmdSendMsg(session) {
|
|
|
1239
1272
|
catch (e) {
|
|
1240
1273
|
throw canisterCallError(`get_ibe_public_key failed: ${e instanceof Error ? e.message : String(e)}`, e);
|
|
1241
1274
|
}
|
|
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
1275
|
// Build tags: recipient + metadata
|
|
1250
|
-
const
|
|
1276
|
+
const baseTags = [
|
|
1251
1277
|
['to', recipientPrincipal],
|
|
1252
|
-
['payload_type', payloadType],
|
|
1253
|
-
['ibe_id', ibeIdentity],
|
|
1254
1278
|
];
|
|
1279
|
+
if (kind17Version === 'v1') {
|
|
1280
|
+
baseTags.push(['payload_type', payloadType], ['ibe_id', ibeIdentity]);
|
|
1281
|
+
}
|
|
1255
1282
|
if (filename) {
|
|
1256
|
-
|
|
1283
|
+
baseTags.push(['filename', filename]);
|
|
1257
1284
|
}
|
|
1258
1285
|
// Include reply tag when responding to an existing message
|
|
1259
1286
|
if (replyMsgId) {
|
|
1260
|
-
|
|
1287
|
+
baseTags.push(['reply', replyMsgId]);
|
|
1261
1288
|
}
|
|
1289
|
+
const createdAt = Math.floor(Date.now() / 1000);
|
|
1290
|
+
const envelopeTags = withSenderPubkeyTag(session, baseTags);
|
|
1291
|
+
const senderPrincipal = session.getPrincipal();
|
|
1292
|
+
const content = kind17Version === 'v1'
|
|
1293
|
+
? buildKind17V1Content(dpkBytes, ibeIdentity, plaintext, payloadType)
|
|
1294
|
+
: buildKind17V2Content(dpkBytes, senderPrincipal, recipientPrincipal, plaintext, payloadType, {
|
|
1295
|
+
kind: 17,
|
|
1296
|
+
ai_id: senderPrincipal,
|
|
1297
|
+
created_at: createdAt,
|
|
1298
|
+
tags: envelopeTags,
|
|
1299
|
+
});
|
|
1300
|
+
log.debug('send-msg envelope prepared', {
|
|
1301
|
+
kind17Version,
|
|
1302
|
+
senderPrincipal,
|
|
1303
|
+
recipientPrincipal,
|
|
1304
|
+
payloadType,
|
|
1305
|
+
plaintextSize: plaintext.length,
|
|
1306
|
+
hasFilename: Boolean(filename),
|
|
1307
|
+
hasReply: Boolean(replyMsgId),
|
|
1308
|
+
contentType: typeof content,
|
|
1309
|
+
});
|
|
1262
1310
|
// Build and sign the Kind17 envelope
|
|
1263
|
-
const envelope = buildSignedEnvelope(session,
|
|
1311
|
+
const envelope = buildSignedEnvelope(session, envelopeTags, content, createdAt);
|
|
1312
|
+
log.debug('send-msg envelope signed', {
|
|
1313
|
+
msgId: envelope.id,
|
|
1314
|
+
createdAt,
|
|
1315
|
+
tagKeys: envelope.tags.map(([key]) => key),
|
|
1316
|
+
contentVersion: typeof envelope.content === 'string'
|
|
1317
|
+
? 'v1-string'
|
|
1318
|
+
: `v${String(envelope.content.v ?? 'unknown')}-object`,
|
|
1319
|
+
});
|
|
1264
1320
|
// Output the envelope as JSON (always JSON for machine consumption)
|
|
1265
1321
|
console.log(JSON.stringify(envelope));
|
|
1266
1322
|
// Auto-POST to zMail if not explicitly disabled with --no-zmail.
|
|
@@ -1274,6 +1330,12 @@ async function cmdSendMsg(session) {
|
|
|
1274
1330
|
: (zmailUrlEnv && zmailUrlEnv.length > 0)
|
|
1275
1331
|
? zmailUrlEnv.replace(/\/+$/, '')
|
|
1276
1332
|
: (await import('./config.js')).default.zmail_url;
|
|
1333
|
+
log.debug('send-msg auto-deliver start', {
|
|
1334
|
+
zmailUrl,
|
|
1335
|
+
msgId: envelope.id,
|
|
1336
|
+
recipientPrincipal,
|
|
1337
|
+
kind17Version,
|
|
1338
|
+
});
|
|
1277
1339
|
const { postEnvelopeToZmail } = await import('./zmail.js');
|
|
1278
1340
|
const result = await postEnvelopeToZmail(zmailUrl, envelope);
|
|
1279
1341
|
log.info(`zMail: delivered (msg_id=${result.msg_id}, to=${result.delivered_to})`);
|
|
@@ -1283,6 +1345,39 @@ async function cmdSendMsg(session) {
|
|
|
1283
1345
|
}
|
|
1284
1346
|
}
|
|
1285
1347
|
}
|
|
1348
|
+
function buildKind17V1Content(dpkBytes, ibeIdentity, plaintext, payloadType) {
|
|
1349
|
+
// Deprecated compatibility format:
|
|
1350
|
+
// v1 wraps a single IBE ciphertext in a JSON string body. It is retained so
|
|
1351
|
+
// recv-msg can continue to read historical mail and so send-side output can
|
|
1352
|
+
// still be compared during migration work, but new delivery is expected to
|
|
1353
|
+
// move to Kind17 v2 only.
|
|
1354
|
+
const ciphertext = cryptoOps.ibeEncrypt(dpkBytes, ibeIdentity, plaintext);
|
|
1355
|
+
const content = {
|
|
1356
|
+
v: 1,
|
|
1357
|
+
type: payloadType,
|
|
1358
|
+
ct: Buffer.from(ciphertext).toString('base64'),
|
|
1359
|
+
};
|
|
1360
|
+
return JSON.stringify(content);
|
|
1361
|
+
}
|
|
1362
|
+
function buildKind17V2Content(dpkBytes, senderPrincipal, recipientPrincipal, plaintext, payloadType, context) {
|
|
1363
|
+
const bodyKey = randomBytes(32);
|
|
1364
|
+
const body = cryptoOps.aes256GcmEncryptRaw(bodyKey, plaintext, { aad: buildKind17V2BodyAad(context, payloadType) });
|
|
1365
|
+
const readers = [...new Set([senderPrincipal, recipientPrincipal])];
|
|
1366
|
+
const keys = {};
|
|
1367
|
+
for (const reader of readers) {
|
|
1368
|
+
const wrappedKeyCiphertext = cryptoOps.ibeEncrypt(dpkBytes, `${reader}:Mail`, bodyKey);
|
|
1369
|
+
keys[reader] = Buffer.from(wrappedKeyCiphertext).toString('base64');
|
|
1370
|
+
}
|
|
1371
|
+
return {
|
|
1372
|
+
v: 2,
|
|
1373
|
+
type: payloadType,
|
|
1374
|
+
alg: 'aes-256-gcm',
|
|
1375
|
+
key_alg: 'vetkey-ibe',
|
|
1376
|
+
ciphertext: Buffer.concat([body.ciphertext, body.tag]).toString('base64'),
|
|
1377
|
+
iv: body.iv.toString('base64'),
|
|
1378
|
+
keys,
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1286
1381
|
/**
|
|
1287
1382
|
* recv-msg: Decrypt a received Kind17 encrypted message via the Mail daemon.
|
|
1288
1383
|
*
|
|
@@ -1354,11 +1449,12 @@ async function cmdRecvMsg(session) {
|
|
|
1354
1449
|
// Parse and validate the Kind17 envelope
|
|
1355
1450
|
const envelope = parseKind17Envelope(dataStr);
|
|
1356
1451
|
const principal = session.getPrincipal();
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1452
|
+
log.debug('recv-msg envelope parsed', {
|
|
1453
|
+
msgId: envelope.id,
|
|
1454
|
+
from: envelope.ai_id,
|
|
1455
|
+
createdAt: envelope.created_at,
|
|
1456
|
+
contentType: typeof envelope.content,
|
|
1457
|
+
});
|
|
1362
1458
|
// Verify this envelope is addressed to us
|
|
1363
1459
|
const recipients = envelope.tags.filter(t => t[0] === 'to').map(t => t[1]);
|
|
1364
1460
|
if (!recipients.includes(principal)) {
|
|
@@ -1367,53 +1463,26 @@ async function cmdRecvMsg(session) {
|
|
|
1367
1463
|
// Verify envelope integrity and sender authentication.
|
|
1368
1464
|
// Returns true only if full Schnorr signature + principal binding was verified.
|
|
1369
1465
|
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`;
|
|
1466
|
+
const parsedContent = parseKind17MessageContent(envelope, principal);
|
|
1467
|
+
const payloadType = parsedContent.payloadType;
|
|
1468
|
+
const filename = parsedContent.filename;
|
|
1469
|
+
log.debug('recv-msg content parsed', {
|
|
1470
|
+
msgId: envelope.id,
|
|
1471
|
+
version: parsedContent.version,
|
|
1472
|
+
payloadType,
|
|
1473
|
+
hasFilename: Boolean(filename),
|
|
1474
|
+
verifiedSender,
|
|
1475
|
+
});
|
|
1396
1476
|
await ensureDaemonReadyForRecvMsg(principal);
|
|
1397
|
-
const
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1477
|
+
const plaintextBytes = parsedContent.version === 'v1'
|
|
1478
|
+
? await decryptKind17V1Content(principal, parsedContent)
|
|
1479
|
+
: await decryptKind17V2Content(principal, envelope, parsedContent);
|
|
1480
|
+
log.debug('recv-msg decrypt complete', {
|
|
1481
|
+
msgId: envelope.id,
|
|
1482
|
+
version: parsedContent.version,
|
|
1483
|
+
plaintextSize: plaintextBytes.length,
|
|
1484
|
+
outputMode: output ? 'file' : payloadType === 'file' ? 'auto-file' : 'stdout',
|
|
1405
1485
|
});
|
|
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
1486
|
// Determine output target
|
|
1418
1487
|
const shouldWriteFile = payloadType === 'file' || !!output;
|
|
1419
1488
|
const resolvedOutput = shouldWriteFile
|
|
@@ -1430,7 +1499,7 @@ async function cmdRecvMsg(session) {
|
|
|
1430
1499
|
payload_type: payloadType,
|
|
1431
1500
|
filename,
|
|
1432
1501
|
verified_sender: verifiedSender,
|
|
1433
|
-
plaintext_size:
|
|
1502
|
+
plaintext_size: plaintextBytes.length,
|
|
1434
1503
|
timestamp: envelope.created_at,
|
|
1435
1504
|
};
|
|
1436
1505
|
if (resolvedOutput) {
|
|
@@ -1449,7 +1518,7 @@ async function cmdRecvMsg(session) {
|
|
|
1449
1518
|
if (filename)
|
|
1450
1519
|
console.log(` File Name: ${filename}`);
|
|
1451
1520
|
console.log(` Time: ${new Date(envelope.created_at * 1000).toISOString()}`);
|
|
1452
|
-
console.log(` Size: ${
|
|
1521
|
+
console.log(` Size: ${plaintextBytes.length} bytes`);
|
|
1453
1522
|
console.log(` Output File: ${resolvedOutput}`);
|
|
1454
1523
|
}
|
|
1455
1524
|
else {
|
|
@@ -1459,11 +1528,167 @@ async function cmdRecvMsg(session) {
|
|
|
1459
1528
|
console.log(` Verified: ${verifiedSender ? 'yes' : 'id-only'}`);
|
|
1460
1529
|
console.log(` Payload Type: ${payloadType}`);
|
|
1461
1530
|
console.log(` Time: ${new Date(envelope.created_at * 1000).toISOString()}`);
|
|
1462
|
-
console.log(` Size: ${
|
|
1531
|
+
console.log(` Size: ${plaintextBytes.length} bytes`);
|
|
1463
1532
|
console.log(' Content:');
|
|
1464
1533
|
console.log(plaintextBytes.toString('utf-8'));
|
|
1465
1534
|
}
|
|
1466
1535
|
}
|
|
1536
|
+
function parseKind17MessageContent(envelope, readerPrincipal) {
|
|
1537
|
+
const filename = getTagValue(envelope.tags, 'filename');
|
|
1538
|
+
const legacyPayloadType = getTagValue(envelope.tags, 'payload_type');
|
|
1539
|
+
const legacyIbeIdentity = getTagValue(envelope.tags, 'ibe_id');
|
|
1540
|
+
if (typeof envelope.content === 'string') {
|
|
1541
|
+
log.debug('recv-msg parsing legacy string content', {
|
|
1542
|
+
msgId: envelope.id,
|
|
1543
|
+
readerPrincipal,
|
|
1544
|
+
contentLength: envelope.content.length,
|
|
1545
|
+
});
|
|
1546
|
+
let parsed;
|
|
1547
|
+
try {
|
|
1548
|
+
parsed = JSON.parse(envelope.content);
|
|
1549
|
+
}
|
|
1550
|
+
catch {
|
|
1551
|
+
throw new Error('Invalid message composition format: content is not valid JSON');
|
|
1552
|
+
}
|
|
1553
|
+
if (!parsed || typeof parsed !== 'object' || parsed.v !== 1 || typeof parsed.ct !== 'string') {
|
|
1554
|
+
throw new Error('Invalid message composition format: missing v=1 or ct field');
|
|
1555
|
+
}
|
|
1556
|
+
if (parsed.type !== 'text' && parsed.type !== 'file') {
|
|
1557
|
+
throw new Error(`Invalid message composition format: type must be "text" or "file", got "${String(parsed.type)}"`);
|
|
1558
|
+
}
|
|
1559
|
+
if (legacyPayloadType && parsed.type !== legacyPayloadType) {
|
|
1560
|
+
throw new Error(`Message composition type "${parsed.type}" does not match envelope payload_type tag "${legacyPayloadType}"`);
|
|
1561
|
+
}
|
|
1562
|
+
if (!legacyIbeIdentity) {
|
|
1563
|
+
throw new Error('Legacy Kind17 v1 message is missing required ibe_id tag');
|
|
1564
|
+
}
|
|
1565
|
+
return {
|
|
1566
|
+
version: 'v1',
|
|
1567
|
+
payloadType: parsed.type,
|
|
1568
|
+
ciphertextBase64: parsed.ct,
|
|
1569
|
+
ibeIdentity: legacyIbeIdentity,
|
|
1570
|
+
filename,
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
const parsed = envelope.content;
|
|
1574
|
+
log.debug('recv-msg parsing structured object content', {
|
|
1575
|
+
msgId: envelope.id,
|
|
1576
|
+
readerPrincipal,
|
|
1577
|
+
contentVersion: parsed.v ?? null,
|
|
1578
|
+
});
|
|
1579
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1580
|
+
throw new Error('Invalid Kind17 content: expected a string or object');
|
|
1581
|
+
}
|
|
1582
|
+
if (parsed.v !== 2) {
|
|
1583
|
+
throw new Error(`Unsupported Kind17 content version: ${String(parsed.v)}`);
|
|
1584
|
+
}
|
|
1585
|
+
if (parsed.type !== 'text' && parsed.type !== 'file') {
|
|
1586
|
+
throw new Error(`Invalid Kind17 v2 content type: expected "text" or "file", got "${String(parsed.type)}"`);
|
|
1587
|
+
}
|
|
1588
|
+
if (parsed.alg !== 'aes-256-gcm') {
|
|
1589
|
+
throw new Error(`Unsupported Kind17 v2 body algorithm: ${String(parsed.alg)}`);
|
|
1590
|
+
}
|
|
1591
|
+
if (parsed.key_alg !== 'vetkey-ibe') {
|
|
1592
|
+
throw new Error(`Unsupported Kind17 v2 key algorithm: ${String(parsed.key_alg)}`);
|
|
1593
|
+
}
|
|
1594
|
+
if (typeof parsed.iv !== 'string' || typeof parsed.ciphertext !== 'string') {
|
|
1595
|
+
throw new Error('Invalid Kind17 v2 content: missing iv or ciphertext');
|
|
1596
|
+
}
|
|
1597
|
+
if (!parsed.keys || typeof parsed.keys !== 'object' || Array.isArray(parsed.keys)) {
|
|
1598
|
+
throw new Error('Invalid Kind17 v2 content: keys must be an object');
|
|
1599
|
+
}
|
|
1600
|
+
const wrappedKeyBase64 = parsed.keys[readerPrincipal];
|
|
1601
|
+
if (typeof wrappedKeyBase64 !== 'string' || wrappedKeyBase64.length === 0) {
|
|
1602
|
+
throw new Error(`Kind17 v2 content does not contain a wrapped key for reader "${readerPrincipal}"`);
|
|
1603
|
+
}
|
|
1604
|
+
return {
|
|
1605
|
+
version: 'v2',
|
|
1606
|
+
payloadType: parsed.type,
|
|
1607
|
+
content: parsed,
|
|
1608
|
+
wrappedKeyBase64,
|
|
1609
|
+
filename,
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
async function decryptKind17V1Content(principal, parsed) {
|
|
1613
|
+
// Legacy receive path for historical inbox content. This stays read-only
|
|
1614
|
+
// compatible while v1 send support is being phased out.
|
|
1615
|
+
const response = await sendRpcToSocket(socketPath(principal), {
|
|
1616
|
+
id: 1,
|
|
1617
|
+
method: 'ibe-decrypt',
|
|
1618
|
+
params: {
|
|
1619
|
+
ibe_identity: parsed.ibeIdentity,
|
|
1620
|
+
ciphertext_base64: parsed.ciphertextBase64,
|
|
1621
|
+
},
|
|
1622
|
+
});
|
|
1623
|
+
if (response.error) {
|
|
1624
|
+
throw new Error(`Daemon decryption failed: ${response.error}`);
|
|
1625
|
+
}
|
|
1626
|
+
const result = response.result;
|
|
1627
|
+
if (!result || typeof result.data_base64 !== 'string' || typeof result.plaintext_size !== 'number') {
|
|
1628
|
+
throw new Error('Daemon returned an invalid decrypt result');
|
|
1629
|
+
}
|
|
1630
|
+
const plaintextBytes = decodeBase64Strict(result.data_base64, 'daemon data_base64');
|
|
1631
|
+
if (plaintextBytes.length !== result.plaintext_size) {
|
|
1632
|
+
throw new Error(`Daemon returned mismatched plaintext size: expected ${result.plaintext_size}, got ${plaintextBytes.length}`);
|
|
1633
|
+
}
|
|
1634
|
+
log.debug('recv-msg v1 decrypt result', {
|
|
1635
|
+
principal,
|
|
1636
|
+
plaintextSize: plaintextBytes.length,
|
|
1637
|
+
ibeIdentity: parsed.ibeIdentity,
|
|
1638
|
+
});
|
|
1639
|
+
return plaintextBytes;
|
|
1640
|
+
}
|
|
1641
|
+
async function decryptKind17V2Content(principal, envelope, parsed) {
|
|
1642
|
+
log.debug('recv-msg v2 wrapped-key decrypt start', {
|
|
1643
|
+
msgId: envelope.id,
|
|
1644
|
+
principal,
|
|
1645
|
+
});
|
|
1646
|
+
const wrappedKeyResponse = await sendRpcToSocket(socketPath(principal), {
|
|
1647
|
+
id: 1,
|
|
1648
|
+
method: 'ibe-decrypt',
|
|
1649
|
+
params: {
|
|
1650
|
+
ibe_identity: `${principal}:Mail`,
|
|
1651
|
+
ciphertext_base64: parsed.wrappedKeyBase64,
|
|
1652
|
+
},
|
|
1653
|
+
});
|
|
1654
|
+
if (wrappedKeyResponse.error) {
|
|
1655
|
+
throw new Error(`Daemon decryption failed: ${wrappedKeyResponse.error}`);
|
|
1656
|
+
}
|
|
1657
|
+
const wrappedKeyResult = wrappedKeyResponse.result;
|
|
1658
|
+
if (!wrappedKeyResult
|
|
1659
|
+
|| typeof wrappedKeyResult.data_base64 !== 'string'
|
|
1660
|
+
|| typeof wrappedKeyResult.plaintext_size !== 'number') {
|
|
1661
|
+
throw new Error('Daemon returned an invalid wrapped-key decrypt result');
|
|
1662
|
+
}
|
|
1663
|
+
const bodyKey = decodeBase64Strict(wrappedKeyResult.data_base64, 'daemon data_base64');
|
|
1664
|
+
if (bodyKey.length !== wrappedKeyResult.plaintext_size) {
|
|
1665
|
+
throw new Error(`Daemon returned mismatched wrapped-key size: expected ${wrappedKeyResult.plaintext_size}, got ${bodyKey.length}`);
|
|
1666
|
+
}
|
|
1667
|
+
log.debug('recv-msg v2 wrapped-key decrypt result', {
|
|
1668
|
+
msgId: envelope.id,
|
|
1669
|
+
principal,
|
|
1670
|
+
wrappedKeySize: bodyKey.length,
|
|
1671
|
+
});
|
|
1672
|
+
const iv = decodeBase64Strict(parsed.content.iv, 'content.iv');
|
|
1673
|
+
const rawCiphertext = decodeBase64Strict(parsed.content.ciphertext, 'content.ciphertext');
|
|
1674
|
+
const { ciphertext, tag } = splitCiphertextAndTag(rawCiphertext);
|
|
1675
|
+
const plaintext = cryptoOps.aes256GcmDecryptRaw(bodyKey, ciphertext, iv, tag, {
|
|
1676
|
+
aad: buildKind17V2BodyAad({
|
|
1677
|
+
kind: 17,
|
|
1678
|
+
ai_id: envelope.ai_id,
|
|
1679
|
+
created_at: envelope.created_at,
|
|
1680
|
+
tags: envelope.tags,
|
|
1681
|
+
}, parsed.payloadType),
|
|
1682
|
+
});
|
|
1683
|
+
log.debug('recv-msg v2 body decrypt result', {
|
|
1684
|
+
msgId: envelope.id,
|
|
1685
|
+
principal,
|
|
1686
|
+
ivSize: iv.length,
|
|
1687
|
+
ciphertextSize: ciphertext.length,
|
|
1688
|
+
plaintextSize: plaintext.length,
|
|
1689
|
+
});
|
|
1690
|
+
return plaintext;
|
|
1691
|
+
}
|
|
1467
1692
|
// ── Message Input Parsing ────────────────────────────────────────────────────
|
|
1468
1693
|
function readMessageInput(text, file) {
|
|
1469
1694
|
if (text && file)
|
|
@@ -1526,8 +1751,10 @@ function parseKind17Envelope(dataStr) {
|
|
|
1526
1751
|
if (!Array.isArray(e.tags)) {
|
|
1527
1752
|
throw new Error('Invalid envelope: tags must be an array');
|
|
1528
1753
|
}
|
|
1529
|
-
|
|
1530
|
-
|
|
1754
|
+
const contentValid = ((typeof e.content === 'string' && e.content.length > 0)
|
|
1755
|
+
|| (e.content !== null && typeof e.content === 'object' && !Array.isArray(e.content)));
|
|
1756
|
+
if (!contentValid) {
|
|
1757
|
+
throw new Error('Invalid envelope: content must be a non-empty string or object');
|
|
1531
1758
|
}
|
|
1532
1759
|
if (typeof e.sig !== 'string' || e.sig.length === 0) {
|
|
1533
1760
|
throw new Error('Invalid envelope: sig must be a non-empty string');
|