Vault

Libraries to implement digital identity wallet into your Flutter/Dart applications, including encrypted backup and restore.

Affinidi TDK - Vault for Dart provides the libraries and tools to implement digital identity wallet into your Flutter/Dart application for creating Decentralised Identity solution to securely manage Decentralised Identifiers (DIDs), Verifiable Credentials, and other data that represents the user’s identity based on different context, for example, you can create an identity for shopping, personal finance, and work.

Installation

Package: affinidi_tdk_vault

dart pub add affinidi_tdk_vault

Check the latest version on pub.dev or view the source on GitHub.

Most of the examples on this page also require affinidi_tdk_vault_data_manager for cloud profile storage.

Create and Initialise Vault

Initialise vault with wallet, storage, and profile interface. To create a wallet, you must generate a seed to create the key pairs. The profile is created using a cloud or local storage option supported by the vault.

Refer to the published SSI Dart package to know more about different types of wallet supported.

Import

import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';
import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';

Parameters

vaultStore VaultStore Required
The Vault Store holding the wallet’s seed and account index.
profileRepositories Map<String, ProfileRepository> Required
Profile repositories keyed by a stable repository ID, for example a cloud repository like VfsProfileRepository.
restorableRepositories Map<String, Restorable> Optional
Backup views for individual profile repositories, keyed by the same repository ID used in profileRepositories. Only needed for repositories that participate in Vault backups; see Back Up and Restore the Vault.
namedRestorables Map<String, Restorable> Optional
Additional, cross-cutting components to include in Vault backups, keyed by a stable component ID, for example consent history. See Back Up Consent History.
defaultProfileRepositoryId String Optional
ID of the repository to use as the default when one is not explicitly specified. Defaults to the first entry in profileRepositories.

Example

// Initialise InMemory storage
final vaultStore = InMemoryVaultStore();
await vaultStore.setAccountIndex(32);

// Generate seed from the storage layer
final seed = vaultStore.getRandomSeed();
await vaultStore.setSeed(seed);

// Initialise profile interface
const vfsRepositoryId = 'vfs';
final profileRepositories = <String, ProfileRepository>{
  vfsRepositoryId: VfsProfileRepository(vfsRepositoryId),
};

// In this example, we are using Bip32 type wallet from SSI package
final vault = await Vault.fromVaultStore(
  vaultStore,
  profileRepositories: profileRepositories,
  defaultProfileRepositoryId: vfsRepositoryId,
);

// Ensure vault is initialised before being able to access any of the repositories
await vault.ensureInitialized();

Most Vault, profile, and shared-storage operations also accept an optional cancelToken (VaultCancelToken) to cancel an in-flight request. Call cancelToken.cancel() to cancel; the pending operation completes with a TdkException whose code is request_cancelled.

Manage Vault Profiles

Create and manage profiles associated with the vault.

Create Vault Profile

Create a profile to store credentials and documents. A profile represents a user’s identity.

These examples use a cloud profile backed by Affinidi Vault (VfsProfileRepository). The vault also supports local profiles through the edge provider packages, which participate in backup and restore.

Import

import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';
import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Parameters

name String Required
Display name for the new profile.

Example

// Must initialize vault before being able to access any of the repositories
await vault.ensureInitialized();

// Create Profile
try {
  final profileRepository = vault.defaultProfileRepository;
  // alternatively can be accessed via profile repository identifier
  // final ProfileRepository profileRepository = vault.profileRepositories[vfsRepositoryId];
  await profileRepository.createProfile(name: 'Work Profile');
} on TdkException catch (error) {
  print([error.code, error.message, error.originalMessage].join('\n'));
} catch (e) {
  print('Error: $e');
}

List Vault Profiles

Retrieve the list of profiles associated with the vault, across all registered profile repositories.

Import

import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';
import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

No parameters required.

Example

// Must initialize vault before being able to access any of the repositories
await vault.ensureInitialized();

// Retrieve list of profiles from the vault
var profiles = await vault.listProfiles();

// Check if we have list of profiles
if (!profiles.isEmpty) {
  profiles.forEach((profile) {
    print('${profile.id} : ${profile.name}');
  });
} else {
  print('Vault profiles is empty.');
}

Update Vault Profile

Update a profile associated with the vault.

Import

import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';
import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Parameters

profile Profile Required
The profile to update, with its mutable fields (such as name) already changed locally.

Example

// Must initialize vault before being able to access any of the repositories
await vault.ensureInitialized();

// Get the list of available profiles from the vault
var profiles = await vault.listProfiles();
// For demonstration purposes, we always get the last profile from the list
var profile = profiles.lastOrNull;

if (profile != null) {
  // Update profile name
  profile.name = 'Test Profile 123';
  // Update profile
  await profileRepository.updateProfile(profile);
} else {
  print('Profile not found.');
}

Delete Vault Profile

Delete a profile associated with the vault.

Import

import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';
import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Parameters

profile Profile Required
The profile to delete.

Example

// Must initialize vault before being able to access any of the repositories
await vault.ensureInitialized();

