Integrate DIDComm for Dart

Learn how to integrate Affinidi Messaging into your projects to enable secure, private and trusted messaging.

The DIDComm for Dart integration guide provides a step-by-step overview for developers to integrate DIDComm-based communication to enable their application to send DIDComm messages. The DIDComm for Dart package enables secure, privacy-preserving, and verifiable messaging between decentralised identifiers (DIDs), allowing users to exchange messages securely and privately without relying on centralised intermediaries.

Key Features

  • Implements the DIDComm v2.1 protocol.

  • Support for DIDComm Messaging Envelope types.

  • Support for digital wallets under Affinidi Dart SSI to manage cryptographic keys.

  • Connect and authenticate with different mediator services that follow the DIDComm v2.1 protocol.

Requirements

  • Dart SDK version 3.6.0

Required Packages

The Dart package below is required to get started with the integration:

PackageDescription
DIDComm for DartA Dart package for implementing secure and private communication on your app using DIDComm v2.1 protocol.

Sample Usage

The examples demonstrate how to construct, sign, encrypt, and unpack messages according to the DIDComm Messaging spec.

Set up DID Manager and DIDs

// Create DID manager for Alice final aliceKeyStore = InMemoryKeyStore(); final aliceWallet = PersistentWallet(aliceKeyStore); final aliceDidManager = DidKeyManager( wallet: aliceWallet, store: InMemoryDidStore(), ); // Create DID manager for Bob final bobKeyStore = InMemoryKeyStore(); final bobWallet = PersistentWallet(bobKeyStore); final bobDidManager = DidKeyManager( wallet: bobWallet, store: InMemoryDidStore(), ); // Generate key for Alice final aliceKeyId = 'alice-key-1'; await aliceWallet.generateKey( keyId: aliceKeyId, keyType: KeyType.p256, ); // Adds Alice's verification method await aliceDidManager.addVerificationMethod(aliceKeyId); final aliceDidDocument = await aliceDidManager.getDidDocument(); // Generate key for Bob final bobKeyId = 'bob-key-1'; await bobWallet.generateKey( keyId: bobKeyId, keyType: KeyType.p256, ); // Adds Bob's verification method await bobDidManager.addVerificationMethod(bobKeyId); final bobDidDocument = await bobDidManager.getDidDocument();

Compose a Plain Text Message

A plain text message is a simple JSON message with headers and a body.

final plainTextMessage = PlainTextMessage( // Unique message ID id: '041b47d4-9c8f-4a24-ae85-b60ec91b025c', // Sender's DID from: aliceDidDocument.id, // Recipient's DID(s) to: [bobDidDocument.id], // Message type URI type: Uri.parse('https://didcomm.org/example/1.0/message'), // Message payload body: {'content': 'Hello, Bob!'}, ); // Add custom headers if needed plainTextMessage['custom-header'] = 'custom-value';

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, );

Encrypt the Message

Encryption of the DIDComm messages is optional, but it is highly recommended to protect their confidentiality. Learn more on DIDComm Messages formats and it’s encryption here .

Authenticated Encryption (authcrypt)

