Smart Contract

AgentEncryption

A.91d0a5b7c9832a8b.AgentEncryption

Valid From

143,525,862

Deployed

1d ago
Feb 27, 2026, 12:08:29 AM UTC

Dependents

0 imports
1// AgentEncryption.cdc
2// End-to-end encryption for on-chain agent messages.
3//
4// THE PROBLEM:
5// Cadence's /storage/ is private at rest — no other account can read it.
6// But transaction arguments are publicly visible on the blockchain.
7// When send_message.cdc includes the message text as an argument,
8// that text is visible on every block explorer forever.
9// The "private storage" only protects against script-based queries,
10// not against reading the transaction payload that PUT the data there.
11//
12// THE SOLUTION:
13// Encrypt ALL content before it enters a transaction.
14// The transaction carries ciphertext + plaintext hash.
15// Storage holds ciphertext. The relay decrypts locally.
16// Block explorers see encrypted gibberish, not your conversations.
17//
18// HOW IT WORKS:
19//
20//  User types "What is FLOW?"
21//       │
22//       ▼
23//  Relay encrypts: E("What is FLOW?", account_key) → "x7Fk9mQ2..."
24//  Relay computes: SHA256("What is FLOW?") → "a3b8c1..."
25//       │
26//       ▼
27//  Transaction: send_message("x7Fk9mQ2...", "a3b8c1...")
28//       │                      ↑ ciphertext      ↑ hash of plaintext
29//       ▼
30//  On-chain: Session stores encrypted content + hash
31//       │
32//  Block explorer sees: "x7Fk9mQ2..." (meaningless)
33//       │
34//       ▼
35//  LLM responds → Relay encrypts response → posts encrypted on-chain
36//       │
37//       ▼
38//  Relay reads back, decrypts locally, shows to user
39//
40// KEY MANAGEMENT:
41// - Each account has an EncryptionConfig resource in private storage
42// - The config stores a public key (on-chain) and key metadata
43// - The actual private/symmetric key NEVER touches the chain
44// - The relay holds the decryption key locally in .env or keyfile
45// - Key rotation is supported: old keys kept for decrypting history
46//
47// ENCRYPTION SCHEME:
48// - Symmetric: XChaCha20-Poly1305 (same as ZeroClaw, industry standard)
49// - The symmetric key is derived from a passphrase or generated randomly
50// - Stored locally in ~/.flowclaw/encryption.key (never on-chain)
51// - The on-chain EncryptionConfig stores only the public verification key
52//   and a key fingerprint so the relay knows which key to use
53//
54// WHAT THIS ACHIEVES:
55// - Transaction payloads: encrypted (public but unreadable)
56// - Storage contents: encrypted (private AND encrypted — defense in depth)
57// - Content hashes: of plaintext (verifiable without decrypting)
58// - Block explorers: see ciphertext only
59// - The relay: only party that can decrypt (holds the key locally)
60
61access(all) contract AgentEncryption {
62
63    // -----------------------------------------------------------------------
64    // Events
65    // -----------------------------------------------------------------------
66    access(all) event EncryptionConfigured(owner: Address, algorithm: String, keyFingerprint: String)
67    access(all) event KeyRotated(owner: Address, oldFingerprint: String, newFingerprint: String)
68    access(all) event EncryptedMessageStored(sessionId: UInt64, contentHash: String, ciphertextLength: UInt64)
69
70    // -----------------------------------------------------------------------
71    // Paths
72    // -----------------------------------------------------------------------
73    access(all) let EncryptionConfigStoragePath: StoragePath
74
75    // -----------------------------------------------------------------------
76    // Entitlements
77    // -----------------------------------------------------------------------
78    access(all) entitlement ManageKeys
79    access(all) entitlement Encrypt
80    access(all) entitlement Verify
81
82    // -----------------------------------------------------------------------
83    // Supported encryption algorithms
84    // -----------------------------------------------------------------------
85    access(all) enum Algorithm: UInt8 {
86        access(all) case xchacha20poly1305    // Symmetric AEAD (recommended)
87        access(all) case aes256gcm            // Alternative symmetric AEAD
88    }
89
90    // -----------------------------------------------------------------------
91    // KeyInfo — metadata about an encryption key (key itself is NEVER on-chain)
92    // -----------------------------------------------------------------------
93    access(all) struct KeyInfo {
94        access(all) let fingerprint: String     // SHA-256 of the key (for identification)
95        access(all) let algorithm: Algorithm
96        access(all) let createdAt: UFix64
97        access(all) let isActive: Bool
98        access(all) let label: String
99
100        init(
101            fingerprint: String,
102            algorithm: Algorithm,
103            label: String,
104            isActive: Bool
105        ) {
106            self.fingerprint = fingerprint
107            self.algorithm = algorithm
108            self.createdAt = getCurrentBlock().timestamp
109            self.isActive = isActive
110            self.label = label
111        }
112    }
113
114    // -----------------------------------------------------------------------
115    // EncryptedPayload — what gets stored on-chain instead of plaintext
116    // -----------------------------------------------------------------------
117    access(all) struct EncryptedPayload {
118        access(all) let ciphertext: String         // Base64-encoded encrypted content
119        access(all) let nonce: String              // Base64-encoded nonce/IV
120        access(all) let plaintextHash: String      // SHA-256 of original plaintext
121        access(all) let keyFingerprint: String     // Which key was used (for rotation)
122        access(all) let algorithm: UInt8           // Which algorithm
123        access(all) let plaintextLength: UInt64    // Length of original text (for estimation)
124
125        init(
126            ciphertext: String,
127            nonce: String,
128            plaintextHash: String,
129            keyFingerprint: String,
130            algorithm: UInt8,
131            plaintextLength: UInt64
132        ) {
133            pre {
134                ciphertext.length > 0: "Ciphertext cannot be empty"
135                nonce.length > 0: "Nonce cannot be empty"
136                plaintextHash.length > 0: "Plaintext hash cannot be empty"
137                keyFingerprint.length > 0: "Key fingerprint cannot be empty"
138            }
139            self.ciphertext = ciphertext
140            self.nonce = nonce
141            self.plaintextHash = plaintextHash
142            self.keyFingerprint = keyFingerprint
143            self.algorithm = algorithm
144            self.plaintextLength = plaintextLength
145        }
146    }
147
148    // -----------------------------------------------------------------------
149    // EncryptionConfig — per-account encryption settings
150    // Stored in /storage/ (private), but only contains key metadata,
151    // NEVER the actual encryption key.
152    // -----------------------------------------------------------------------
153    access(all) resource EncryptionConfig {
154        access(self) var activeKey: KeyInfo?
155        access(self) var keyHistory: [KeyInfo]   // For decrypting old messages after rotation
156        access(all) var isEnabled: Bool
157        access(all) var totalEncrypted: UInt64
158        access(all) var totalDecryptVerified: UInt64
159
160        init() {
161            self.activeKey = nil
162            self.keyHistory = []
163            self.isEnabled = false
164            self.totalEncrypted = 0
165            self.totalDecryptVerified = 0
166        }
167
168        // --- ManageKeys: configure encryption ---
169
170        access(ManageKeys) fun configureKey(
171            fingerprint: String,
172            algorithm: Algorithm,
173            label: String
174        ) {
175            post {
176                self.activeKey != nil: "Active key must be set after configuration"
177                self.isEnabled: "Encryption must be enabled after configuration"
178            }
179
180            // If there's an existing active key, move it to history
181            if let oldKey = self.activeKey {
182                let deactivated = KeyInfo(
183                    fingerprint: oldKey.fingerprint,
184                    algorithm: oldKey.algorithm,
185                    label: oldKey.label,
186                    isActive: false
187                )
188                self.keyHistory.append(deactivated)
189
190                emit KeyRotated(
191                    owner: self.owner!.address,
192                    oldFingerprint: oldKey.fingerprint,
193                    newFingerprint: fingerprint
194                )
195            }
196
197            self.activeKey = KeyInfo(
198                fingerprint: fingerprint,
199                algorithm: algorithm,
200                label: label,
201                isActive: true
202            )
203            self.isEnabled = true
204
205            let algoStr = algorithm == Algorithm.xchacha20poly1305
206                ? "xchacha20-poly1305" : "aes-256-gcm"
207
208            emit EncryptionConfigured(
209                owner: self.owner!.address,
210                algorithm: algoStr,
211                keyFingerprint: fingerprint
212            )
213        }
214
215        access(ManageKeys) fun disable() {
216            self.isEnabled = false
217        }
218
219        access(ManageKeys) fun enable() {
220            pre {
221                self.activeKey != nil: "Configure a key first"
222            }
223            self.isEnabled = true
224        }
225
226        // --- Encrypt: record encrypted operations ---
227
228        access(Encrypt) fun recordEncryption() {
229            self.totalEncrypted = self.totalEncrypted + 1
230        }
231
232        // --- Verify: validate a payload's integrity ---
233
234        access(Verify) fun verifyPayload(payload: EncryptedPayload): Bool {
235            // Check that the key fingerprint matches a known key
236            if let active = self.activeKey {
237                if active.fingerprint == payload.keyFingerprint {
238                    return true
239                }
240            }
241            for key in self.keyHistory {
242                if key.fingerprint == payload.keyFingerprint {
243                    return true
244                }
245            }
246            return false
247        }
248
249        access(Verify) fun recordVerification() {
250            self.totalDecryptVerified = self.totalDecryptVerified + 1
251        }
252
253        // --- Read ---
254
255        access(all) fun getActiveKeyInfo(): KeyInfo? {
256            return self.activeKey
257        }
258
259        access(all) fun getKeyHistory(): [KeyInfo] {
260            return self.keyHistory
261        }
262
263        access(all) fun getKeyForFingerprint(fingerprint: String): KeyInfo? {
264            if let active = self.activeKey {
265                if active.fingerprint == fingerprint {
266                    return active
267                }
268            }
269            for key in self.keyHistory {
270                if key.fingerprint == fingerprint {
271                    return key
272                }
273            }
274            return nil
275        }
276    }
277
278    // -----------------------------------------------------------------------
279    // Public factory
280    // -----------------------------------------------------------------------
281    access(all) fun createEncryptionConfig(): @EncryptionConfig {
282        return <- create EncryptionConfig()
283    }
284
285    // -----------------------------------------------------------------------
286    // Init
287    // -----------------------------------------------------------------------
288    init() {
289        self.EncryptionConfigStoragePath = /storage/FlowClawEncryptionConfig
290    }
291}
292