// Get the list of available profiles from the vault
var profiles = await vault.listProfiles();
// For demonstration purposes, we always get the last profile from the list
var profile = profiles.lastOrNull;

if (profile != null) {
  // Delete profile
  await profileRepository.deleteProfile(profile);
} else {
  print('Profile not found.');
}

Share Vault Profile

Share an entire profile with another user to grant access to all data within the profile.

Profile and item sharing requires the profile repository to implement the ProfileAccessSharing interface. The VfsProfileRepository from affinidi_tdk_vault_data_manager supports both profile and item-level sharing.

Import

import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';
import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Parameters

profileId String Required
ID of the profile to share.
toDid String Required
DID of the user receiving access.
permissions Permissions Required
Permissions to grant: Permissions.read, Permissions.write, or Permissions.all.
expiresAt DateTime Optional
When set, access to the profile is automatically revoked at this date/time. See Share Items with Time-Bound Access.

Example

// Must initialize vault before being able to access any of the repositories
await vault.ensureInitialized();

// Owner shares their profile with a grantee
final granteeDid = 'did:key:recipient-did';
final sharedProfile = await vault.shareProfile(
  profileId: 'my-profile-id',
  toDid: granteeDid,
  permissions: Permissions.read, // or Permissions.write, Permissions.all
);

// Owner sends SharedProfileDto to grantee (via your application's communication channel)

Accept Shared Profile

Accept a shared profile that was granted by another user (grantee side).

Import

import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';
import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Parameters

profileId String Required
ID of the grantee’s profile to add the shared storage to.
sharedProfile SharedProfileDto Required
The shared profile info received from the owner, including its KEK.

Example

// Must initialize vault before being able to access any of the repositories
await vault.ensureInitialized();

try {
  // Grantee accepts the shared profile received from owner
  final updatedProfile = await vault.addSharedProfile(
    profileId: 'grantee-profile-id',
    sharedProfile: sharedProfile, // SharedProfileDto received from owner
  );

  print('Successfully accepted shared profile: ${updatedProfile.id}');
} on TdkException catch (error) {
  print([error.code, error.message, error.originalMessage].join('\n'));
} catch (e) {
  print('Error: $e');
}

Share Individual Items

Share specific files or folders (items) with another user instead of sharing the entire profile.

Import

import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';
import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Example

// Must initialize vault before being able to access any of the repositories
await vault.ensureInitialized();

try {
  final granteeDid = 'did:key:recipient-did';

  // 1. Get current permissions policy
  final policy = await vault.getItemPermissionsPolicy(
    profileId: 'my-profile-id',
    granteeDid: granteeDid,
  );

  // 2. Add permission locally
  policy.addPermission(
    ['file-123'], // List of item IDs (files or folders)
    [Permissions.read, Permissions.write],
  );

  // 3. Set the complete policy on the backend
  final kek = await vault.setItemAccess(
    profileId: 'my-profile-id',
    granteeDid: granteeDid,
    policy: policy,
  );

  // 4. Create SharedItemsDto to send to grantee
  final sharedItem = SharedItemsDto(
    kek: kek,
    ownerProfileId: 'my-profile-id',
    ownerProfileDID: 'did:key:owner-did',
    itemIds: ['file-123'],
  );

  // Owner sends SharedItemsDto to grantee (via your application's communication channel)
} on TdkException catch (error) {
  print([error.code, error.message, error.originalMessage].join('\n'));
} catch (e) {
  print('Error: $e');
}

Accept Shared Items

Accept shared items (files/folders) that were granted by another user (grantee side).

Import

import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';
import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Parameters

profileId String Required
ID of the grantee’s profile to add the shared items to.
sharedItems SharedItemsDto Required
The shared item info received from the owner, including its KEK and item IDs.

Example

// Must initialize vault before being able to access any of the repositories
await vault.ensureInitialized();

try {
  // Grantee accepts the shared items received from owner
  final updatedProfile = await vault.acceptSharedItems(
    profileId: 'grantee-profile-id',
    sharedItems: sharedItem, // SharedItemsDto received from owner
  );

  print('Successfully accepted shared items into ${updatedProfile.id}');
} on TdkException catch (error) {
  print([error.code, error.message, error.originalMessage].join('\n'));
} catch (e) {
  print('Error: $e');
}

Share Items with Time-Bound Access

Share items, or an entire profile, with automatic expiration by specifying an expiresAt date/time.

Import

import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';
import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Example

// Must initialize vault before being able to access any of the repositories
await vault.ensureInitialized();

try {
  final granteeDid = 'did:key:recipient-did';

  // Get current permissions policy
  final policy = await vault.getItemPermissionsPolicy(
    profileId: 'my-profile-id',
    granteeDid: granteeDid,
  );

  // Add permission with expiration date
  policy.addPermission(
    ['file-123'],
    [Permissions.read],
    expiresAt: DateTime.now().add(Duration(hours: 1)), // Access valid for 1 hour
  );

  // Set the policy
  final kek = await vault.setItemAccess(
    profileId: 'my-profile-id',
    granteeDid: granteeDid,
    policy: policy,
  );

  print('Time-bound access granted. Expires in 1 hour.');

  // After the expiresAt DateTime passes, access is automatically revoked by the backend
  // The grantee will no longer be able to access the shared item
} on TdkException catch (error) {
  print([error.code, error.message, error.originalMessage].join('\n'));
} catch (e) {
  print('Error: $e');
}

