@lawrenceliang-btc/atel-sdk 0.8.7
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/LICENSE +21 -0
- package/README.md +221 -0
- package/bin/atel.mjs +2692 -0
- package/bin/tunnel-manager.mjs +171 -0
- package/dist/anchor/base.d.ts +21 -0
- package/dist/anchor/base.js +26 -0
- package/dist/anchor/bsc.d.ts +20 -0
- package/dist/anchor/bsc.js +25 -0
- package/dist/anchor/evm.d.ts +99 -0
- package/dist/anchor/evm.js +262 -0
- package/dist/anchor/index.d.ts +173 -0
- package/dist/anchor/index.js +165 -0
- package/dist/anchor/mock.d.ts +43 -0
- package/dist/anchor/mock.js +100 -0
- package/dist/anchor/solana.d.ts +95 -0
- package/dist/anchor/solana.js +298 -0
- package/dist/auditor/index.d.ts +54 -0
- package/dist/auditor/index.js +141 -0
- package/dist/collaboration/index.d.ts +146 -0
- package/dist/collaboration/index.js +237 -0
- package/dist/crypto/index.d.ts +162 -0
- package/dist/crypto/index.js +231 -0
- package/dist/endpoint/index.d.ts +147 -0
- package/dist/endpoint/index.js +390 -0
- package/dist/envelope/index.d.ts +104 -0
- package/dist/envelope/index.js +156 -0
- package/dist/executor/index.d.ts +71 -0
- package/dist/executor/index.js +398 -0
- package/dist/gateway/index.d.ts +278 -0
- package/dist/gateway/index.js +520 -0
- package/dist/graph/index.d.ts +215 -0
- package/dist/graph/index.js +524 -0
- package/dist/handshake/index.d.ts +166 -0
- package/dist/handshake/index.js +287 -0
- package/dist/identity/index.d.ts +155 -0
- package/dist/identity/index.js +250 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +28 -0
- package/dist/negotiation/index.d.ts +133 -0
- package/dist/negotiation/index.js +160 -0
- package/dist/network/index.d.ts +78 -0
- package/dist/network/index.js +207 -0
- package/dist/orchestrator/index.d.ts +190 -0
- package/dist/orchestrator/index.js +297 -0
- package/dist/policy/index.d.ts +100 -0
- package/dist/policy/index.js +206 -0
- package/dist/proof/index.d.ts +220 -0
- package/dist/proof/index.js +541 -0
- package/dist/registry/index.d.ts +98 -0
- package/dist/registry/index.js +129 -0
- package/dist/rollback/index.d.ts +76 -0
- package/dist/rollback/index.js +91 -0
- package/dist/schema/capability-schema.json +52 -0
- package/dist/schema/index.d.ts +128 -0
- package/dist/schema/index.js +163 -0
- package/dist/schema/task-schema.json +69 -0
- package/dist/score/index.d.ts +174 -0
- package/dist/score/index.js +275 -0
- package/dist/service/index.d.ts +34 -0
- package/dist/service/index.js +273 -0
- package/dist/service/server.d.ts +7 -0
- package/dist/service/server.js +22 -0
- package/dist/trace/index.d.ts +217 -0
- package/dist/trace/index.js +446 -0
- package/dist/trust/index.d.ts +84 -0
- package/dist/trust/index.js +107 -0
- package/dist/trust-sync/index.d.ts +30 -0
- package/dist/trust-sync/index.js +57 -0
- package/package.json +71 -0
- package/skill/SKILL.md +363 -0
- package/skill/references/commercial.md +184 -0
- package/skill/references/executor.md +356 -0
- package/skill/references/networking.md +64 -0
- package/skill/references/onchain.md +73 -0
- package/skill/references/security.md +96 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module: Collaboration Anchor
|
|
3
|
+
*
|
|
4
|
+
* Enhanced on-chain anchoring for multi-agent collaboration scenarios.
|
|
5
|
+
* Records the full lifecycle of agent collaboration on-chain:
|
|
6
|
+
*
|
|
7
|
+
* 1. Handshake Anchor — proves two agents established a verified session
|
|
8
|
+
* 2. Task Delegation Anchor — proves a task was delegated with consent
|
|
9
|
+
* 3. Execution Proof Anchor — proves task execution result (existing ProofBundle)
|
|
10
|
+
* 4. Trust Score Anchor — proves trust score at a point in time
|
|
11
|
+
* 5. Dispute Evidence Anchor — immutable evidence for dispute resolution
|
|
12
|
+
*
|
|
13
|
+
* Each anchor is a SHA-256 hash of the relevant data, stored on-chain
|
|
14
|
+
* via the existing AnchorManager infrastructure.
|
|
15
|
+
*/
|
|
16
|
+
import { createHash } from 'node:crypto';
|
|
17
|
+
// ─── Hash Helpers ────────────────────────────────────────────────
|
|
18
|
+
function sha256(input) {
|
|
19
|
+
return createHash('sha256').update(input, 'utf-8').digest('hex');
|
|
20
|
+
}
|
|
21
|
+
function sortedStringify(obj) {
|
|
22
|
+
if (obj === null || obj === undefined)
|
|
23
|
+
return JSON.stringify(obj);
|
|
24
|
+
if (typeof obj !== 'object')
|
|
25
|
+
return JSON.stringify(obj);
|
|
26
|
+
if (Array.isArray(obj))
|
|
27
|
+
return `[${obj.map(sortedStringify).join(',')}]`;
|
|
28
|
+
const record = obj;
|
|
29
|
+
const keys = Object.keys(record).sort();
|
|
30
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${sortedStringify(record[k])}`).join(',')}}`;
|
|
31
|
+
}
|
|
32
|
+
function hashObject(obj) {
|
|
33
|
+
return sha256(sortedStringify(obj));
|
|
34
|
+
}
|
|
35
|
+
// ─── Collaboration Anchor Manager ────────────────────────────────
|
|
36
|
+
/**
|
|
37
|
+
* Manages on-chain anchoring of multi-agent collaboration events.
|
|
38
|
+
*
|
|
39
|
+
* Builds on top of AnchorManager to provide typed, structured
|
|
40
|
+
* anchoring for the full collaboration lifecycle.
|
|
41
|
+
*/
|
|
42
|
+
export class CollaborationAnchor {
|
|
43
|
+
anchorManager;
|
|
44
|
+
defaultChain;
|
|
45
|
+
records = [];
|
|
46
|
+
constructor(anchorManager, defaultChain = 'mock') {
|
|
47
|
+
this.anchorManager = anchorManager;
|
|
48
|
+
this.defaultChain = defaultChain;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Anchor a handshake session establishment.
|
|
52
|
+
* Proves that two agents verified each other's identity at a specific time.
|
|
53
|
+
*/
|
|
54
|
+
async anchorHandshake(session, chain) {
|
|
55
|
+
const data = {
|
|
56
|
+
type: 'handshake',
|
|
57
|
+
sessionId: session.sessionId,
|
|
58
|
+
localDid: session.localDid,
|
|
59
|
+
remoteDid: session.remoteDid,
|
|
60
|
+
encrypted: session.encrypted,
|
|
61
|
+
createdAt: session.createdAt,
|
|
62
|
+
};
|
|
63
|
+
const hash = hashObject(data);
|
|
64
|
+
const anchor = await this.anchorManager.anchor(hash, chain ?? this.defaultChain, {
|
|
65
|
+
type: 'handshake',
|
|
66
|
+
participants: [session.localDid, session.remoteDid],
|
|
67
|
+
});
|
|
68
|
+
const record = {
|
|
69
|
+
type: 'handshake',
|
|
70
|
+
hash,
|
|
71
|
+
anchor,
|
|
72
|
+
participants: [session.localDid, session.remoteDid],
|
|
73
|
+
description: `Handshake between ${session.localDid} and ${session.remoteDid}`,
|
|
74
|
+
eventTimestamp: session.createdAt,
|
|
75
|
+
};
|
|
76
|
+
this.records.push(record);
|
|
77
|
+
return record;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Anchor a task delegation event.
|
|
81
|
+
* Proves that a task was delegated with specific consent and policy.
|
|
82
|
+
*/
|
|
83
|
+
async anchorTaskDelegation(delegation, chain) {
|
|
84
|
+
const hash = hashObject(delegation);
|
|
85
|
+
const anchor = await this.anchorManager.anchor(hash, chain ?? this.defaultChain, {
|
|
86
|
+
type: 'task_delegation',
|
|
87
|
+
taskType: delegation.taskType,
|
|
88
|
+
participants: [delegation.requestorDid, delegation.executorDid],
|
|
89
|
+
});
|
|
90
|
+
const record = {
|
|
91
|
+
type: 'task_delegation',
|
|
92
|
+
hash,
|
|
93
|
+
anchor,
|
|
94
|
+
participants: [delegation.requestorDid, delegation.executorDid],
|
|
95
|
+
description: `Task "${delegation.taskType}" delegated from ${delegation.requestorDid} to ${delegation.executorDid}`,
|
|
96
|
+
eventTimestamp: delegation.timestamp,
|
|
97
|
+
};
|
|
98
|
+
this.records.push(record);
|
|
99
|
+
return record;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Anchor an execution proof bundle.
|
|
103
|
+
* Proves the result of a task execution with full Merkle proof.
|
|
104
|
+
*/
|
|
105
|
+
async anchorExecutionProof(proof, chain) {
|
|
106
|
+
// Anchor the proof_id + trace_root combination
|
|
107
|
+
const hash = sha256(`${proof.proof_id}:${proof.trace_root}:${proof.executor}`);
|
|
108
|
+
const anchor = await this.anchorManager.anchor(hash, chain ?? this.defaultChain, {
|
|
109
|
+
type: 'execution_proof',
|
|
110
|
+
proof_id: proof.proof_id,
|
|
111
|
+
trace_root: proof.trace_root,
|
|
112
|
+
executor: proof.executor,
|
|
113
|
+
trace_length: proof.trace_length,
|
|
114
|
+
});
|
|
115
|
+
const record = {
|
|
116
|
+
type: 'execution_proof',
|
|
117
|
+
hash,
|
|
118
|
+
anchor,
|
|
119
|
+
participants: [proof.executor],
|
|
120
|
+
description: `Execution proof ${proof.proof_id} by ${proof.executor} (${proof.trace_length} events)`,
|
|
121
|
+
eventTimestamp: proof.created_at,
|
|
122
|
+
};
|
|
123
|
+
this.records.push(record);
|
|
124
|
+
return record;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Anchor a trust score snapshot.
|
|
128
|
+
* Creates an immutable record of an agent's trust score at a point in time.
|
|
129
|
+
* Links to previous snapshot for chain integrity.
|
|
130
|
+
*/
|
|
131
|
+
async anchorTrustScore(snapshot, chain) {
|
|
132
|
+
const hash = hashObject(snapshot);
|
|
133
|
+
const anchor = await this.anchorManager.anchor(hash, chain ?? this.defaultChain, {
|
|
134
|
+
type: 'trust_score',
|
|
135
|
+
agentDid: snapshot.agentDid,
|
|
136
|
+
score: snapshot.score,
|
|
137
|
+
previousHash: snapshot.previousHash,
|
|
138
|
+
});
|
|
139
|
+
const record = {
|
|
140
|
+
type: 'trust_score',
|
|
141
|
+
hash,
|
|
142
|
+
anchor,
|
|
143
|
+
participants: [snapshot.agentDid],
|
|
144
|
+
description: `Trust score ${snapshot.score} for ${snapshot.agentDid} (${snapshot.completedTasks} completed, ${snapshot.failedTasks} failed)`,
|
|
145
|
+
eventTimestamp: snapshot.timestamp,
|
|
146
|
+
};
|
|
147
|
+
this.records.push(record);
|
|
148
|
+
return record;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Anchor dispute evidence.
|
|
152
|
+
* Creates an immutable record for dispute resolution.
|
|
153
|
+
*/
|
|
154
|
+
async anchorDisputeEvidence(evidence, chain) {
|
|
155
|
+
const hash = hashObject(evidence);
|
|
156
|
+
const anchor = await this.anchorManager.anchor(hash, chain ?? this.defaultChain, {
|
|
157
|
+
type: 'dispute_evidence',
|
|
158
|
+
disputeId: evidence.disputeId,
|
|
159
|
+
});
|
|
160
|
+
const record = {
|
|
161
|
+
type: 'dispute_evidence',
|
|
162
|
+
hash,
|
|
163
|
+
anchor,
|
|
164
|
+
participants: [evidence.complainantDid, evidence.respondentDid],
|
|
165
|
+
description: `Dispute ${evidence.disputeId}: ${evidence.complainantDid} vs ${evidence.respondentDid}`,
|
|
166
|
+
eventTimestamp: evidence.timestamp,
|
|
167
|
+
};
|
|
168
|
+
this.records.push(record);
|
|
169
|
+
return record;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Anchor a key rotation event.
|
|
173
|
+
* Proves that an agent rotated their encryption keys at a specific time.
|
|
174
|
+
*/
|
|
175
|
+
async anchorKeyRotation(agentDid, rotationSeq, newPublicKeyHash, chain) {
|
|
176
|
+
const data = {
|
|
177
|
+
type: 'key_rotation',
|
|
178
|
+
agentDid,
|
|
179
|
+
rotationSeq,
|
|
180
|
+
newPublicKeyHash,
|
|
181
|
+
timestamp: new Date().toISOString(),
|
|
182
|
+
};
|
|
183
|
+
const hash = hashObject(data);
|
|
184
|
+
const anchor = await this.anchorManager.anchor(hash, chain ?? this.defaultChain, {
|
|
185
|
+
type: 'key_rotation',
|
|
186
|
+
agentDid,
|
|
187
|
+
rotationSeq,
|
|
188
|
+
});
|
|
189
|
+
const record = {
|
|
190
|
+
type: 'key_rotation',
|
|
191
|
+
hash,
|
|
192
|
+
anchor,
|
|
193
|
+
participants: [agentDid],
|
|
194
|
+
description: `Key rotation #${rotationSeq} for ${agentDid}`,
|
|
195
|
+
eventTimestamp: data.timestamp,
|
|
196
|
+
};
|
|
197
|
+
this.records.push(record);
|
|
198
|
+
return record;
|
|
199
|
+
}
|
|
200
|
+
// ── Query ────────────────────────────────────────────────────
|
|
201
|
+
/**
|
|
202
|
+
* Get all collaboration anchor records.
|
|
203
|
+
*/
|
|
204
|
+
getRecords() {
|
|
205
|
+
return [...this.records];
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Get records by type.
|
|
209
|
+
*/
|
|
210
|
+
getRecordsByType(type) {
|
|
211
|
+
return this.records.filter((r) => r.type === type);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Get records involving a specific agent.
|
|
215
|
+
*/
|
|
216
|
+
getRecordsByParticipant(did) {
|
|
217
|
+
return this.records.filter((r) => r.participants.includes(did));
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Verify a collaboration anchor against the chain.
|
|
221
|
+
*/
|
|
222
|
+
async verifyAnchor(record) {
|
|
223
|
+
try {
|
|
224
|
+
const verification = await this.anchorManager.verify(record.hash, record.anchor.txHash, record.anchor.chain);
|
|
225
|
+
return {
|
|
226
|
+
valid: verification.valid,
|
|
227
|
+
detail: verification.detail ?? (verification.valid ? 'Anchor verified on-chain' : 'Verification failed'),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
return {
|
|
232
|
+
valid: false,
|
|
233
|
+
detail: `Verification error: ${err.message}`,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module: Crypto
|
|
3
|
+
*
|
|
4
|
+
* End-to-end encryption for ATEL inter-agent communication.
|
|
5
|
+
* Uses X25519 key exchange + XSalsa20-Poly1305 symmetric encryption.
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* 1. During handshake, agents exchange X25519 public keys
|
|
9
|
+
* 2. Both derive a shared secret via Diffie-Hellman
|
|
10
|
+
* 3. All subsequent messages are encrypted with the shared key
|
|
11
|
+
*
|
|
12
|
+
* Also provides key rotation utilities.
|
|
13
|
+
*/
|
|
14
|
+
/** X25519 key pair for Diffie-Hellman key exchange */
|
|
15
|
+
export interface EncryptionKeyPair {
|
|
16
|
+
publicKey: Uint8Array;
|
|
17
|
+
secretKey: Uint8Array;
|
|
18
|
+
}
|
|
19
|
+
/** An encrypted payload with nonce */
|
|
20
|
+
export interface EncryptedPayload {
|
|
21
|
+
/** Encryption version marker */
|
|
22
|
+
enc: 'atel.enc.v1';
|
|
23
|
+
/** Base64-encoded encrypted ciphertext */
|
|
24
|
+
ciphertext: string;
|
|
25
|
+
/** Base64-encoded 24-byte nonce */
|
|
26
|
+
nonce: string;
|
|
27
|
+
/** Sender's ephemeral X25519 public key (base64), for forward secrecy */
|
|
28
|
+
ephemeralPubKey?: string;
|
|
29
|
+
}
|
|
30
|
+
/** Session encryption state */
|
|
31
|
+
export interface EncryptionSession {
|
|
32
|
+
/** Remote agent DID */
|
|
33
|
+
remoteDid: string;
|
|
34
|
+
/** Shared secret derived from DH exchange */
|
|
35
|
+
sharedKey: Uint8Array;
|
|
36
|
+
/** Our X25519 key pair for this session */
|
|
37
|
+
localKeyPair: EncryptionKeyPair;
|
|
38
|
+
/** Remote agent's X25519 public key */
|
|
39
|
+
remotePublicKey: Uint8Array;
|
|
40
|
+
/** Session creation time */
|
|
41
|
+
createdAt: number;
|
|
42
|
+
/** Key rotation counter */
|
|
43
|
+
rotationCount: number;
|
|
44
|
+
}
|
|
45
|
+
/** Key rotation event */
|
|
46
|
+
export interface KeyRotationEvent {
|
|
47
|
+
/** New X25519 public key (base64) */
|
|
48
|
+
newPublicKey: string;
|
|
49
|
+
/** Signature over the new key using Ed25519 identity key */
|
|
50
|
+
signature: string;
|
|
51
|
+
/** Rotation sequence number */
|
|
52
|
+
rotationSeq: number;
|
|
53
|
+
/** Timestamp */
|
|
54
|
+
timestamp: string;
|
|
55
|
+
}
|
|
56
|
+
export declare class CryptoError extends Error {
|
|
57
|
+
constructor(message: string);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Generate an X25519 key pair for Diffie-Hellman key exchange.
|
|
61
|
+
*/
|
|
62
|
+
export declare function generateEncryptionKeyPair(): EncryptionKeyPair;
|
|
63
|
+
/**
|
|
64
|
+
* Derive a shared secret from local secret key and remote public key.
|
|
65
|
+
* Uses X25519 Diffie-Hellman + SHA-256 key derivation.
|
|
66
|
+
*
|
|
67
|
+
* @param localSecretKey - Our X25519 secret key (32 bytes).
|
|
68
|
+
* @param remotePublicKey - Their X25519 public key (32 bytes).
|
|
69
|
+
* @returns 32-byte shared secret.
|
|
70
|
+
*/
|
|
71
|
+
export declare function deriveSharedKey(localSecretKey: Uint8Array, remotePublicKey: Uint8Array): Uint8Array;
|
|
72
|
+
/**
|
|
73
|
+
* Encrypt a plaintext message using a shared key.
|
|
74
|
+
* Uses XSalsa20-Poly1305 (NaCl secretbox).
|
|
75
|
+
*
|
|
76
|
+
* @param plaintext - The message to encrypt (UTF-8 string).
|
|
77
|
+
* @param sharedKey - 32-byte shared secret.
|
|
78
|
+
* @returns EncryptedPayload with ciphertext and nonce.
|
|
79
|
+
*/
|
|
80
|
+
export declare function encrypt(plaintext: string, sharedKey: Uint8Array): EncryptedPayload;
|
|
81
|
+
/**
|
|
82
|
+
* Decrypt an encrypted payload using a shared key.
|
|
83
|
+
*
|
|
84
|
+
* @param payload - The encrypted payload.
|
|
85
|
+
* @param sharedKey - 32-byte shared secret.
|
|
86
|
+
* @returns The decrypted plaintext string.
|
|
87
|
+
* @throws CryptoError if decryption fails (wrong key, tampered data).
|
|
88
|
+
*/
|
|
89
|
+
export declare function decrypt(payload: EncryptedPayload, sharedKey: Uint8Array): string;
|
|
90
|
+
/**
|
|
91
|
+
* Manages encryption sessions with remote agents.
|
|
92
|
+
* Handles key exchange, encryption/decryption, and key rotation.
|
|
93
|
+
*/
|
|
94
|
+
export declare class EncryptionManager {
|
|
95
|
+
private sessions;
|
|
96
|
+
/**
|
|
97
|
+
* Create an encryption session with a remote agent.
|
|
98
|
+
*
|
|
99
|
+
* @param remoteDid - The remote agent's DID.
|
|
100
|
+
* @param remoteEncPubKey - The remote agent's X25519 public key.
|
|
101
|
+
* @returns The local X25519 public key to send to the remote agent.
|
|
102
|
+
*/
|
|
103
|
+
createSession(remoteDid: string, remoteEncPubKey: Uint8Array): Uint8Array;
|
|
104
|
+
/**
|
|
105
|
+
* Create an encryption session with pre-computed keys.
|
|
106
|
+
* Used by HandshakeManager when keys are already exchanged.
|
|
107
|
+
*
|
|
108
|
+
* @param remoteDid - The remote agent's DID.
|
|
109
|
+
* @param localKeyPair - Our X25519 key pair.
|
|
110
|
+
* @param remotePublicKey - Their X25519 public key.
|
|
111
|
+
* @param sharedKey - Pre-derived shared secret.
|
|
112
|
+
*/
|
|
113
|
+
createSessionWithKeys(remoteDid: string, localKeyPair: EncryptionKeyPair, remotePublicKey: Uint8Array, sharedKey: Uint8Array): void;
|
|
114
|
+
/**
|
|
115
|
+
* Encrypt a message for a remote agent.
|
|
116
|
+
*
|
|
117
|
+
* @param remoteDid - The remote agent's DID.
|
|
118
|
+
* @param plaintext - The message to encrypt.
|
|
119
|
+
* @returns The encrypted payload.
|
|
120
|
+
* @throws CryptoError if no session exists.
|
|
121
|
+
*/
|
|
122
|
+
encryptFor(remoteDid: string, plaintext: string): EncryptedPayload;
|
|
123
|
+
/**
|
|
124
|
+
* Decrypt a message from a remote agent.
|
|
125
|
+
*
|
|
126
|
+
* @param remoteDid - The remote agent's DID.
|
|
127
|
+
* @param payload - The encrypted payload.
|
|
128
|
+
* @returns The decrypted plaintext.
|
|
129
|
+
* @throws CryptoError if no session exists or decryption fails.
|
|
130
|
+
*/
|
|
131
|
+
decryptFrom(remoteDid: string, payload: EncryptedPayload): string;
|
|
132
|
+
/**
|
|
133
|
+
* Rotate the encryption key for a session.
|
|
134
|
+
* Generates a new key pair and re-derives the shared secret.
|
|
135
|
+
*
|
|
136
|
+
* @param remoteDid - The remote agent's DID.
|
|
137
|
+
* @param newRemotePublicKey - The remote agent's new X25519 public key.
|
|
138
|
+
* @returns The new local X25519 public key.
|
|
139
|
+
*/
|
|
140
|
+
rotateKey(remoteDid: string, newRemotePublicKey: Uint8Array): Uint8Array;
|
|
141
|
+
/**
|
|
142
|
+
* Check if an encryption session exists.
|
|
143
|
+
*/
|
|
144
|
+
hasSession(remoteDid: string): boolean;
|
|
145
|
+
/**
|
|
146
|
+
* Get session info (without exposing the shared key).
|
|
147
|
+
*/
|
|
148
|
+
getSessionInfo(remoteDid: string): {
|
|
149
|
+
remoteDid: string;
|
|
150
|
+
createdAt: number;
|
|
151
|
+
rotationCount: number;
|
|
152
|
+
} | undefined;
|
|
153
|
+
/**
|
|
154
|
+
* Destroy a session and zero out all key material.
|
|
155
|
+
*/
|
|
156
|
+
destroySession(remoteDid: string): void;
|
|
157
|
+
/**
|
|
158
|
+
* Destroy all sessions.
|
|
159
|
+
*/
|
|
160
|
+
destroyAll(): void;
|
|
161
|
+
private getSessionOrThrow;
|
|
162
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module: Crypto
|
|
3
|
+
*
|
|
4
|
+
* End-to-end encryption for ATEL inter-agent communication.
|
|
5
|
+
* Uses X25519 key exchange + XSalsa20-Poly1305 symmetric encryption.
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* 1. During handshake, agents exchange X25519 public keys
|
|
9
|
+
* 2. Both derive a shared secret via Diffie-Hellman
|
|
10
|
+
* 3. All subsequent messages are encrypted with the shared key
|
|
11
|
+
*
|
|
12
|
+
* Also provides key rotation utilities.
|
|
13
|
+
*/
|
|
14
|
+
import nacl from 'tweetnacl';
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
16
|
+
// ─── Custom Errors ───────────────────────────────────────────────
|
|
17
|
+
export class CryptoError extends Error {
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'CryptoError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// ─── Key Generation ──────────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Generate an X25519 key pair for Diffie-Hellman key exchange.
|
|
26
|
+
*/
|
|
27
|
+
export function generateEncryptionKeyPair() {
|
|
28
|
+
const kp = nacl.box.keyPair();
|
|
29
|
+
return { publicKey: kp.publicKey, secretKey: kp.secretKey };
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Derive a shared secret from local secret key and remote public key.
|
|
33
|
+
* Uses X25519 Diffie-Hellman + SHA-256 key derivation.
|
|
34
|
+
*
|
|
35
|
+
* @param localSecretKey - Our X25519 secret key (32 bytes).
|
|
36
|
+
* @param remotePublicKey - Their X25519 public key (32 bytes).
|
|
37
|
+
* @returns 32-byte shared secret.
|
|
38
|
+
*/
|
|
39
|
+
export function deriveSharedKey(localSecretKey, remotePublicKey) {
|
|
40
|
+
const rawShared = nacl.box.before(remotePublicKey, localSecretKey);
|
|
41
|
+
// KDF: SHA-256 over the raw shared secret + context string
|
|
42
|
+
const kdf = createHash('sha256');
|
|
43
|
+
kdf.update(Buffer.from('atel-session-key-v1'));
|
|
44
|
+
kdf.update(Buffer.from(rawShared));
|
|
45
|
+
return new Uint8Array(kdf.digest());
|
|
46
|
+
}
|
|
47
|
+
// ─── Encryption / Decryption ─────────────────────────────────────
|
|
48
|
+
/**
|
|
49
|
+
* Encrypt a plaintext message using a shared key.
|
|
50
|
+
* Uses XSalsa20-Poly1305 (NaCl secretbox).
|
|
51
|
+
*
|
|
52
|
+
* @param plaintext - The message to encrypt (UTF-8 string).
|
|
53
|
+
* @param sharedKey - 32-byte shared secret.
|
|
54
|
+
* @returns EncryptedPayload with ciphertext and nonce.
|
|
55
|
+
*/
|
|
56
|
+
export function encrypt(plaintext, sharedKey) {
|
|
57
|
+
if (sharedKey.length !== 32) {
|
|
58
|
+
throw new CryptoError(`Invalid shared key length: expected 32, got ${sharedKey.length}`);
|
|
59
|
+
}
|
|
60
|
+
const nonce = nacl.randomBytes(24);
|
|
61
|
+
const messageBytes = new TextEncoder().encode(plaintext);
|
|
62
|
+
const ciphertext = nacl.secretbox(messageBytes, nonce, sharedKey);
|
|
63
|
+
if (!ciphertext) {
|
|
64
|
+
throw new CryptoError('Encryption failed');
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
enc: 'atel.enc.v1',
|
|
68
|
+
ciphertext: Buffer.from(ciphertext).toString('base64'),
|
|
69
|
+
nonce: Buffer.from(nonce).toString('base64'),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Decrypt an encrypted payload using a shared key.
|
|
74
|
+
*
|
|
75
|
+
* @param payload - The encrypted payload.
|
|
76
|
+
* @param sharedKey - 32-byte shared secret.
|
|
77
|
+
* @returns The decrypted plaintext string.
|
|
78
|
+
* @throws CryptoError if decryption fails (wrong key, tampered data).
|
|
79
|
+
*/
|
|
80
|
+
export function decrypt(payload, sharedKey) {
|
|
81
|
+
if (sharedKey.length !== 32) {
|
|
82
|
+
throw new CryptoError(`Invalid shared key length: expected 32, got ${sharedKey.length}`);
|
|
83
|
+
}
|
|
84
|
+
if (payload.enc !== 'atel.enc.v1') {
|
|
85
|
+
throw new CryptoError(`Unsupported encryption version: ${payload.enc}`);
|
|
86
|
+
}
|
|
87
|
+
const ciphertext = Uint8Array.from(Buffer.from(payload.ciphertext, 'base64'));
|
|
88
|
+
const nonce = Uint8Array.from(Buffer.from(payload.nonce, 'base64'));
|
|
89
|
+
const plaintext = nacl.secretbox.open(ciphertext, nonce, sharedKey);
|
|
90
|
+
if (!plaintext) {
|
|
91
|
+
throw new CryptoError('Decryption failed: invalid key or tampered ciphertext');
|
|
92
|
+
}
|
|
93
|
+
return new TextDecoder().decode(plaintext);
|
|
94
|
+
}
|
|
95
|
+
// ─── Encryption Session Manager ──────────────────────────────────
|
|
96
|
+
/**
|
|
97
|
+
* Manages encryption sessions with remote agents.
|
|
98
|
+
* Handles key exchange, encryption/decryption, and key rotation.
|
|
99
|
+
*/
|
|
100
|
+
export class EncryptionManager {
|
|
101
|
+
sessions = new Map();
|
|
102
|
+
/**
|
|
103
|
+
* Create an encryption session with a remote agent.
|
|
104
|
+
*
|
|
105
|
+
* @param remoteDid - The remote agent's DID.
|
|
106
|
+
* @param remoteEncPubKey - The remote agent's X25519 public key.
|
|
107
|
+
* @returns The local X25519 public key to send to the remote agent.
|
|
108
|
+
*/
|
|
109
|
+
createSession(remoteDid, remoteEncPubKey) {
|
|
110
|
+
const localKeyPair = generateEncryptionKeyPair();
|
|
111
|
+
const sharedKey = deriveSharedKey(localKeyPair.secretKey, remoteEncPubKey);
|
|
112
|
+
this.sessions.set(remoteDid, {
|
|
113
|
+
remoteDid,
|
|
114
|
+
sharedKey,
|
|
115
|
+
localKeyPair,
|
|
116
|
+
remotePublicKey: remoteEncPubKey,
|
|
117
|
+
createdAt: Date.now(),
|
|
118
|
+
rotationCount: 0,
|
|
119
|
+
});
|
|
120
|
+
return localKeyPair.publicKey;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Create an encryption session with pre-computed keys.
|
|
124
|
+
* Used by HandshakeManager when keys are already exchanged.
|
|
125
|
+
*
|
|
126
|
+
* @param remoteDid - The remote agent's DID.
|
|
127
|
+
* @param localKeyPair - Our X25519 key pair.
|
|
128
|
+
* @param remotePublicKey - Their X25519 public key.
|
|
129
|
+
* @param sharedKey - Pre-derived shared secret.
|
|
130
|
+
*/
|
|
131
|
+
createSessionWithKeys(remoteDid, localKeyPair, remotePublicKey, sharedKey) {
|
|
132
|
+
this.sessions.set(remoteDid, {
|
|
133
|
+
remoteDid,
|
|
134
|
+
sharedKey,
|
|
135
|
+
localKeyPair,
|
|
136
|
+
remotePublicKey,
|
|
137
|
+
createdAt: Date.now(),
|
|
138
|
+
rotationCount: 0,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Encrypt a message for a remote agent.
|
|
143
|
+
*
|
|
144
|
+
* @param remoteDid - The remote agent's DID.
|
|
145
|
+
* @param plaintext - The message to encrypt.
|
|
146
|
+
* @returns The encrypted payload.
|
|
147
|
+
* @throws CryptoError if no session exists.
|
|
148
|
+
*/
|
|
149
|
+
encryptFor(remoteDid, plaintext) {
|
|
150
|
+
const session = this.getSessionOrThrow(remoteDid);
|
|
151
|
+
return encrypt(plaintext, session.sharedKey);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Decrypt a message from a remote agent.
|
|
155
|
+
*
|
|
156
|
+
* @param remoteDid - The remote agent's DID.
|
|
157
|
+
* @param payload - The encrypted payload.
|
|
158
|
+
* @returns The decrypted plaintext.
|
|
159
|
+
* @throws CryptoError if no session exists or decryption fails.
|
|
160
|
+
*/
|
|
161
|
+
decryptFrom(remoteDid, payload) {
|
|
162
|
+
const session = this.getSessionOrThrow(remoteDid);
|
|
163
|
+
return decrypt(payload, session.sharedKey);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Rotate the encryption key for a session.
|
|
167
|
+
* Generates a new key pair and re-derives the shared secret.
|
|
168
|
+
*
|
|
169
|
+
* @param remoteDid - The remote agent's DID.
|
|
170
|
+
* @param newRemotePublicKey - The remote agent's new X25519 public key.
|
|
171
|
+
* @returns The new local X25519 public key.
|
|
172
|
+
*/
|
|
173
|
+
rotateKey(remoteDid, newRemotePublicKey) {
|
|
174
|
+
const session = this.getSessionOrThrow(remoteDid);
|
|
175
|
+
const newLocalKeyPair = generateEncryptionKeyPair();
|
|
176
|
+
const newSharedKey = deriveSharedKey(newLocalKeyPair.secretKey, newRemotePublicKey);
|
|
177
|
+
// Zero out old keys
|
|
178
|
+
session.sharedKey.fill(0);
|
|
179
|
+
session.localKeyPair.secretKey.fill(0);
|
|
180
|
+
session.sharedKey = newSharedKey;
|
|
181
|
+
session.localKeyPair = newLocalKeyPair;
|
|
182
|
+
session.remotePublicKey = newRemotePublicKey;
|
|
183
|
+
session.rotationCount++;
|
|
184
|
+
return newLocalKeyPair.publicKey;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Check if an encryption session exists.
|
|
188
|
+
*/
|
|
189
|
+
hasSession(remoteDid) {
|
|
190
|
+
return this.sessions.has(remoteDid);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Get session info (without exposing the shared key).
|
|
194
|
+
*/
|
|
195
|
+
getSessionInfo(remoteDid) {
|
|
196
|
+
const session = this.sessions.get(remoteDid);
|
|
197
|
+
if (!session)
|
|
198
|
+
return undefined;
|
|
199
|
+
return {
|
|
200
|
+
remoteDid: session.remoteDid,
|
|
201
|
+
createdAt: session.createdAt,
|
|
202
|
+
rotationCount: session.rotationCount,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Destroy a session and zero out all key material.
|
|
207
|
+
*/
|
|
208
|
+
destroySession(remoteDid) {
|
|
209
|
+
const session = this.sessions.get(remoteDid);
|
|
210
|
+
if (session) {
|
|
211
|
+
session.sharedKey.fill(0);
|
|
212
|
+
session.localKeyPair.secretKey.fill(0);
|
|
213
|
+
this.sessions.delete(remoteDid);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Destroy all sessions.
|
|
218
|
+
*/
|
|
219
|
+
destroyAll() {
|
|
220
|
+
for (const [did] of this.sessions) {
|
|
221
|
+
this.destroySession(did);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
getSessionOrThrow(remoteDid) {
|
|
225
|
+
const session = this.sessions.get(remoteDid);
|
|
226
|
+
if (!session) {
|
|
227
|
+
throw new CryptoError(`No encryption session with ${remoteDid}`);
|
|
228
|
+
}
|
|
229
|
+
return session;
|
|
230
|
+
}
|
|
231
|
+
}
|