@ajna-inc/vaults
v0.6.3
Published
Post-quantum encrypted vaults with DIDComm protocol for Credo. Client-side encryption using ML-KEM-768, AES-256-GCM, and Shamir secret sharing with P2P sharing via DIDComm messages.
Downloads
549
Readme
Post-Quantum Encrypted Vaults
Post-quantum encrypted vaults with DIDComm protocol for Credo. Client-side encryption using ML-KEM-768, AES-256-GCM, and Shamir secret sharing with P2P sharing via DIDComm messages.
Overview
The Vaults module provides secure, client-side encrypted storage with peer-to-peer sharing capabilities. All cryptographic operations happen locally on the agent - no server ever sees plaintext data. It supports multiple encryption suites including post-quantum ML-KEM-768, flexible access policies (passphrase, any-of, all-of, threshold), and optional external storage (S3) for large files.
Table of Contents
- Features
- How It Works
- Encryption Suites
- Access Policies
- Protocol Flows
- Usage Examples
- Installation
- Message Types
- Event System
- Security Considerations
Features
Encryption Suites
- S3 Suite: Passphrase-based encryption (Argon2id KDF + AES-256-GCM)
- P1 Suite: Post-quantum encryption (ML-KEM-768 + AES-256-GCM)
- H1 Suite: Hybrid mode (planned)
Access Policies
- Passphrase: Single-user encryption with passphrase-derived key
- Any-of: Multiple recipients, any one can decrypt independently
- All-of: Multiple recipients, all must cooperate to decrypt
- Threshold: Shamir secret sharing with configurable t-of-n threshold
Vault Operations
- Create, open, update, delete encrypted vaults
- List vaults and get metadata without decrypting
- KEM keypair generation and management
- Document signing vault workflows
DIDComm Protocol
- 15 message types for P2P vault sharing
- Request, grant, and deny access to vaults
- Threshold share request and distribution
- External storage operator support
- Problem reporting for error handling
Storage
- Inline storage in agent wallet
- External S3 storage for large files
- Configurable inline/external threshold
- Storage operator mode
How It Works
Architecture
Application Code
|
VaultsApi
create / open / update / delete / list
KEM key mgmt / signing vaults
|
+----------------+----------------+
| | |
VaultService KEM Service SigningVaultSvc
| | |
+--------+-------+-------+--------+
| |
EncryptionSvc HPKEService
| |
WASM Crypto (30+ functions)
|
+----------+----------+
| | |
AEAD KEM Shamir
AES-256 ML-KEM-768 Secret
GCM Sharing
| | |
VaultRepository StorageService
| |
Agent Wallet S3 / ExternalEncryption Flow
- Passphrase vault: Passphrase -> Argon2id KDF -> 256-bit CEK -> AES-256-GCM encrypt
- Post-quantum vault: ML-KEM-768 encapsulate -> shared secret -> HKDF -> CEK -> AES-256-GCM encrypt
- Threshold vault: CEK -> Shamir split into n shares -> wrap each share with recipient's KEM public key
Key Commitment
Every vault includes a key commitment (kcmp) in the header to verify the correct key was used during decryption, preventing key confusion attacks.
Encryption Suites
S3 Suite (Passphrase)
KDF: Argon2id (configurable memory and iterations) AEAD: AES-256-GCM Use Case: Personal vaults protected by a passphrase
- Passphrase is stretched via Argon2id with random salt
- Derived key encrypts data with AES-256-GCM
- Key commitment ensures correct passphrase verification
P1 Suite (Post-Quantum)
KEM: ML-KEM-768 (NIST PQC standard) AEAD: AES-256-GCM Use Case: Sharing vaults between agents with quantum-resistant security
- Recipient's ML-KEM public key encapsulates a shared secret
- Shared secret is expanded via HKDF to derive CEK
- CEK encrypts data with AES-256-GCM
Access Policies
Passphrase Policy
Single user, passphrase-derived encryption key.
Any-of Policy
Multiple recipients each get an independently-encrypted copy of the CEK wrapped with their KEM public key. Any single recipient can decrypt.
All-of Policy
All participants must cooperate. The CEK is split such that every participant's contribution is required for reconstruction.
Threshold Policy
Shamir secret sharing splits the CEK into n shares with a threshold of t. Any t shares can reconstruct the CEK. Each share is wrapped with the respective participant's KEM public key.
Protocol Flows
1. Create and Open a Vault
Agent
|-- create(passphrase, data) --> Argon2id + AES-256-GCM --> VaultRecord stored
|-- open(passphrase) --> derive key --> AES-256-GCM decrypt --> plaintext2. Share Vault via DIDComm
Owner Recipient
| |
| 1. GrantAccessMessage (header + CT) |
|--------------------------------------->|
| |
| 2. VaultStoredAckMessage |
|<---------------------------------------|3. Request Access
Requester Owner
| |
| 1. RequestAccessMessage |
|--------------------------------------->|
| |
| 2. GrantAccessMessage / DenyAccess |
|<---------------------------------------|4. Threshold Reconstruction
Requester ShareHolder1 ShareHolder2
| | |
| RequestShareMessage | |
|----------------------------->| |
| RequestShareMessage |
|--------------------------------------------->|
| | |
| ProvideShareMessage | |
|<-----------------------------| |
| ProvideShareMessage |
|<---------------------------------------------|
| |
| [Reconstruct CEK from t shares] |5. Document Signing Vault
Owner Signer
| |
| createSigningVault(doc, recipientDid) |
| shareSigningVault(connectionId) |
|--------------------------------------->|
| |
| openSigningVault()|
| sign document |
| returnSignedDocument() |
|<---------------------------------------|Usage Examples
Basic Vault Operations
import { Agent } from '@credo-ts/core'
import { VaultsModule } from '@ajna-inc/vaults'
const agent = new Agent({
config: { /* ... */ },
modules: {
vaults: new VaultsModule()
}
})
await agent.initialize()
// Create a vault
const vault = await agent.modules.vaults.create({
passphrase: 'my-secure-passphrase',
data: Buffer.from('sensitive document content'),
metadata: {
description: 'Contract draft',
tags: ['legal', 'draft']
}
})
// Open (decrypt) a vault
const plaintext = await agent.modules.vaults.open(vault.vaultId, {
passphrase: 'my-secure-passphrase'
})
// List all vaults
const vaults = await agent.modules.vaults.list()
// Get vault info without decrypting
const info = await agent.modules.vaults.getInfo(vault.vaultId)
// Update vault data
await agent.modules.vaults.update(vault.vaultId, {
passphrase: 'my-secure-passphrase',
data: Buffer.from('updated content')
})
// Delete a vault
await agent.modules.vaults.delete(vault.vaultId)KEM Key Management
// Generate an ML-KEM-768 keypair
const keypair = await agent.modules.vaults.generateKemKeypair()
// Store a peer's KEM public key
await agent.modules.vaults.storePeerKemKey(connectionId, peerPublicKey)
// Retrieve peer's key for encryption
const peerKey = await agent.modules.vaults.getPeerKemKey(connectionId)Signing Vault Workflow
// Owner creates a signing vault for a specific recipient
const signingVault = await agent.modules.vaults.createSigningVault({
data: documentBytes,
recipientDid: signerDid
})
// Owner shares the vault with the signer
await agent.modules.vaults.shareSigningVault(connectionId, signingVault.vaultId)
// Signer opens the vault
const doc = await agent.modules.vaults.openSigningVault(vaultId)
// Signer signs and returns the document
await agent.modules.vaults.returnSignedDocument(connectionId, {
vaultId,
signedData: signedDocBytes
})S3 Storage Configuration
const agent = new Agent({
modules: {
vaults: new VaultsModule({
storage: {
bucket: 'my-vault-bucket',
region: 'us-east-1',
accessKeyId: 'AKIA...',
secretAccessKey: '...'
},
inlineThreshold: 1024 * 1024, // 1MB - larger vaults go to S3
operatorMode: false
})
}
})Installation
pnpm add @ajna-inc/vaultsMessage Types
| Message | Purpose |
|---------|---------|
| CreateVaultMessage | Notify of vault creation |
| UpdateVaultMessage | Update vault data |
| DeleteVaultMessage | Delete vault |
| RequestAccessMessage | Request access to a vault |
| GrantAccessMessage | Grant access with encrypted data |
| DenyAccessMessage | Deny access request |
| RequestShareMessage | Request a threshold share |
| ProvideShareMessage | Provide a threshold share |
| DenyShareMessage | Deny share request |
| StoreVaultMessage | Store vault at storage service |
| VaultStoredAckMessage | Confirm storage |
| RetrieveVaultMessage | Retrieve vault from storage |
| VaultDataMessage | Vault data transmission |
| VaultReferenceMessage | Storage reference |
| VaultProblemReportMessage | Error reporting |
Event System
Subscribe to vault lifecycle events:
import { VaultEventTypes } from '@ajna-inc/vaults'
agent.events.on(VaultEventTypes.VaultCreated, (event) => {
console.log('Vault created:', event.payload.vaultId)
})
agent.events.on(VaultEventTypes.AccessGranted, (event) => {
console.log('Access granted to vault:', event.payload.vaultId)
})
agent.events.on(VaultEventTypes.ThresholdMet, (event) => {
console.log('Threshold met, vault can be decrypted')
})Available Events
VaultCreated,VaultOpened,VaultUpdated,VaultDeletedVaultShared,VaultErrorAccessRequested,AccessGranted,AccessDeniedThresholdMet,ShareProvided,ShareRequested,ShareDeniedStorageAllocated,StorageUploaded,StorageDownloaded
Security Considerations
Cryptographic Primitives
- ML-KEM-768: NIST post-quantum standard for key encapsulation
- AES-256-GCM: Authenticated encryption with 256-bit keys
- Argon2id: Memory-hard KDF resistant to GPU/ASIC attacks
- Shamir Secret Sharing: Information-theoretically secure threshold scheme
- Key Commitment: Prevents key confusion and related attacks
Best Practices
- Use strong passphrases for passphrase-based vaults
- Configure adequate Argon2id memory (default is reasonable for most use cases)
- Store KEM private keys securely in the agent wallet
- Verify key commitments are checked on every decryption
- Use threshold policies for high-value data requiring multi-party authorization
- Consider external storage for large files to keep wallet lean
Client-Side Only
All encryption and decryption happens locally on the agent. No server, mediator, or storage operator ever has access to plaintext data or encryption keys.
Vault Header Format
{
v: 1, // Protocol version
suite: 'S3' | 'P1' | 'H1', // Encryption suite
aead: 'AES-256-GCM', // AEAD algorithm
docId: string, // Document identifier
vaultId: string, // Vault identifier
epoch: number, // Version counter
nonce: string, // base64url nonce
kcmp: string, // Key commitment
salt?: string, // Argon2id salt (S3 suite)
argon2?: { // KDF parameters (S3 suite)
memory: number,
iterations: number
},
policy?: { // Access policy
mode: 'passphrase' | 'any-of' | 'all-of' | 'threshold'
},
recipients?: RecipientWrap[], // Per-recipient wrapped keys
shares?: ThresholdShare[], // Threshold shares
metadata?: { // Optional metadata
description?: string,
tags?: string[]
}
}Error Handling
import { VaultError, BadSuiteError, DecryptKemError, DecryptAeadError, PolicyError } from '@ajna-inc/vaults'
try {
await agent.modules.vaults.open(vaultId, { passphrase: 'wrong' })
} catch (error) {
if (error instanceof DecryptAeadError) {
console.log('Wrong passphrase or corrupted data')
} else if (error instanceof DecryptKemError) {
console.log('KEM decapsulation failed - wrong key')
} else if (error instanceof PolicyError) {
console.log('Access policy violation')
} else if (error instanceof BadSuiteError) {
console.log('Unknown encryption suite')
}
}Testing
# Run vault tests
pnpm test
# Watch mode
pnpm test:watchLicense
Apache-2.0