Get Item Access Permissions

Retrieve the current access permissions for shared items.

Import

import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';
import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Parameters

profileId String Required
ID of the profile that owns the items.
granteeDid String Required
DID of the user to get permissions for.

Example

// Must initialize vault before being able to access any of the repositories
await vault.ensureInitialized();

try {
  final granteeDid = 'did:key:recipient-did';

  // Get current item access permissions
  final permissions = await vault.getItemAccess(
    profileId: 'my-profile-id',
    granteeDid: granteeDid,
  );

  permissions.forEach((permission) {
    print('Item: ${permission.itemIds}');
    print('Permissions: ${permission.rights}');
    if (permission.expiresAt != null) {
      print('Expires at: ${permission.expiresAt}');
    }
  });
} on TdkException catch (error) {
  print([error.code, error.message, error.originalMessage].join('\n'));
} catch (e) {
  print('Error: $e');
}

Read Shared Item Content

Read the content of a shared item (file/folder) that another user has shared with you.

Import

import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';
import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Parameters

ownerProfileId String Required
ID of the profile that owns the shared item.
itemId String Required
ID of the item to read.

Example

// Must initialize vault before being able to access any of the repositories
await vault.ensureInitialized();

try {
  // Read content from a shared item
  final content = await vault.readSharedItem(
    ownerProfileId: 'owner-profile-id',
    itemId: 'file-123',
  );

  print('Shared item content: $content');
} on TdkException catch (error) {
  print([error.code, error.message, error.originalMessage].join('\n'));
} catch (e) {
  print('Error: $e');
}

Revoke Profile Access

Revoke access to a profile for a specific user.

Import

import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';
import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Parameters

profileId String Required
ID of the profile to revoke access from.
granteeDid String Required
DID of the user to revoke access from.

Example

// Must initialize vault before being able to access any of the repositories
await vault.ensureInitialized();

try {
  final granteeDid = 'did:key:recipient-did';

  // Revoke profile access from grantee
  await vault.revokeProfileAccess(
    profileId: 'my-profile-id',
    granteeDid: granteeDid,
  );

  print('Profile access revoked successfully');
} on TdkException catch (error) {
  print([error.code, error.message, error.originalMessage].join('\n'));
} catch (e) {
  print('Error: $e');
}

Revoke Item Access

Revoke access to specific items (files/folders) for a user.

Import

import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';
import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Example

// Must initialize vault before being able to access any of the repositories
await vault.ensureInitialized();

try {
  final granteeDid = 'did:key:recipient-did';

  // Get current permissions policy
  final policy = await vault.getItemPermissionsPolicy(
    profileId: 'my-profile-id',
    granteeDid: granteeDid,
  );

  // Remove permission locally
  policy.removePermission(
    ['file-123'],
    [], // Empty list removes all permissions for the specified items
  );

  // Set the updated policy to revoke access
  await vault.setItemAccess(
    profileId: 'my-profile-id',
    granteeDid: granteeDid,
    policy: policy,
  );

  print('Item access revoked successfully');
} on TdkException catch (error) {
  print([error.code, error.message, error.originalMessage].join('\n'));
} catch (e) {
  print('Error: $e');
}

Manage Vault Files

Create folders and upload files to manage documents associated with the vault.

Create Folder

Create a folder in the vault’s profile.

Import

import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';
import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Parameters

folderName String Required
Name of the new folder.
parentFolderId String Required
ID of the parent folder to create this folder in. Pass the profile ID to create it at the root.

Example

// Must initialize vault before being able to access any of the repositories
await vault.ensureInitialized();

// Get the list of available profiles from the vault
var profiles = await vault.listProfiles();
// For demonstration purposes, we always get the last profile from the list
var profile = profiles.lastOrNull;

if (profile != null) {
  // We'll create a folder under a profile (root folder)
  final rootFolderId = profile.id;

  await profile.defaultFileStorage!.createFolder(
    folderName: '<Folder_Name>',
    parentFolderId: rootFolderId,
  );
} else {
  print('Profile not found.');
}

Get Folder

Get the items (files and subfolders) in a vault folder, with pagination support.

Import

import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';
import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Parameters

folderId String Optional
ID of the folder to list. Defaults to the profile’s root folder.
limit int Optional
Maximum number of items to return in this page.
exclusiveStartItemId String Optional
The lastEvaluatedItemId from a previous page, to fetch the next page.

Example

// Must initialize vault before being able to access any of the repositories
await vault.ensureInitialized();

// Get the list of available profiles from the vault
var profiles = await vault.listProfiles();
// For demonstration purposes, we always get the last profile from the list
var profile = profiles.lastOrNull;

