This document outlines the implementation plan for advanced features to enhance the DIDComm message storage and SecretResolver system for production use.
packagecom.trustweave.credential.didcomm.crypto.secret.encryptionimportjavax.crypto.Cipherimportjavax.crypto.spec.GCMParameterSpecimportjavax.crypto.spec.SecretKeySpecimportjava.security.SecureRandomimportjava.security.spec.KeySpecimportjavax.crypto.SecretKeyFactoryimportjavax.crypto.spec.PBEKeySpecimportjava.util.Base64/**
* Encrypts/decrypts keys using AES-256-GCM.
*/classKeyEncryption(privatevalmasterKey:ByteArray// Derived from user password/master key){privatevalalgorithm="AES/GCM/NoPadding"privatevalkeyLength=256privatevalivLength=12// 96 bits for GCMprivatevaltagLength=128// 16 bytesfunencrypt(plaintext:ByteArray):EncryptedData{valiv=ByteArray(ivLength).apply{SecureRandom().nextBytes(this)}valsecretKey=SecretKeySpec(masterKey,"AES")valparameterSpec=GCMParameterSpec(tagLength,iv)valcipher=Cipher.getInstance(algorithm)cipher.init(Cipher.ENCRYPT_MODE,secretKey,parameterSpec)valciphertext=cipher.doFinal(plaintext)returnEncryptedData(iv=iv,ciphertext=ciphertext,algorithm=algorithm)}fundecrypt(encrypted:EncryptedData):ByteArray{valsecretKey=SecretKeySpec(masterKey,"AES")valparameterSpec=GCMParameterSpec(tagLength,encrypted.iv)valcipher=Cipher.getInstance(algorithm)cipher.init(Cipher.DECRYPT_MODE,secretKey,parameterSpec)returncipher.doFinal(encrypted.ciphertext)}}data classEncryptedData(valiv:ByteArray,valciphertext:ByteArray,valalgorithm:String)/**
* Derives master key from password using PBKDF2.
*/objectMasterKeyDerivation{funderiveKey(password:CharArray,salt:ByteArray,iterations:Int=100000):ByteArray{valkeySpec:KeySpec=PBEKeySpec(password,salt,iterations,256// Key length in bits)valkeyFactory=SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")valsecretKey=keyFactory.generateSecret(keySpec)returnsecretKey.encoded}}
packagecom.trustweave.credential.didcomm.crypto.secretimportcom.trustweave.credential.didcomm.crypto.secret.encryption.*importkotlinx.coroutines.Dispatchersimportkotlinx.coroutines.withContextimportkotlinx.serialization.json.*importorg.didcommx.didcomm.secret.Secretimportjava.io.Fileimportjava.io.FileInputStreamimportjava.io.FileOutputStreamimportjava.nio.file.Filesimportjava.nio.file.attribute.PosixFilePermissionimportjava.util.concurrent.locks.ReentrantReadWriteLockimportkotlin.concurrent.readimportkotlin.concurrent.write/**
* Encrypted file-based local key store for production use.
*
* Stores keys in an encrypted file with the following format:
*
* File Structure:
* - Header (JSON): version, salt, algorithm, metadata
* - Key Blocks (encrypted): Each key stored as encrypted JSON
*
* Security:
* - Keys encrypted with AES-256-GCM
* - Master key derived from password using PBKDF2
* - File permissions restricted (600 on Unix)
* - Atomic writes for consistency
*/classEncryptedFileLocalKeyStore(privatevalkeyFile:File,privatevalmasterKey:ByteArray,// Should be derived from passwordprivatevalkeyEncryption:KeyEncryption=KeyEncryption(masterKey)):LocalKeyStore{privatevallock=ReentrantReadWriteLock()privatevaljson=Json{prettyPrint=false;encodeDefaults=false}init{// Ensure file exists and has correct permissionsif(!keyFile.exists()){keyFile.createNewFile()setSecurePermissions(keyFile)}else{setSecurePermissions(keyFile)}}overridesuspendfunget(keyId:String):Secret?=withContext(Dispatchers.IO){lock.read{valkeys=loadKeys()keys[keyId]}}overridesuspendfunstore(keyId:String,secret:Secret)=withContext(Dispatchers.IO){lock.write{valkeys=loadKeys().toMutableMap()keys[keyId]=secretsaveKeys(keys)}}overridesuspendfundelete(keyId:String):Boolean=withContext(Dispatchers.IO){lock.write{valkeys=loadKeys().toMutableMap()valremoved=keys.remove(keyId)!=nullif(removed){saveKeys(keys)}removed}}overridesuspendfunlist():List<String>=withContext(Dispatchers.IO){lock.read{valkeys=loadKeys()keys.keys.toList()}}privatefunloadKeys():Map<String,Secret>{if(!keyFile.exists()||keyFile.length()==0L){returnemptyMap()}try{valfileContent=keyFile.readBytes()valencryptedData=parseEncryptedFile(fileContent)valdecryptedContent=keyEncryption.decrypt(encryptedData)valjsonString=String(decryptedContent,Charsets.UTF_8)valkeysJson=json.parseToJsonElement(jsonString).jsonObjectreturnkeysJson.entries.associate{(keyId,secretJson)->keyIdtojson.decodeFromJsonElement(Secret.serializer(),secretJson)}}catch(e:Exception){throwIllegalStateException("Failed to load keys from encrypted file",e)}}privatefunsaveKeys(keys:Map<String,Secret>){try{valkeysJson=buildJsonObject{keys.forEach{(keyId,secret)->put(keyId,json.encodeToJsonElement(Secret.serializer(),secret))}}valjsonString=json.encodeToString(JsonObject.serializer(),keysJson)valplaintext=jsonString.toByteArray(Charsets.UTF_8)valencryptedData=keyEncryption.encrypt(plaintext)valfileContent=serializeEncryptedFile(encryptedData)// Atomic write: write to temp file, then renamevaltempFile=File(keyFile.parent,"${keyFile.name}.tmp")tempFile.writeBytes(fileContent)setSecurePermissions(tempFile)// Atomic renameif(keyFile.exists()){keyFile.delete()}tempFile.renameTo(keyFile)}catch(e:Exception){throwIllegalStateException("Failed to save keys to encrypted file",e)}}privatefunparseEncryptedFile(content:ByteArray):EncryptedData{// Parse file format:// [4 bytes: version][4 bytes: iv length][iv][ciphertext]varoffset=0valversion=content.sliceArray(offsetuntiloffset+4)offset+=4valivLength=content.sliceArray(offsetuntiloffset+4).fold(0){acc,byte->(accshl8)or(byte.toInt()and0xFF)}offset+=4valiv=content.sliceArray(offsetuntiloffset+ivLength)offset+=ivLengthvalciphertext=content.sliceArray(offsetuntilcontent.size)returnEncryptedData(iv=iv,ciphertext=ciphertext,algorithm="AES/GCM/NoPadding")}privatefunserializeEncryptedFile(encrypted:EncryptedData):ByteArray{valversion=byteArrayOf(0x01,0x00,0x00,0x00)// Version 1valivLength=byteArrayOf(((encrypted.iv.sizeshr24)and0xFF).toByte(),((encrypted.iv.sizeshr16)and0xFF).toByte(),((encrypted.iv.sizeshr8)and0xFF).toByte(),(encrypted.iv.sizeand0xFF).toByte())returnversion+ivLength+encrypted.iv+encrypted.ciphertext}privatefunsetSecurePermissions(file:File){try{// Set permissions to 600 (owner read/write only)if(System.getProperty("os.name").lowercase().contains("win")){// Windows: Use Java NIOfile.setReadable(false,false)file.setWritable(false,false)file.setReadable(true,true)file.setWritable(true,true)}else{// Unix: Use POSIX permissionsvalperms=setOf(PosixFilePermission.OWNER_READ,PosixFilePermission.OWNER_WRITE)Files.setPosixFilePermissions(file.toPath(),perms)}}catch(e:Exception){// Ignore if permissions can't be set}}}/**
* Factory for creating EncryptedFileLocalKeyStore with password.
*/objectEncryptedFileLocalKeyStoreFactory{/**
* Creates an encrypted file key store from a password.
*
* @param keyFile File to store keys
* @param password Password for encryption
* @param salt Salt for key derivation (optional, will be generated)
* @return EncryptedFileLocalKeyStore instance
*/funcreate(keyFile:File,password:CharArray,salt:ByteArray?=null):EncryptedFileLocalKeyStore{valactualSalt=salt?:ByteArray(16).apply{java.security.SecureRandom().nextBytes(this)}valmasterKey=MasterKeyDerivation.deriveKey(password=password,salt=actualSalt,iterations=100000)returnEncryptedFileLocalKeyStore(keyFile,masterKey)}}
Testing Strategy
Unit tests for encryption/decryption
Integration tests with file system
Security tests for key access
Performance tests for large key sets
Dependencies
BouncyCastle (already included)
Kotlinx Serialization (already included)
2. Message Encryption at Rest
Overview
Encrypt message JSON in the database to protect sensitive data.
classPostgresDidCommMessageStorage(privatevaldataSource:DataSource,privatevalencryption:MessageEncryption?=null):DidCommMessageStorage{overridefunsetEncryption(encryption:MessageEncryption?){// Update encryption instance}overridesuspendfunstore(message:DidCommMessage):String=withContext(Dispatchers.IO){valmessageToStore=if(encryption!=null){// Encrypt messagevalencrypted=encryption.encrypt(message)// Store encrypted datastoreEncrypted(encrypted,message.id)returnmessage.id}else{// Store unencrypted (existing logic)storeUnencrypted(message)returnmessage.id}}privatesuspendfunstoreEncrypted(encrypted:EncryptedMessage,messageId:String){// Store encrypted data in database// Add columns: encrypted_data BYTEA, key_version INT, iv BYTEA}}
Database Schema Updates
1
2
3
4
5
6
7
8
-- Add encryption columns to messages tableALTERTABLEdidcomm_messagesADDCOLUMNIFNOTEXISTSencrypted_dataBYTEA;ALTERTABLEdidcomm_messagesADDCOLUMNIFNOTEXISTSkey_versionINT;ALTERTABLEdidcomm_messagesADDCOLUMNIFNOTEXISTSivBYTEA;ALTERTABLEdidcomm_messagesADDCOLUMNIFNOTEXISTSis_encryptedBOOLEANDEFAULTFALSE;-- Create index for key version (for key rotation queries)CREATEINDEXIFNOTEXISTSidx_messages_key_versionONdidcomm_messages(key_version);
packagecom.trustweave.credential.didcomm.storage.archiveimportcom.trustweave.credential.didcomm.models.DidCommMessageimportcom.trustweave.credential.didcomm.storage.DidCommMessageStorageimportkotlinx.coroutines.Dispatchersimportkotlinx.coroutines.withContextimportkotlinx.serialization.json.*importjava.io.ByteArrayOutputStreamimportjava.util.zip.GZIPOutputStream/**
* Archives messages to cold storage.
*/interfaceMessageArchiver{/**
* Archives messages matching the policy.
*/suspendfunarchiveMessages(policy:ArchivePolicy):ArchiveResult/**
* Restores archived messages.
*/suspendfunrestoreMessages(archiveId:String):RestoreResult}data classArchiveResult(valarchiveId:String,valmessageCount:Int,valarchiveSize:Long,valstorageLocation:String)data classRestoreResult(valmessageCount:Int,valrestoredIds:List<String>)/**
* S3-based message archiver.
*/classS3MessageArchiver(privatevalstorage:DidCommMessageStorage,privatevals3Client:Any,// AWS S3 clientprivatevalbucketName:String,privatevalprefix:String="archives/"):MessageArchiver{overridesuspendfunarchiveMessages(policy:ArchivePolicy):ArchiveResult=withContext(Dispatchers.IO){// Find messages to archivevalmessagesToArchive=findMessagesToArchive(policy)if(messagesToArchive.isEmpty()){return@withContextArchiveResult(archiveId="",messageCount=0,archiveSize=0,storageLocation="")}// Create archive file (compressed JSONL)valarchiveId=generateArchiveId()valarchiveData=createArchiveFile(messagesToArchive)// Upload to S3vals3Key="$prefix$archiveId.jsonl.gz"uploadToS3(s3Key,archiveData)// Mark messages as archived in databasemarkAsArchived(messagesToArchive.map{it.id})ArchiveResult(archiveId=archiveId,messageCount=messagesToArchive.size,archiveSize=archiveData.size.toLong(),storageLocation="s3://$bucketName/$s3Key")}privatesuspendfunfindMessagesToArchive(policy:ArchivePolicy):List<DidCommMessage>{// Query all messages and filter by policy// In production, use efficient query based on policyreturnemptyList()// Implementation}privatefuncreateArchiveFile(messages:List<DidCommMessage>):ByteArray{valjson=Json{prettyPrint=false;encodeDefaults=false}valoutput=ByteArrayOutputStream()GZIPOutputStream(output).use{gzip->messages.forEach{message->valline=json.encodeToString(DidCommMessage.serializer(),message)+"\n"gzip.write(line.toByteArray(Charsets.UTF_8))}}returnoutput.toByteArray()}privatesuspendfunuploadToS3(key:String,data:ByteArray){// Upload to S3// Implementation depends on S3 client}privatesuspendfunmarkAsArchived(messageIds:List<String>){// Update database to mark messages as archived// Add 'archived' flag to messages table}privatefungenerateArchiveId():String{returnjava.util.UUID.randomUUID().toString()}}
packagecom.trustweave.credential.didcomm.storage.replicationimportcom.trustweave.credential.didcomm.models.DidCommMessageimportcom.trustweave.credential.didcomm.storage.DidCommMessageStorageimportkotlinx.coroutines.asyncimportkotlinx.coroutines.awaitAllimportkotlinx.coroutines.coroutineScope/**
* Manages message replication across multiple storage backends.
*/classReplicationManager(privatevalprimary:DidCommMessageStorage,privatevalreplicas:List<DidCommMessageStorage>,privatevalreplicationMode:ReplicationMode=ReplicationMode.ASYNC):DidCommMessageStorage{enumclassReplicationMode{SYNC,// Wait for all replicasASYNC,// Fire and forgetQUORUM// Wait for majority}overridesuspendfunstore(message:DidCommMessage):String=coroutineScope{// Write to primaryvalmessageId=primary.store(message)// Replicate to replicaswhen(replicationMode){ReplicationMode.SYNC->{replicas.map{async{it.store(message)}}.awaitAll()}ReplicationMode.ASYNC->{replicas.forEach{replica->// Fire and forgetkotlinx.coroutines.launch{try{replica.store(message)}catch(e:Exception){// Log error, continue}}}}ReplicationMode.QUORUM->{valquorum=(replicas.size/2)+1replicas.map{async{it.store(message)}}.take(quorum).awaitAll()}}messageId}overridesuspendfunget(messageId:String):DidCommMessage?{// Try primary firstreturnprimary.get(messageId)?:run{// If not found, try replicasreplicas.firstNotNullOfOrNull{it.get(messageId)}}}// Implement other methods with replication logic...}
/**
* Health check for replica databases.
*/interfaceReplicaHealthCheck{suspendfuncheckHealth(storage:DidCommMessageStorage):HealthStatussuspendfungetHealthyReplicas():List<DidCommMessageStorage>}data classHealthStatus(valisHealthy:Boolean,vallatency:Long,// millisecondsvallastCheck:Instant)
6. Advanced Search Capabilities
Overview
Implement full-text search, faceted search, and complex query capabilities.
/**
* PostgreSQL full-text search implementation.
*/classPostgresFullTextSearch(privatevalstorage:PostgresDidCommMessageStorage):AdvancedSearch{overridesuspendfunfullTextSearch(query:String,limit:Int,offset:Int):List<DidCommMessage>{// Use PostgreSQL tsvector/tsquery for full-text search// Add GIN index on searchable fieldsreturnemptyList()// Implementation}// Implement other methods...}
/**
* PostgreSQL-based analytics implementation.
*/classPostgresMessageAnalytics(privatevalstorage:PostgresDidCommMessageStorage):MessageAnalytics{overridesuspendfungetStatistics(startTime:Instant,endTime:Instant,groupBy:GroupBy):MessageStatistics{// Query database for statistics// Use SQL aggregations and GROUP BYreturnMessageStatistics(totalMessages=0,sentMessages=0,receivedMessages=0,averageMessageSize=0,timeSeries=emptyList())}// Implement other methods...}
8. Key Rotation Automation
Overview
Automate key rotation for DIDComm keys to maintain security.
Architecture
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Key Rotation Manager
│
├── Rotation Policy
│ ├── Time-based (e.g., every 90 days)
│ ├── Usage-based (e.g., after N uses)
│ └── Manual trigger
│
├── Rotation Process
│ ├── Generate new key
│ ├── Update DID document
│ ├── Migrate messages
│ └── Archive old key
│
└── Key Lifecycle
├── Active
├── Rotating
└── Archived
packagecom.trustweave.credential.didcomm.crypto.rotationimportcom.trustweave.credential.didcomm.crypto.secret.LocalKeyStoreimportcom.trustweave.kms.KeyManagementServiceimportorg.didcommx.didcomm.secret.Secretimportkotlinx.coroutines.Dispatchersimportkotlinx.coroutines.withContext/**
* Manages key rotation for DIDComm keys.
*/classKeyRotationManager(privatevalkeyStore:LocalKeyStore,privatevalkms:KeyManagementService,privatevalpolicy:KeyRotationPolicy){/**
* Checks and rotates keys if needed.
*/suspendfuncheckAndRotate():RotationResult=withContext(Dispatchers.IO){valkeysToRotate=findKeysToRotate()valresults=keysToRotate.map{keyId->rotateKey(keyId)}RotationResult(rotatedCount=results.size,results=results)}/**
* Rotates a specific key.
*/suspendfunrotateKey(keyId:String):KeyRotationResult=withContext(Dispatchers.IO){// 1. Get current keyvaloldKey=keyStore.get(keyId)?:throwIllegalArgumentException("Key not found: $keyId")// 2. Generate new keyvalnewKeyId=generateNewKeyId(keyId)valnewKey=generateNewKey(newKeyId)// 3. Store new keykeyStore.store(newKeyId,newKey)// 4. Update DID document (if applicable)updateDidDocument(keyId,newKeyId)// 5. Archive old keyarchiveOldKey(keyId,oldKey)KeyRotationResult(oldKeyId=keyId,newKeyId=newKeyId,success=true)}privatesuspendfunfindKeysToRotate():List<String>{valallKeys=keyStore.list()returnallKeys.filter{keyId->valmetadata=getKeyMetadata(keyId)policy.shouldRotate(keyId,metadata)}}privatesuspendfungetKeyMetadata(keyId:String):KeyMetadata{// Get metadata from key store or separate metadata storereturnKeyMetadata(keyId=keyId,createdAt=Instant.now().minus(100,ChronoUnit.DAYS),lastUsedAt=null,usageCount=0)}privatefungenerateNewKeyId(oldKeyId:String):String{// Generate new key ID (e.g., increment version)return"$oldKeyId-v2"}privatesuspendfungenerateNewKey(keyId:String):Secret{// Generate new key using KMS// Implementation depends on key typethrowNotImplementedError("Key generation to be implemented")}privatesuspendfunupdateDidDocument(oldKeyId:String,newKeyId:String){// Update DID document with new key// Implementation depends on DID method}privatesuspendfunarchiveOldKey(keyId:String,key:Secret){// Archive old key (don't delete immediately)// Keep for decryption of old messages}}data classRotationResult(valrotatedCount:Int,valresults:List<KeyRotationResult>)data classKeyRotationResult(valoldKeyId:String,valnewKeyId:String,valsuccess:Boolean,valerror:String?=null)