DIDComm Crypto Implementation Notes
Current Status: Placeholder Implementation
The current DidCommCrypto class has placeholder implementations for the core cryptographic operations. This means the code structure is correct, but the actual cryptographic calculations are not implemented.
What’s Missing
1. ECDH-1PU Key Agreement (Lines 260-287)
Current (Placeholder):
1
2
3
4
5
6
7
8
private suspend fun performEcdh1puKeyAgreement(...): ByteArray {
// Simplified ECDH-1PU implementation
// In production, use a proper DIDComm library
// This is a placeholder that demonstrates the structure
// For now, return a placeholder shared secret
return ByteArray(32) // ❌ This is just dummy data!
}
What It Should Do:
- Load the private key from KMS (using the
keyId) - Convert JWK public keys to
ECPublicKeyobjects - Perform ECDH-1PU key agreement:
- ECDH with sender’s public key:
ECDH(sender_private, recipient_public) - ECDH with ephemeral key:
ECDH(ephemeral_private, recipient_public) - Combine using the ECDH-1PU algorithm (RFC 7748 + additional steps)
- ECDH with sender’s public key:
- Return the actual shared secret (32 bytes for X25519/Ed25519)
Why It’s Complex:
- ECDH-1PU is a specific variant that includes the sender’s public key in the key agreement
- Requires proper curve point operations
- Must handle different curve types (Ed25519, secp256k1, P-256, etc.)
- Needs proper key derivation
2. Key Derivation (Lines 289-295)
Current (Simplified):
1
2
3
4
5
6
7
private fun deriveKeys(sharedSecret: ByteArray): Pair<ByteArray, ByteArray> {
// Derive CEK and KEK using HKDF
// Simplified: split the shared secret
val cek = sharedSecret.sliceArray(0..15) // ❌ Just splitting bytes!
val kek = sharedSecret.sliceArray(16..31)
return Pair(cek, kek)
}
What It Should Do:
- Use HKDF (HMAC-based Key Derivation Function) as specified in RFC 5869
- Derive Content Encryption Key (CEK) - 32 bytes for AES-256
- Derive Key Encryption Key (KEK) - 32 bytes for AES-256-KW
- Use proper salt and info parameters as per DIDComm spec
Why It Matters:
- Simple byte splitting is not cryptographically secure
- HKDF ensures proper key derivation with entropy
- Required for interoperability with other DIDComm implementations
3. Private Key Access
Current Issue:
The code tries to use kms.getPublicKey(keyId) but never accesses the private key, which is needed for:
- ECDH key agreement (requires private key)
- Decryption (requires recipient’s private key)
What’s Needed:
- A way to get private keys from KMS (or store them separately)
- Proper key material handling
- Secure key storage and access
What a Full Implementation Would Look Like
Option 1: Use didcomm-java Library
The didcomm-java library (from org.didcommx:didcomm) provides:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Add to build.gradle.kts
dependencies {
implementation("org.didcommx:didcomm:0.3.2")
}
// Usage
import org.didcommx.didcomm.DIDComm
import org.didcommx.didcomm.message.Message
import org.didcommx.didcomm.pack.EncryptedPackedMessage
val didComm = DIDComm()
// Pack (encrypt) a message
val packed = didComm.pack(
message = message,
from = fromDid,
to = listOf(toDid),
signFrom = fromKeyId
)
// Unpack (decrypt) a message
val unpacked = didComm.unpack(
packed = packedMessage,
to = recipientDid,
from = senderDid
)
Benefits:
- ✅ Fully tested and compliant
- ✅ Handles all edge cases
- ✅ Interoperable with other DIDComm implementations
- ✅ Actively maintained
Option 2: Implement ECDH-1PU Manually
If you want to implement it yourself, you’d need to:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
private suspend fun performEcdh1puKeyAgreement(
privateKeyId: String,
senderPublicKeyJwk: Map<String, Any?>,
recipientPublicKeyJwk: Map<String, Any?>,
epk: JsonObject
): ByteArray {
// 1. Get private key from KMS
val privateKey = kms.getPrivateKey(privateKeyId) // ⚠️ Need this method!
// 2. Convert JWK to EC public keys
val senderPublicKey = jwkToECPublicKey(senderPublicKeyJwk)
val recipientPublicKey = jwkToECPublicKey(recipientPublicKeyJwk)
val ephemeralPublicKey = jwkToECPublicKey(epk)
// 3. Perform ECDH operations
val keyAgreement = KeyAgreement.getInstance("ECDH", "BC")
keyAgreement.init(privateKey)
// ECDH with sender's key
keyAgreement.doPhase(senderPublicKey, true)
val senderShared = keyAgreement.generateSecret()
// ECDH with ephemeral key
keyAgreement.init(privateKey)
keyAgreement.doPhase(ephemeralPublicKey, true)
val ephemeralShared = keyAgreement.generateSecret()
// 4. Combine using ECDH-1PU algorithm
// This is the complex part - requires specific algorithm
val combined = combineEcdh1pu(senderShared, ephemeralShared, senderPublicKey)
return combined
}
private fun deriveKeys(sharedSecret: ByteArray): Pair<ByteArray, ByteArray> {
// Use HKDF
val hkdf = HKDF.fromHmacSha256()
val salt = ByteArray(32) // Should be from message or protocol
val info = "didcomm-encryption".toByteArray()
val cek = hkdf.extractAndExpand(sharedSecret, salt, info, 32)
val kek = hkdf.extractAndExpand(sharedSecret, salt, "didcomm-key-wrapping".toByteArray(), 32)
return Pair(cek, kek)
}
Challenges:
- ❌ ECDH-1PU is complex and easy to get wrong
- ❌ Need to handle multiple curve types
- ❌ Requires extensive testing
- ❌ Must ensure interoperability
Recommendation
For Production: Use didcomm-java library
- Add dependency:
1 2 3
dependencies { implementation("org.didcommx:didcomm:0.3.2") }
- Refactor
DidCommCryptoto use the library:1 2 3 4 5 6 7 8 9 10 11 12 13 14
class DidCommCrypto( private val kms: KeyManagementService, private val resolveDid: suspend (String) -> DidDocument? ) { private val didComm = DIDComm() suspend fun encrypt(...): DidCommEnvelope { // Use didComm.pack() instead of manual encryption } suspend fun decrypt(...): JsonObject { // Use didComm.unpack() instead of manual decryption } }
- Benefits:
- Production-ready cryptography
- Full DIDComm V2 compliance
- Interoperability guaranteed
- Less code to maintain
- Security tested by community
Current Implementation Status
| Component | Status | Notes |
|---|---|---|
| Message Structure | ✅ Complete | JWM format correct |
| Envelope Structure | ✅ Complete | Correct format |
| AES-256-GCM | ✅ Working | Real implementation |
| AES-256-KW | ✅ Working | Real implementation |
| ECDH-1PU | ❌ Placeholder | Returns dummy data |
| HKDF | ❌ Placeholder | Just splits bytes |
| Private Key Access | ❌ Missing | KMS doesn’t expose private keys |
Summary
The current implementation has the structure correct but uses placeholder functions that return dummy data instead of performing real cryptographic operations. For production use, you should either:
- Use
didcomm-javalibrary (recommended) - Drop-in replacement - Implement ECDH-1PU manually - Complex, error-prone, requires extensive testing
The placeholder code demonstrates the flow and structure, but will not actually encrypt/decrypt messages correctly.