if (profile != null) {
  // We'll retrieve the items under a profile (root folder)
  final rootFolderId = profile.id;
  // List items; getFolder returns a PaginatedList
  final page = await profile.defaultFileStorage?.getFolder(folderId: rootFolderId);

  page?.items.forEach((item) {
    print('${item.id} : ${item.name}');
  });

  // page.hasMore is true when page.lastEvaluatedItemId is not null; pass it
  // back as exclusiveStartItemId to fetch the next page.
} else {
  print('Profile not found.');
}

Rename Folder

Rename a folder in the vault’s profile.

Import

import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';
import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Parameters

folderId String Required
ID of the folder to rename.
newName String Required
New name for the folder.

Example

// Must initialize vault before being able to access any of the repositories
await vault.ensureInitialized();

// Get the list of available profiles from the vault
var profiles = await vault.listProfiles();
// For demonstration purposes, we always get the last profile from the list
var profile = profiles.lastOrNull;

if (profile != null) {
  // Rename the folder
  await profile.defaultFileStorage!.renameFolder(
    folderId: '<Folder_ID>',
    newName: '<Folder_Name>',
  );
} else {
  print('Profile not found.');
}

Delete Folder

Delete a folder in the vault’s profile. The folder must be empty.

Import

import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';
import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Parameters

folderId String Required
ID of the folder to delete.

Example

// Must initialize vault before being able to access any of the repositories
await vault.ensureInitialized();

// Get the list of available profiles from the vault
var profiles = await vault.listProfiles();
// For demonstration purposes, we always get the last profile from the list
var profile = profiles.lastOrNull;

if (profile != null) {
  try {
    await profile.defaultFileStorage?.deleteFolder(folderId: '<Folder_ID>');
  } on TdkException catch (error) {
    print([error.code, error.message, error.originalMessage].join('\n'));
  } catch (e) {
    print('Error: $e');
  }
} else {
  print('Profile not found.');
}

Create File

Create a file in a folder in the vault’s profile.

Import

import 'dart:typed_data';

import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';
import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Parameters

fileName String Required
Name of the new file. Must be unique within its parent folder.
data Uint8List Required
File content to upload.
parentFolderId String Optional
ID of the parent folder to create this file in. Defaults to the profile’s root folder.
onSendProgress VaultProgressCallback Optional
Callback invoked with upload progress.

Example

// Must initialize vault before being able to access any of the repositories
await vault.ensureInitialized();

// Get the list of available profiles from the vault
var profiles = await vault.listProfiles();
// For demonstration purposes, we always get the last profile from the list
var profile = profiles.lastOrNull;

if (profile != null) {
  // We'll retrieve the folders under a profile (root folder)
  final rootFolderId = profile.id;
  // Create a dummy file
  final fileContent = Uint8List.fromList([1, 2, 3]);

  try {
    // create a file, we'll create a file in the root folder, instead of subfolder.
    await profile.defaultFileStorage!.createFile(
      fileName: 'Doc Test File 1',
      data: fileContent,
      parentFolderId: rootFolderId,
    );
  } on TdkException catch (error) {
    print([error.code, error.message, error.originalMessage].join('\n'));
  } catch (e) {
    print('Error: $e');
  }
} else {
  print('Profile not found.');
}

Get File

Get the file metadata in the vault’s profile.

Import

import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';
import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Parameters

fileId String Required
ID of the file to retrieve.

Example

// Must initialize vault before being able to access any of the repositories
await vault.ensureInitialized();

// Get the list of available profiles from the vault
var profiles = await vault.listProfiles();
// For demonstration purposes, we always get the last profile from the list
var profile = profiles.lastOrNull;

if (profile != null) {
  // Retrieve the file object
  final file = await profile.defaultFileStorage!.getFile(fileId: '<File_ID>');

  print(file.name);
} else {
  print('Profile not found.');
}

Get File Content

Get the content of a file in the vault’s profile.

Import

import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';
import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Parameters

fileId String Required
ID of the file to read.
onReceiveProgress VaultProgressCallback Optional
Callback invoked with download progress.

Example

// Must initialize vault before being able to access any of the repositories
await vault.ensureInitialized();

// Get the list of available profiles from the vault
var profiles = await vault.listProfiles();
// For demonstration purposes, we always get the last profile from the list
var profile = profiles.lastOrNull;

if (profile != null) {
  // Retrieve the file content
  final fileData = await profile.defaultFileStorage!.getFileContent(
    fileId: '<File_ID>',
  );

  print(fileData);
} else {
  print('Profile not found.');
}

Rename File

Rename a file in the vault’s profile.

Import

import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';
import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Parameters

fileId String Required
ID of the file to rename.
newName String Required
New name for the file. Must be unique within its parent folder.

Example

// Must initialize vault before being able to access any of the repositories
await vault.ensureInitialized();

// Get the list of available profiles from the vault
var profiles = await vault.listProfiles();
// For demonstration purposes, we always get the last profile from the list
var profile = profiles.lastOrNull;

