Affinidi TDK - Vault

The Affinidi TDK Vault package provides libraries and tools for embedding Affinidi Vault into your existing Dart applications. It implements the Self-Sovereign Identity (SSI), a fundamental concept of managing digital identities in a decentralised manner.

It allows you to manage multiple profiles representing your digital identity based on different contexts, including the data related to the profile stored in secure cloud storage; for example, you can have an identity for shopping, banking, or work.

Requirements

  • Dart SDK version ^3.6.0

Installation

Run:

dart pub add affinidi_tdk_vault

or manually add the package to your pubspec.yaml file:

dependencies:
  affinidi_tdk_vault: ^<version_number>

Usage

After successfully installing the package, import it into your code.
(Note: the sample code below requires the installation of affinidi_tdk_vault_data_manager as a dependency.)

import 'dart:typed_data';

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

void main() async {
  // Initialise InMemory storage
  final accountIndex = 0;
  final vaultStore = InMemoryVaultStore();
  await vaultStore.writeAccountIndex(accountIndex);

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

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

  // Create vault from vault store
  final vault = await Vault.fromVaultStore(
    keyStorage,
    profileRepositories: profileRepositories,
    defaultProfileRepositoryId: vfsRepositoryId,
  );

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

Note: The example above uses VfsProfileRepository from the affinidi_tdk_vault_data_manager package to enable cloud storage functionality. If you only need local storage or custom implementations, you can use just the core vault package.

Backup and restore

Vault backups contain durable state from the VaultStore, local repositories that implement Restorable, and explicitly registered named components. Cloud repositories are recorded by ID but their remote data is not copied; after the wallet is restored they reconnect through their normal configuration.

The core API uses ByteData, keeping file access platform-independent:

import 'dart:io';

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 factories are keyed by the same stable repository and component IDs used when the Vault was created. Repository factories receive the restored VaultStore, allowing local repositories to construct their encryption services before the Vault is opened normally:

final fileBytes = await File('vault.backup').readAsBytes();
final passphrase = await File(passphraseFilePath).readAsBytes();
try {
  final restoredVault = await backupService.restoreBackup(
    backupData: ByteData.sublistView(fileBytes),
    passphrase: passphrase,
    vaultStoreFactory: createVaultStore,
    repositoryFactories: {
      'edge': ProfileRepositoryRegistration.withBackupData(
        id: 'edge',
        factory: createEdgeRepository,
        asRestorable: restorableIdentity,
      ),
      'cloud': ProfileRepositoryRegistration.withoutBackupData(
        id: 'cloud',
        factory: (_) => createCloudRepository(),
      ),
    },
  );
} finally {
  passphrase.fillRange(0, passphrase.length, 0);
}

All repository and named-component factories are validated before wallet state is written. Every destination VaultStore, local repository, and named component must be empty; restore rejects existing local data and never deletes it automatically. Concurrent restores through the same VaultBackupService instance are serialized so those emptiness checks and imports do not overlap. Unknown IDs, incompatible versions, incorrect passphrases, and tampered backups are also rejected. Avoid targeting the same persistence layer from multiple service instances at the same time.

Applications should persist their own restore-in-progress flag immediately before calling restoreBackup and clear it after restore completes. If the flag remains set on the next app start, show a recovery interface before opening or mutating the destination. After the user confirms that the partial restore may be deleted, discard all registered local restore state:

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

discardInterruptedRestore is intentionally destructive. It clears named components and local repositories before the VaultStore, leaves cloud and other non-restorable repositories untouched, and can safely be called again if the app is interrupted during cleanup. Clear the application flag only after cleanup succeeds. Backup selection and retry should be a separate subsequent action.

Platform-specific state can participate without coupling its domain package to Vault. For example, create one FlutterSecureConsentStorage, register it in Vault.fromVaultStore as consentHistory, and inject the same instance into IOTA services as ConsentStorage.

Profile Sharing

The Vault package supports sharing profiles and individual items (files/folders) with other users. This enables collaborative access to data while maintaining security through encryption.

Sharing a Profile

To share an entire profile with another user:

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

// Owner sends SharedProfileDto to grantee 
// Grantee accepts the shared profile
await vault.addSharedProfile(
  profileId: 'grantee-profile-id',
  sharedProfile: sharedProfile,
);

Sharing Individual Items

To share specific files or folders (items) with another user:

// Owner shares a file with a grantee
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'], [Permissions.read, Permissions.write]);

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

// 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 
// Grantee accepts the shared item
await vault.acceptSharedItems(
  profileId: 'grantee-profile-id',
  sharedItem: sharedItem,
);

Time-Bound Sharing

To share items with automatic expiration (time-bound access), specify an expiresAt DateTime when adding permissions:

// Owner shares a file with time-bound access (expires at a specific date/time)
final granteeDid = 'did:key:recipient-did';
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 until 1 hour from now
);

// After the expiresAt DateTime passes, access is automatically revoked by the backend
// The grantee will no longer be able to access the shared item

Note:

  • Time-bound sharing is only supported for item-level sharing (files/folders), not for profile-level sharing.
  • If expiresAt is not provided, access is unlimited (default behavior).
  • expiresAt can be in any timezone, it will be automatically converted to UTC before sending to the backend.
  • expiresAt can be set to any date (past or future). Applications should enforce UI restrictions if needed.
  • Once the expiration date/time passes, the backend automatically revokes access.

Revoking Access

To revoke access to a profile or item:

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

// Revoke item access from grantee 
final policy = await vault.getItemPermissionsPolicy(
  profileId: 'my-profile-id',
  granteeDid: granteeDid,
);

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

// Set complete policy
await vault.setItemAccess(
  profileId: 'my-profile-id',
  granteeDid: granteeDid,
  policy: policy,
);

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

For more sample usage, go to the example folder, including:

Support & feedback

If you face any issues or have suggestions, please don't hesitate to contact us using this link.

Reporting technical issues

If you have a technical issue with the package's codebase, you can also create an issue directly in GitHub.

  1. Ensure the bug was not already reported by searching on GitHub under Issues.

  2. If you're unable to find an open issue addressing the problem, open a new one. Be sure to include a title and clear description, as much relevant information as possible, and a code sample or an executable test case demonstrating the expected behaviour that is not occurring.

Contributing

Want to contribute?

Head over to our CONTRIBUTING guidelines.

Libraries

affinidi_tdk_vault
This library allows managing Vault instances and their profiles