Smart Contract
AgentEncryption
A.91d0a5b7c9832a8b.AgentEncryption
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