if (profile != null) {
  // Rename the file object
  await profile.defaultFileStorage!.renameFile(
    fileId: '<File_ID>',
    newName: '<File_Name>',
  );
} else {
  print('Profile not found.');
}

Delete File

Delete a file from the vault’s profile.

Import

import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';
import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Parameters

fileId String Required
ID of the file to delete.

Example

// Must initialize vault before being able to access any of the repositories
await vault.ensureInitialized();

// Get the list of available profiles from the vault
var profiles = await vault.listProfiles();
// For demonstration purposes, we always get the last profile from the list
var profile = profiles.lastOrNull;

if (profile != null) {
  try {
    await profile.defaultFileStorage!.deleteFile(fileId: '<File_ID>');
  } on TdkException catch (error) {
    print([error.code, error.message, error.originalMessage].join('\n'));
  } catch (e) {
    print('Error: $e');
  }
} else {
  print('Profile not found.');
}

Manage Vault Credentials

Manage credentials claimed from credential issuers into the vault.

Claim Credential

Claim the credential from the issuer.

Import

import 'dart:typed_data';

import 'package:affinidi_tdk_claim_verifiable_credential/oid4vci_claim_verifiable_credential.dart';
import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';
import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Example

// Must initialize vault before being able to access any of the repositories
await vault.ensureInitialized();

try {
  final keyDerivationPath = "m/44'/60'/0'/0/0";
  final keyPair = await wallet.deriveKey(derivationPath: keyDerivationPath);

  final didDocument = DidKey.generateDocument(keyPair.publicKey);
  final signer = DidSigner(
    didDocument: didDocument,
    didKeyId: didDocument.verificationMethod.first.id,
    keyPair: keyPair,
    signatureScheme: SignatureScheme.ecdsa_secp256k1_sha256,
  );

  // Create a new instance of ClaimVerifiableCredentialService
  final claimVerifiableCredentialService = OID4VCIClaimVerifiableCredentialService(
    didSigner: signer,
  );

  final uri = Uri.parse(
    'https://example.com/callback?credential_offer_uri=https://issuer.example.com/offer/123',
  );

  final context = await claimVerifiableCredentialService.loadCredentialOffer(uri);

  VerifiableCredential? credential;
  String? txCode = '<TX_CODE_FROM_ISSER>';

  // Check if the credential offer is issued with Transaction Code
  if (context.credentialOffer.isTxCodeRequired) {
    // Claim credential with Transaction Code
    // Transaction Code is generated and must be provided by the issuer
    credential = await claimVerifiableCredentialService.claimCredential(
      claimContext: context,
      txCode: txCode,
    );
  } else {
    // Claim credential without Transaction Code
    credential = await claimVerifiableCredentialService.claimCredential(
      claimContext: context,
    );
  }

  print('Credential: $credential');
} on TdkException catch (error) {
  print([error.code, error.message, error.originalMessage].join('\n'));
} catch (e) {
  print('Error: $e');
}

Save Credential

Store a claimed credential into the vault’s profile.

Import

import 'dart:typed_data';

import 'package:affinidi_tdk_claim_verifiable_credential/oid4vci_claim_verifiable_credential.dart';
import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';
import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Parameters

verifiableCredential VerifiableCredential Required
The claimed credential to store.

Example

// Must initialize vault before being able to access any of the repositories
await vault.ensureInitialized();

try {
  // ... claim the credential as shown above, then:
  if (credential.id != null) {
    // Get the list of available profiles from the vault
    var profiles = await vault.listProfiles();
    // For demonstration purposes, we always get the last profile from the list
    var profile = profiles.lastOrNull;
    // do we have a profile selected?
    if (profile != null) {
      // Save credential on the selected vault profile
      await profile.defaultCredentialStorage?.saveCredential(
        verifiableCredential: credential,
      );
    }
  }
} on TdkException catch (error) {
  print([error.code, error.message, error.originalMessage].join('\n'));
} catch (e) {
  print('Error: $e');
}

List Credentials

List credentials from the vault’s profile, with pagination support.

Import

import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';
import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Parameters

limit int Optional
Maximum number of credentials to return in this page.
exclusiveStartItemId String Optional
The lastEvaluatedItemId from a previous page, to fetch the next page.

Example

// Must initialize vault before being able to access any of the repositories
await vault.ensureInitialized();

try {
  // Get the list of available profiles from the vault
  var profiles = await vault.listProfiles();
  // For demonstration purposes, we always get the last profile from the list
  var profile = profiles.lastOrNull;
  // do we have a profile selected?
  if (profile != null) {
    // listCredentials returns a PaginatedList
    final page = await profile.defaultCredentialStorage?.listCredentials();

    print('Credentials: ${page?.items}');
  }
} on TdkException catch (error) {
  print([error.code, error.message, error.originalMessage].join('\n'));
} catch (e) {
  print('Error: $e');
}

Get Credential

Get a credential from the vault’s profile.

Import

import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';
import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Parameters

digitalCredentialId String Required
ID of the credential to retrieve.

Example

// Must initialize vault before being able to access any of the repositories
await vault.ensureInitialized();