final aliceMatchedKeyIds = aliceDidDocument.matchKeysInKeyAgreement( otherDidDocuments: [bobDidDocument], ); final encryptedMessage = await EncryptedMessage.packWithAuthentication( // The signed or plain text message to encrypt message, 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).

Anonymous Encryption (anoncrypt)

If you want to encrypt a message without revealing the sender’s identity, use packAnonymously:

final anonymousEncryptedMessage = await EncryptedMessage.packAnonymously( // The signed or plain text message to encrypt message, // Key type for recipient's key agreement (required) keyType: KeyType.p256, // List of recipient DID Documents recipientDidDocuments: [bobDidDocument], // Encryption algorithm encryptionAlgorithm: EncryptionAlgorithm.a256cbc, );
  • 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.

Pack DIDComm Message

The DidcommMessage class provides high-level helper methods for common packing and unpacking workflows. These helpers simplify signing and encrypting messages according to your security and privacy requirements.

packIntoEncryptedMessage

Packs a plain text message into an encrypted message. Use this for confidential messages (authcrypt or anoncrypt, depending on parameters).

// Authenticated encryption (authcrypt) final aliceMatchedKeyIds = aliceDidDocument.matchKeysInKeyAgreement( otherDidDocuments: [bobDidDocument], ); final encrypted = await DidcommMessage.packIntoEncryptedMessage( plainTextMessage, keyPair: await aliceDidManager.getKeyPairByDidKeyId(aliceMatchedKeyIds.first), didKeyId: aliceMatchedKeyIds.first, recipientDidDocuments: [bobDidDocument], keyWrappingAlgorithm: KeyWrappingAlgorithm.ecdh1Pu, encryptionAlgorithm: EncryptionAlgorithm.a256cbc, ); // Anonymous encryption (anoncrypt) final anonEncrypted = await DidcommMessage.packIntoEncryptedMessage( plainTextMessage, keyType: KeyType.p256, recipientDidDocuments: [bobDidDocument], keyWrappingAlgorithm: KeyWrappingAlgorithm.ecdh1Es, encryptionAlgorithm: EncryptionAlgorithm.a256cbc, );

packIntoSignedMessage

Packs a plain text message into a signed message. Use this for non-repudiation and message integrity.

final signed = await DidcommMessage.packIntoSignedMessage( plainTextMessage, signer: aliceSigner, );

packIntoSignedAndEncryptedMessages

Packs a plain text message into a signed message and then encrypts it. Use this for both non-repudiation and confidentiality in a single step. Encryption can be anonymous (anoncrypt) or authenticated (authcrypt), depending on the provided parameters. anoncrypt(sign(plaintext)) should be preferred over authcrypt(sign(plaintext)). Find more details here.

// Anonymous encryption - anoncrypt(sign(plaintext)) // this is a preferred way final signedAndAnonEncrypted = await DidcommMessage.packIntoSignedAndEncryptedMessages( plainTextMessage, keyType: KeyType.p256, recipientDidDocuments: [bobDidDocument], keyWrappingAlgorithm: KeyWrappingAlgorithm.ecdh1Es, encryptionAlgorithm: EncryptionAlgorithm.a256cbc, signer: aliceSigner, ); // Authenticated encryption with signed message - authcrypt(sign(plaintext))) final signedAndEncrypted = await DidcommMessage.packIntoSignedAndEncryptedMessages( plainTextMessage, keyPair: aliceKeyPair, didKeyId: aliceDidDocument.keyAgreement[0].id, recipientDidDocuments: [bobDidDocument], keyWrappingAlgorithm: KeyWrappingAlgorithm.ecdh1Pu, encryptionAlgorithm: EncryptionAlgorithm.a256cbc, signer: aliceSigner, );

packIntoAnoncryptAndAuthcryptMessages

Packs a plain text message into two envelopes using authenticated (authcrypt) and anonymous (anoncrypt) encryption layers. This method first wraps the message with authenticated encryption (authcrypt, ECDH-1PU) and then the resulting message is wrapped with anonymous encryption (anoncrypt, ECDH-ES).

Use this when you want to provide sender authenticity (authcrypt) and sender anonymity from intermediaries (anoncrypt). Only the final recipient can verify the sender’s identity. The result is a message with the envelope type anoncrypt(authcrypt(plaintext)).

final doublyEncrypted = await DidcommMessage.packIntoAnoncryptAndAuthcryptMessages( plainTextMessage, keyPair: aliceKeyPair, didKeyId: aliceDidDocument.keyAgreement[0].id, recipientDidDocuments: [bobDidDocument], // Algorithm for content encryption of the outer anoncrypt layer encryptionAlgorithm: EncryptionAlgorithm.a256cbc, );

Add Sender DID to ACL (Access Control List)

To be able to send a message, sender DID should be added to the recipient Access Control List.

// Bob adds Alice's DID to the ACL final bobAccessListAddMessage = AccessListAddMessage( id: const Uuid().v4(), from: bobDidDocument.id, to: [bobMediatorDocument.id], theirDids: [aliceDidDocument.id], expiresTime: expiresTime, ); final bobAccessListAddSentMessage = await bobMediatorClient.sendAclManagementMessage( bobAccessListAddMessage, );

Send a Message

Alice is going to use Bob’s Mediator to send him a message.

// Alice initiates mediator client final aliceMediatorClient = await MediatorClient.init( mediatorDidDocument: bobMediatorDocument, didManager: aliceDidManager, authorizationProvider: await AffinidiAuthorizationProvider.init( mediatorDidDocument: bobMediatorDocument, didManager: aliceDidManager, ), forwardMessageOptions: const ForwardMessageOptions( shouldSign: true, keyWrappingAlgorithm: KeyWrappingAlgorithm.ecdhEs, encryptionAlgorithm: EncryptionAlgorithm.a256cbc, ), ); // Alice sends a message to Bob final sentMessage = await aliceMediatorClient.sendMessage( forwardMessage, );

Unpack DIDComm Message

Bob receives the encrypted message and unpacks it.

unpackToPlainTextMessage

final messages = await bobMediatorClient.fetchMessages(); final unpackedMessage = await DidcommMessage.unpackToPlainTextMessage( // The received message message: jsonDecode(sentMessageByAlice) as Map<String, dynamic>, // Bob's DID manager for decryption recipientDidManager: bobDidManager, // Expected wrapping expectedMessageWrappingTypes: [MessageWrappingType.authcryptSignPlaintext], // List of expected signers' key IDs expectedSigners: [aliceSigner.didKeyId], );
  • message: The received message as a decoded JSON map.

  • recipientDidManager: The DID manager instance used to decrypt the message.

  • expectedMessageWrappingTypes: List of expected message wrapping types. This argument ensures the unpacked message matches the expected security and privacy guarantees.

The values are from the MessageWrappingType enum, which maps to the DIDComm IANA media types, such as authcryptPlaintext for authenticated encryption, signedPlaintext for signed messages, and plaintext for unprotected messages. It helps prevent downgrade attacks and ensures the message is processed as intended.

Note: If expectedMessageWrappingTypes is omitted or set to null, the default expected wrapping type is [MessageWrappingType.authcryptPlaintext] according to the DIDComm specification. This means only authenticated encrypted messages will be accepted by default unless you explicitly specify otherwise.

  • expectedSigners: List of key IDs whose signatures are expected and will be verified.

What’s Next

  Explore Meeting Place, an implementation of Affinidi Messaging

  Integrate Affinidi Messaging into your applications