DIDComm Library
A Dart package for implementing secure and private communication on your app using DIDComm v2 Messaging protocol. DIDComm v2 Messaging is a decentralised communication protocol that uses a Decentralised Identifier (DID) to establish a secure communication channel and send a private and verifiable message.
The DIDComm for Dart package provides the tools and libraries to enable your app to send DIDComm messages. It supports various encryption algorithms and DID methods, such as did:peer, did:key, and did:web for signing and encrypting to ensure the secure and private transport of messages to the intended recipient, establishing verifiable and trusted communication.
Core Concepts
The DIDComm for Dart package utilises existing open standards and cryptographic techniques to provide secure, private, and verifiable communication.
Decentralised Identifier (DID) - A globally unique identifier that enables secure interactions. The DID is the cornerstone of Self-Sovereign Identity (SSI), a concept that aims to put individuals or entities in control of their digital identities.
DID Document - A DID is a URI (Uniform Resource Identifier) that resolves into a DID Document that contains information such as cryptographic public keys, authentication methods, and service endpoints. It allows others to verify signatures, authenticate interactions, and validate messages cryptographically.
DIDComm Message - is a JSON Web Message (JWM), a lightweight, secure, and standardised format for structured communication using JSON. It represents headers, message types, routing metadata, and payloads designed to enable secure and interoperable communication across different systems.
Mediator - A service that handles and routes messages sent between participants (e.g., users, organisations, another mediator, or even AI agents).
DID Manager - Establishes the relationship between DID methods and key pairs from the Wallet, supporting different algorithms for signing and verifying messages.
Key Features
Implements the DIDComm Message v2.0 protocol.
Support for DIDComm Messaging Envelope types. Refer to the DIDComm Message Envelopes section to learn more.
Support for digital wallets under Affinidi Dart SSI to manage cryptographic keys.
Connect and authenticate with different mediator services that follow the DIDComm Message v2.0 protocol.
Supported Curves and Algorithms
The DIDComm package supports the following curves and algorithms for signing and encrypting content.
Curves and Signing Algorithms
| Curve | Signing Algorithm | Used in Content Encryption | Notes |
|---|---|---|---|
| Ed25519 | EdDSA | ✅ Yes* | Ed25519 is for signing only; X25519 is used for encryption/key exchange through Ed25519/X25519 key derivation |
| P-384 | ES384 | ✅ Yes | ES384 support for signing is not listed in the DIDComm spec |
| P-521 | ES521 | ✅ Yes | ES521 support for signing is not listed in the DIDComm spec |
| secp256k1 | ES256K | ✅ Yes | secp256k1 support for encryption is not listed in the DIDComm spec |
| P-256 | ES256 | ✅ Yes | Deprecated in DIDComm v2 in favor of P-384 |
Content Encryption Algorithms
| Encryption Algorithm | Notes |
|---|---|
| A256CBC-HS512 | Used for Authcrypt/Anoncrypt |
| A256GCM | Used for Anoncrypt |
| XC20P | Not supported yet |
Note: XC20P is part of the DIDComm spec, but not yet supported by the Affinidi SSI.
Supported DID Methods
The DIDComm package supports the following DID methods to represent the identity of each entity.
| DID Method | Note |
|---|---|
| did:key | Fully supported by Affinidi Dart SSI’s DID Manager |
| did:peer | Fully supported by Affinidi Dart SSI’s DID Manager |
| did:web | Only DID resolution is supported |
DIDComm Message Envelopes
DIDComm v2 messages can be sent in the following formats: plaintext, signed, and encrypted. Each format, called “envelope”, provides different security and privacy guarantees and can be combined in various ways.
Plaintext message: A message that is neither signed nor encrypted. It is readable by anyone and provides no integrity or authenticity guarantees. Used for non-sensitive data, debugging, or as the inner content of other envelopes.
Signed message: A message that is digitally signed but not encrypted. Anyone can read it, but the recipient can prove who signed it (non-repudiation)—used when the message’s origin must be provable to the recipient or third parties.
Encrypted message: An encrypted message for one or more recipients. Only the intended recipients can read the content of the message. Encryption can be:
Authenticated encryption (authcrypt, ECDH-1PU): Proves the sender’s identity to the recipient (but not to intermediaries). Used when both confidentiality and sender authenticity are required.
It uses the ECDH-1PU for authenticated encryption (authcrypt), where the sender’s key is involved in the encryption process, allowing the recipient to verify the sender’s identity.
Anonymous encryption (anoncrypt, ECDH-ES): Hides the sender’s identity from the recipient and intermediaries. It is used when the sender’s anonymity is required.
It uses ECDH-ES for anonymous encryption (anoncrypt), where only the recipient’s key is used, and the sender remains anonymous.
Get Started
Requirements
- Dart SDK version ^3.6.0
Installation
Run:
dart pub add didcommor manually, add the package into your pubspec.yaml file:
dependencies:
didcomm: ^<version_number>and then run the command below to install the package:
dart pub getVisit the pub.dev install page of the Dart package for more information.
Sample Usage
Below is a step-by-step example of secure communication between Alice and Bob using the DIDComm Dart package. The example demonstrates how to construct, sign, encrypt, and unpack messages according to the DIDComm Messaging spec.
1. Set up DID Manager and DIDs
final aliceKeyStore = InMemoryKeyStore();
final aliceWallet = PersistentWallet(aliceKeyStore);
final aliceDidManager = DidKeyManager(
wallet: aliceWallet,
store: InMemoryDidStore(),
);
final bobKeyStore = InMemoryKeyStore();
final bobWallet = PersistentWallet(bobKeyStore);
final bobDidManager = DidKeyManager(
wallet: bobWallet,
store: InMemoryDidStore(),
);
final aliceKeyId = 'alice-key-1';
await aliceWallet.generateKey(
keyId: aliceKeyId,
keyType: KeyType.p256,
);
await aliceDidManager.addVerificationMethod(aliceKeyId);
final aliceDidDocument = await aliceDidManager.getDidDocument();
final bobKeyId = 'bob-key-1';
await bobWallet.generateKey(
keyId: bobKeyId,
keyType: KeyType.p256,
);
await bobDidManager.addVerificationMethod(bobKeyId);
final bobDidDocument = await bobDidManager.getDidDocument();2. Compose a Plain Text Message
A plain text message is a simple JSON message with headers and a body.
final plainTextMessage = PlainTextMessage(
id: '041b47d4-9c8f-4a24-ae85-b60ec91b025c', // Unique message ID
from: aliceDidDocument.id, // Sender's DID
to: [bobDidDocument.id], // Recipient's DID(s)
type: Uri.parse('https://didcomm.org/example/1.0/message'), // Message type URI
body: {'content': 'Hello, Bob!'}, // Message payload
);
plainTextMessage['custom-header'] = 'custom-value'; // Add custom headers if needed3. Sign the Message
Signing a message is optional in DIDComm. It is required when you need to provide non-repudiation—proof that the sender cannot deny authorship of the message. Signing a message is essential for scenarios where:
- The recipient must prove to a third party that the sender authored the message (e.g., legal, regulatory, or audit requirements).
- The message may be forwarded or relayed, and recipients must verify its origin independently of the transport channel.
- You want to ensure message integrity and origin even if the message is not encrypted.
final aliceSigner = await aliceDidManager.getSigner(
aliceDidDocument.assertionMethod.first.id,
);
final signedMessage = await SignedMessage.pack(
plainTextMessage,
signer: aliceSigner, // The signer instance
);4. Encrypt the Message
Although optional, encrypting DIDComm messages is highly recommended to protect their confidentiality. DIDComm supports two main types of encryption:
- Authenticated Encryption (authcrypt, ECDH-1PU):
- Proves the sender’s identity to the recipient (but not to intermediaries).
- Used when both confidentiality and sender authenticity are required.
- Only the intended recipient can read the message and verify that the sender’s key encrypts the message.
- Protects the sender’s identity from intermediaries and eavesdroppers.
Choose authcrypt when you want the recipient to know who sent the message (authenticated, private communication).
- Anonymous Encryption (anoncrypt, ECDH-ES):
- Hides the sender’s identity from both the recipient and intermediaries.
- Used when sender anonymity is required.
- Only the intended recipient can read the message but cannot determine who sent it.
Choose anoncrypt when you want to keep the sender’s identity hidden (anonymous tips, whistleblowing, or privacy-preserving scenarios).
Example: Authenticated Encryption (authcrypt)
final aliceMatchedKeyIds = aliceDidDocument.matchKeysInKeyAgreement(
otherDidDocuments: [bobDidDocument],
);
final encryptedMessage = await EncryptedMessage.packWithAuthentication(
message, // The signed or plain text message to encrypt
keyPair: await aliceDidManager.getKeyPairByDidKeyId(aliceMatchedKeyIds.first),
didKeyId: aliceMatchedKeyIds.first,
recipientDidDocuments: [bobDidDocument],
);keyPair: Alice’s key pair used for authenticated encryption.didKeyId: The key ID from Alice’s DID Document for key agreement.recipientDidDocuments: The recipient’s DID Document(s).encryptionAlgorithm: The encryption algorithm to use (e.g.,a256cbc).
Example: Anonymous Encryption (anoncrypt)
If you want to encrypt a message without revealing the sender’s identity, use packAnonymously:
final anonymousEncryptedMessage = await EncryptedMessage.packAnonymously(
message, // The signed or plain text message to encrypt
keyType: KeyType.p256, // Key type for recipient's key agreement (required)
recipientDidDocuments: [bobDidDocument], // List of recipient DID Documents
encryptionAlgorithm: EncryptionAlgorithm.a256cbc, // Encryption algorithm
);message: The message to encrypt (can be plain or signed).recipientDidDocuments: The recipient’s DID Document(s).encryptionAlgorithm: The encryption algorithm to use.keyType: The key type for the recipient’s key agreement key (e.g.,KeyType.p256,KeyType.ed25519).
In this case, Bob can decrypt and read the message but cannot determine who sent it. This approach is helpful for scenarios where sender anonymity is required.
More details about the key type selection for authcrypt and anoncrypt.
Key Type Selection for Authcrypt and Anoncrypt
When encrypting messages, you must select a key type for a key agreement that all parties support.
Authcrypt (authenticated encryption, ECDH-1PU):
- For key agreement, both the sender and all recipients must use compatible key types (e.g., both must have P-256 or X25519 keys in their DID Documents).
- You typically use the sender’s DidManager to find a compatible key pair and key agreement method with each recipient.
- Use the
matchKeysInKeyAgreementextension method to find compatible key IDs from the sender’s wallet for all recipients.
Anoncrypt (anonymous encryption, ECDH-ES):
- Only uses the recipients’ key agreement keys; does not involve the sender’s key pair.
- You must select a key type that all recipients for key agreement support.
- Use the
getCommonKeyTypesInKeyAgreementsextension method on the list of recipient DID Documents to determine the set of key types common to all recipients.
Examples:
Authcrypt:
final compatibleKeyIds = aliceDidDocument.matchKeysInKeyAgreement(
otherDidDocuments: [
bobDidDocument
// and other recipients
],
);
if (compatibleKeyIds.isEmpty) {
throw Exception('No compatible key agreement method found between Alice and Bob');
}
final aliceDidKeyId = compatibleKeyIds.first; // Use this key ID for AliceAnoncrypt:
final commonKeyTypes = [
bobDidDocument,
// and other recipients
].getCommonKeyTypesInKeyAgreements();
if (commonKeyTypes.isEmpty) {
throw Exception('No common key type found for anoncrypt between recipients');
}
final keyType = commonKeyTypes.first; // Use this key type for anoncryptThis ensures that the correct and compatible keys are used for ECDH-1PU (authcrypt) and ECDH-ES (anoncrypt) operations, and that all recipients can decrypt the message using a supported key agreement method.
Security Safeguards
The DIDComm package performs different verification processes to ensure the integrity and authenticity of the messages received, including a custom verification and processing after the message is unpacked.
Message Layer Addressing Consistency
To ensure trust and prevent message tampering or misrouting, DIDComm v2 enforces message layer addressing consistency between the plaintext message and its cryptographic envelopes. According to the DIDComm v2 spec:
- The
fromattribute in the plaintext message must match theskid(sender key ID) in the encryption layer. - The
toattribute in the plaintext message must contain thekid(recipient key ID) in the encryption layer. - The
fromattribute in the plaintext message must match the signer’skidin a signed message.
Envelope’s layout:
encrypted message:
header:
skid: did:example:alice#key-1
recipients:
- kid: did:example:bob#key-1
payload:
signed message:
signatures:
- kid: did:example:alice#key-1
payload:
plain text message:
from: did:example:alice#key-1
to: did:example:bob#key-1
body: ...If any of these checks fail, the message is considered invalid and an error is raised.
When you call unpackToPlainTextMessage in this Dart library, addressing consistency checks are performed automatically. If any inconsistency is detected, unpackToPlainTextMessage throws an error, preventing further processing of potentially malicious or misrouted messages. This strict enforcement helps maintain the integrity and authenticity of DIDComm messages, as required by the specification.
For development or debugging purposes, you can disable addressing consistency checks by passing validateAddressingConsistency: false to unpackToPlainTextMessage. This allows you to inspect or process messages that would otherwise be rejected due to addressing mismatches.
Important: Disabling these checks should only be done in trusted, non-production environments, as it weakens security guarantees and may expose your application to spoofed or misrouted messages.
Message Wrapping Verification
The expectedMessageWrappingTypes argument of unpackToPlainTextMessage lets you specify which message wrapping types (e.g., authenticated encryption, signed, plaintext) are allowed when unpacking a message. This is an important security feature that helps prevent downgrade attacks and ensures the message you receive matches your security expectations.
- If you provide a list of wrapping types (such as
[MessageWrappingType.authcryptSignPlaintext, MessageWrappingType.authcryptPlaintext, MessageWrappingType.anoncryptPlaintext]), only messages with those wrappings will be accepted. Any message with a different wrapping will be rejected. - If you do not specify
expectedMessageWrappingTypes(i.e., leave itnullor omit it), the default is[MessageWrappingType.authcryptPlaintext]as recommended by the DIDComm v2 specification. This means only authenticated encrypted messages will be accepted by default.
Tip: Always set expectedMessageWrappingTypes explicitly to match your protocol or application’s requirements. This helps ensure you are not tricked into processing a message with weaker security than intended.
Example how to use:
final plainTextMessage =
await DidcommMessage.unpackToPlainTextMessage(
message: json,
recipientDidManager: bobDidManager,
expectedMessageWrappingTypes: [
MessageWrappingType.anoncryptSignPlaintext,
MessageWrappingType.authcryptSignPlaintext,
MessageWrappingType.authcryptPlaintext,
MessageWrappingType.anoncryptAuthcryptPlaintext,
],
);Signers Verification
When unpacking a message, every signature present is automatically and individually verified by the library to be cryptographically correct and to match the claimed key. If you want to make sure that a certain signers signed the message, you can use the expectedSigners argument.
The expectedSigners argument of unpackToPlainTextMessage lets you specify a set of signers’ key IDs that you require to have signed the message. This is especially important for signed messages that may contain multiple signatures (for example, in multi-party workflows or protocols requiring multiple approvals).
Example with multiple signatures:
{
"payload":"<decoded plain text message>",
"signatures":[
{
"protected":"...",
"signature":"<signature-1 base64>",
"header":{
"kid":"did:example:alice#key-1"
}
},
{
"protected":"...",
"signature":"<signature-2 base64>",
"header":{
"kid":"did:example:bob#key-1"
}
}
]
}When you provide a list of key IDs in expectedSigners, the function will check that all of the provided signers have actually signed the message. Additional signatures beyond those listed in expectedSigners are allowed, but if any of the expected signers are missing from the message’s signatures, the message will be rejected. This ensures that all required parties have signed the message, and prevents partial or incomplete multi-signature messages from being processed.
If you do not specify expectedSigners, the function will not enforce any signer requirements beyond the addressing consistency checks (i.e., it will only check that the from field matches at least one signature’s key ID, as required by the DIDComm spec).
Tip: Always use expectedSigners when you need to ensure that a message was signed by at least a specific set of parties, especially in scenarios where multiple signatures are possible or required. This is critical for protocols that require multi-party approval or consensus.
Example how to use:
final plainTextMessage =
await DidcommMessage.unpackToPlainTextMessage(
message: json,
recipientDidManager: bobDidManager,
expectedMessageWrappingTypes: [
MessageWrappingType.anoncryptSignPlaintext,
MessageWrappingType.authcryptSignPlaintext,
MessageWrappingType.signPlaintext,
],
expectedSigners: [
'did:example:alice#key-1',
'did:example:bob#key-1',
],
);Glad to hear it! Please tell us how we can improve more.
Sorry to hear that. Please tell us how we can improve.
Thank you for sharing your feedback so we can improve your experience.