try {
  // Get the list of available profiles from the vault
  var profiles = await vault.listProfiles();
  // For demonstration purposes, we always get the last profile from the list
  var profile = profiles.lastOrNull;
  // do we have a profile selected?
  if (profile != null) {
    // Get credential from the selected vault profile
    final credential = await profile.defaultCredentialStorage?.getCredential(
      digitalCredentialId: '<Credential_ID>',
    );

    print('Credential: $credential');
  }
} on TdkException catch (error) {
  print([error.code, error.message, error.originalMessage].join('\n'));
} catch (e) {
  print('Error: $e');
}

Delete Credential

Delete a credential from the vault’s profile.

Import

import 'package:affinidi_tdk_vault_data_manager/affinidi_tdk_vault_data_manager.dart';
import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Parameters

digitalCredentialId String Required
ID of the credential to delete.

Example

// Must initialize vault before being able to access any of the repositories
await vault.ensureInitialized();

try {
  // Get the list of available profiles from the vault
  var profiles = await vault.listProfiles();
  // For demonstration purposes, we always get the last profile from the list
  var profile = profiles.lastOrNull;
  // do we have a profile selected?
  if (profile != null) {
    // Delete credential from the selected vault profile
    await profile.defaultCredentialStorage?.deleteCredential(
      digitalCredentialId: '<Credential_ID>',
    );
  }
} on TdkException catch (error) {
  print([error.code, error.message, error.originalMessage].join('\n'));
} catch (e) {
  print('Error: $e');
}

Back Up and Restore the Vault

Vault backup and restore is the disaster-recovery path for a wallet: a backup is an encrypted snapshot of the wallet’s durable state, the Vault Store’s seed and account index, every local repository that implements Restorable, and any additional named components you explicitly register, such as consent history. Cloud repositories such as VfsProfileRepository are recorded by ID only; their remote data is not copied, and a vault restore reconnects them through their normal configuration instead.

Define a Passphrase Policy

PassphrasePolicy validates the strength of a backup passphrase before createBackup encrypts anything. VaultBackupService uses PassphrasePolicy.standard (minimum 12 characters, requiring an uppercase letter, a number, and a special character) unless you supply your own.

Import

import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Parameters

minLength int Optional
Minimum passphrase length, in Unicode code points. Defaults to 12.
requireUppercase bool Optional
Whether the passphrase must contain an uppercase letter. Defaults to true.
requireNumber bool Optional
Whether the passphrase must contain a digit. Defaults to true.
requireSpecialCharacter bool Optional
Whether the passphrase must contain a non-alphanumeric character. Defaults to true.

Example

const passphrasePolicy = PassphrasePolicy(minLength: 16);

// Check a candidate passphrase without creating a backup.
final violation = passphrasePolicy.validate(candidatePassphraseBytes);
if (violation != null) {
  print('Passphrase rejected: ${violation.code}');
}

Create a Backup

Encrypt and export the vault’s durable state with VaultBackupService.createBackup. The result is a self-contained ByteData you can write to a file or upload to your own storage.

Import

import 'dart:io';
import 'dart:typed_data';

import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Parameters

vault Vault Required
The initialised vault to back up.
passphrase Uint8List Required
The passphrase used to encrypt the backup. Must satisfy the VaultBackupService’s configured passphrasePolicy.

Example

final backupService = VaultBackupService(
  passphrasePolicy: const PassphrasePolicy(minLength: 16),
);
final passphrase = await File(passphraseFilePath).readAsBytes();
late final ByteData backup;
try {
  backup = await backupService.createBackup(
    vault: vault,
    passphrase: passphrase,
  );
} finally {
  passphrase.fillRange(0, passphrase.length, 0);
}

// Native applications can persist the bytes with dart:io.
await File('vault.backup').writeAsBytes(
  backup.buffer.asUint8List(backup.offsetInBytes, backup.lengthInBytes),
);

Restore a Backup

Restore a backup into empty storage with VaultBackupService.restoreBackup. Every restore destination, the VaultStore, each restorable repository, and each named component, must already be empty; restore rejects existing local data and never deletes it automatically.

Repository factories are keyed by the same stable repository ID used when the vault was created. Use ProfileRepositoryRegistration.withBackupData for a repository whose data is part of the backup (it receives the restored VaultStore so it can build its own encryption service before the vault opens normally), and .withoutBackupData for a cloud-backed repository that only needs its stable ID and factory restored, since its data never left the cloud.

Import

import 'dart:io';
import 'dart:typed_data';

import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Parameters

backupData ByteData Required
The encrypted backup previously produced by createBackup.
passphrase Uint8List Required
The passphrase used to encrypt the backup.
vaultStoreFactory FutureOr<VaultStore> Function() Required
Creates the empty destination VaultStore.
repositoryFactories Map<String, ProfileRepositoryRegistration> Required
Repository registrations keyed by repository ID, one per repository recorded in the backup.
namedRestorableFactories Map<String, RestorableFactory> Optional
Factories for named components recorded in the backup, keyed by the same component ID used when the vault was created, for example consentHistory. Defaults to {}.

