Large or sensitive off-chain data (e.g., document attachments, audit evidence, binary assets) is stored on a private IPFS swarm with mandatory AES-256-GCM encryption. Only nodes sharing the same encryption key can decrypt. The CID returned by IPFS is stored on-chain as a reference; the plaintext never leaves the encrypted boundary.
Key property: Even if IPFS storage is compromised, data is unreadable without the AES-256-GCM key.
Flow Diagram β Upload (Encrypt β Store β Pin)
sequenceDiagram
autonumber
participant Caller as π₯οΈ API / SubChain
participant IC as ποΈ IPFSClient
participant AES as π AESEncryption
participant IPFS as π IPFS Daemon
Caller->>IC: upload_json(data, encrypt=True, metadata)
IC->>IC: json.dumps(data) β raw_bytes
IC->>AES: encrypt(raw_bytes, aad=json(metadata))
Note right of AES: AES-256-GCM<br/>nonce = random 96-bit (secrets.token_bytes(12))<br/>ciphertext = AESGCM.encrypt(nonce, plaintext, aad)<br/>output = nonce || ciphertext
AES-->>IC: ciphertext, nonce
IC->>IPFS: add_bytes(ciphertext)
IPFS-->>IC: CID (Content Identifier)
alt auto_pin=True (default)
IC->>IPFS: pin.add(CID)
Note right of IPFS: Prevents garbage collection
end
IC-->>Caller: { cid, size, encrypted: True, nonce: hex(nonce) }
Note over Caller: Store CID + nonce on-chain for later retrieval
Flow Diagram β Download (Retrieve β Decrypt)
sequenceDiagram
autonumber
participant Caller as π₯οΈ API / SubChain
participant IC as ποΈ IPFSClient
participant AES as π AESEncryption
participant IPFS as π IPFS Daemon
Caller->>IC: download_json(cid, encrypted=True, nonce=hex, metadata)
IC->>IPFS: cat(cid) β ciphertext bytes
IPFS-->>IC: ciphertext
IC->>AES: decrypt(ciphertext, nonce_bytes, aad=json(metadata))
Note right of AES: Verify GCM authentication tag first<br/>If tag invalid β raise DecryptionError
AES-->>IC: plaintext bytes
IC->>IC: json.loads(plaintext) β dict
IC-->>Caller: Decrypted data β
Error Handling β IPFS Unavailable
flowchart LR
CALL["upload_json(data)"]
CONN["Connect to IPFS daemon\n(HRC_IPFS_HOST)"]
FAIL["β Connection refused\nor timeout"]
RETRY["Retry with backoff\n(max 3 attempts)"]
ERR["Raise IPFSConnectionError\nLog + Alert via Risk Alerts"]
OK["β CID returned"]
CALL --> CONN
CONN -->|Success| OK
CONN -->|Fail| FAIL --> RETRY
RETRY -->|Max retries| ERR
RETRY -->|Reconnected| OK
Step-by-Step Breakdown
Step
Description
1. Serialize
json.dumps(data) β raw bytes
2. Encrypt
AES-256-GCM with random 96-bit nonce. AAD (additional authenticated data) = JSON-serialized metadata
3. Upload
Raw ciphertext bytes sent to IPFS daemon via Kubo RPC API (httpx)
4. Pin
pin.add(CID) prevents garbage collection by IPFS GC daemon
5. Return
Caller receives { cid, nonce } β both must be stored on-chain for retrieval
6. Retrieve
cat(cid) fetches ciphertext; decrypt() verifies GCM authentication tag before decrypting
Security Properties
Property
Mechanism
Confidentiality
AES-256-GCM encryption
Integrity
GCM authentication tag (authenticated encryption)
Replay protection
Unique random 96-bit nonce per upload
Key management
HRC_IPFS_ENCRYPTION_KEY env var; auto-generated if missing
Access control
Policy Engine (Policy Enforcement) gates upload/download API calls