Repository registrations

Each repositoryFactories value is a ProfileRepositoryRegistration, built with one of two constructors. ProfileRepositoryRegistration.withBackupData registers a repository whose data is included in the backup:

id String Required
The repository ID. Matches both the key in repositoryFactories and the ID recorded in the backup manifest.
factory FutureOr<T> Function(VaultStore vaultStore) Required
Builds the empty destination repository. It receives the restored VaultStore, so a local repository can construct its encryption service before the vault opens.
asRestorable Restorable Function(T repository) Required
Adapts the created repository to a Restorable view. Pass the built-in restorableIdentity helper when the repository already implements Restorable (as EdgeProfileRepository does); supply your own adapter only when it does not.

ProfileRepositoryRegistration.withoutBackupData registers a cloud-backed repository that carries no backup data. It takes the same id and factory, but no asRestorable, since nothing local is restored for it. Both restorableIdentity and both registration constructors come from affinidi_tdk_vault.

Example

final fileBytes = await File('vault.backup').readAsBytes();
final passphrase = await File(passphraseFilePath).readAsBytes();
try {
  final restoredVault = await VaultBackupService().restoreBackup(
    backupData: ByteData.sublistView(fileBytes),
    passphrase: passphrase,
    // Every restore destination must be empty. Use a new persistent
    // VaultStore and repository storage in an application instead of
    // overwriting existing storage.
    vaultStoreFactory: InMemoryVaultStore.new,
    repositoryFactories: {
      'edge': ProfileRepositoryRegistration.withBackupData(
        id: 'edge',
        factory: (vaultStore) => EdgeProfileRepository(
          'edge',
          EdgeDriftRepositoryFactory(database: edgeDatabase),
          EdgeEncryptionService(vaultStore: vaultStore),
        ),
        asRestorable: restorableIdentity,
      ),
      // VFS data remains in cloud storage, so only its stable ID and
      // factory are restored.
      'vfs': ProfileRepositoryRegistration.withoutBackupData(
        id: 'vfs',
        factory: (_) => VfsProfileRepository('vfs'),
      ),
    },
  );
} finally {
  passphrase.fillRange(0, passphrase.length, 0);
}

Recover an Interrupted Restore

Persist your own restore-in-progress flag immediately before calling restoreBackup, and clear it once restore completes. If the flag is still set the next time the app starts, a previous restore was interrupted, for example by a crash or an app kill. Show a recovery interface, and after the user confirms the partial restore may be discarded, call discardInterruptedRestore to clear the leftover state before offering backup selection and retry as a separate action.

discardInterruptedRestore clears named components first, then repositories registered with .withBackupData, then the VaultStore last, and it is safe to call repeatedly. Repositories registered with .withoutBackupData are left completely untouched, since their data lives in the cloud and was never part of the interrupted local restore.

Import

import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Parameters

vaultStoreFactory FutureOr<VaultStore> Function() Required
Creates the same destination VaultStore used by the interrupted restoreBackup call.
repositoryFactories Map<String, ProfileRepositoryRegistration> Required
The same repository registrations passed to the interrupted restoreBackup call.
namedRestorableFactories Map<String, RestorableFactory> Optional
The same named-component factories passed to the interrupted restoreBackup call. Defaults to {}.

Example

if (await restoreState.isInProgress()) {
  await VaultBackupService().discardInterruptedRestore(
    vaultStoreFactory: createVaultStore,
    repositoryFactories: repositoryFactories,
    namedRestorableFactories: namedRestorableFactories,
  );
  await restoreState.clear();
}

Implement a Custom Restorable Component

Register your own storage as a named or repository-scoped component in a Vault backup by implementing Restorable. Extend a class that already implements it, such as VaultStore, where possible; implement the interface directly for a component with no existing Restorable base, such as your own local database wrapper.

Import

import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';

Parameters

export Future<Map<String, dynamic>> Function() Required
Returns a JSON-serialisable map of this component’s own data. Must not include any other component’s data.
validateImport Future<void> Function(Map<String, dynamic> data) Required
Runs every deterministic format and compatibility check import needs, without mutating durable state.
isEmpty Future<bool> Function() Required
Returns whether this component’s destination currently has no existing durable state.
clearAllData Future<void> Function() Required
Deletes all durable state owned by this component. Must be idempotent.
import Future<void> Function(Map<String, dynamic> data) Required
Restores a previously validated payload into an empty destination.
rollbackImport Future<void> Function() Required
Rolls back durable state written by import. Must be a no-op when no import was attempted or the import completed successfully, and must be idempotent.

Example

class MyRestorableComponent implements Restorable {
  static const _schemaVersion = '1.0.0';
  Map<String, dynamic>? _data;
  bool _importPendingRollback = false;

  @override
  Future<Map<String, dynamic>> export() async {
    return {'schemaVersion': _schemaVersion, ...?_data};
  }

  @override
  Future<void> validateImport(Map<String, dynamic> data) async {
    if (data['schemaVersion'] != _schemaVersion) {
      throw VaultRestoreException.invalidBackupFormat();
    }
  }

  @override
  Future<bool> isEmpty() async => _data == null;

  @override
  Future<void> import(Map<String, dynamic> data) async {
    await validateImport(data);
    if (!await isEmpty()) {
      throw VaultRestoreException.destinationNotEmpty('MyRestorableComponent');
    }
    _importPendingRollback = true;
    _data = data;
    _importPendingRollback = false;
  }

  @override
  Future<void> clearAllData() async {
    _data = null;
    _importPendingRollback = false;
  }

  @override
  Future<void> rollbackImport() async {
    if (!_importPendingRollback) return;
    await clearAllData();
  }
}

Register the component through restorableRepositories (keyed by profile repository ID) or namedRestorables (keyed by any component ID you choose) when creating the vault; see Create and Initialise Vault.


affinidi_tdk_vault_iota’s consent records can participate in Vault backups without any custom Restorable code. FlutterSecureConsentStorage, from affinidi_tdk_vault_flutter_utils, implements both ConsentStorage (for IotaConsentRecordService) and Restorable. Register one instance in Vault.fromVaultStore as a named component, and inject the same instance into your IOTA services as ConsentStorage, so the two features share one store.

Import

import 'package:affinidi_tdk_vault/affinidi_tdk_vault.dart';
import 'package:affinidi_tdk_vault_flutter_utils/affinidi_tdk_vault_flutter_utils.dart';
import 'package:affinidi_tdk_vault_iota/affinidi_tdk_vault_iota.dart';

Example

final consentStorage = FlutterSecureConsentStorage();

final vault = await Vault.fromVaultStore(
  vaultStore,
  profileRepositories: profileRepositories,
  // Registering it here includes consent history in every backup and restore.
  namedRestorables: {'consentHistory': consentStorage},
);
await vault.ensureInitialized();

// Inject the same instance into IotaConsentRecordService as its ConsentStorage.
final consentService = IotaConsentRecordService(
  store: consentStorage,
  cryptography: cryptography,
  shareResponseService: responseService,
);

Restoring a backup that includes consent history requires a matching entry in namedRestorableFactories, keyed by the same 'consentHistory' ID:

final restoredVault = await VaultBackupService().restoreBackup(
  backupData: backupData,
  passphrase: passphrase,
  vaultStoreFactory: createVaultStore,
  repositoryFactories: repositoryFactories,
  namedRestorableFactories: {
    'consentHistory': () => FlutterSecureConsentStorage(),
  },
);

Error Handling

All errors are thrown as TdkException, exposing the code that identifies the failure.

CodeThrown byDescription
vault_not_initializedAny profile, sharing, or storage-usage call before ensureInitialized(); Vault.fromVaultStoreThe vault has not been initialised, or the VaultStore passed to fromVaultStore has no seed.
invalid_profile_identifiergetProfileById, readSharedItemNo profile matches the given ID, no profiles exist for the current user, or a shared item’s owner profile cannot be found.
invalid_profile_repository_identifierVault.fromVaultStore, repository lookups, createBackupThe named profile repository does not exist, defaultProfileRepositoryId does not match a supplied repository, or (during createBackup) a registered repository’s key does not match the repository’s own .id.
invalid_file_storage_identifierProfile.defaultFileStorageId setterThe assigned file storage ID does not match any file repository on the profile.
invalid_credential_storage_identifierProfile.defaultCredentialStorageId setterThe assigned credential storage ID does not match any credential repository on the profile.
invalid_shared_storage_identifierProfile.sharedStorage, addSharedStorage, removeSharedStorageThe requested shared storage does not exist, or one with the same ID already exists.
missing_profile_repositoryVault.fromVaultStoreNo profile repositories were supplied.
unsupported_profile_access_sharingshareProfile, addSharedProfile, acceptSharedItems, item permission callsThe profile’s repository does not implement ProfileAccessSharing.
unsupported_profile_storage_usage_reportinggetStorageUsageThe profile’s repository does not support usage reporting.
storage_limit_exceededcreateFile (via VfsProfileRepository)The file exceeds the per-file upload cap, or uploading it would exceed the vault’s cumulative 500 MB storage cap.
request_cancelledAny call passed a cancelTokenThe caller cancelled the operation via its VaultCancelToken.
weak_passphrasecreateBackupThe supplied passphrase fails the configured PassphrasePolicy.
backup_creation_failedcreateBackupAn unexpected error occurred while exporting or encrypting the backup.
invalid_backup_formatcreateBackup, restoreBackup, discardInterruptedRestore, a Restorable implementation’s validateImport/importThe backup is malformed, uses an unsupported schema version, was tampered with, or its repository/named-component topology does not match what was registered.
restore_destination_not_emptyrestoreBackupA restore destination, the VaultStore, a repository, or a named component, already contains data.
restore_rollback_failedrestoreBackup, discardInterruptedRestoreA failed restore, or discardInterruptedRestore, could not fully clean up one or more destinations; the message lists which.