initCryptoIdentity method

Future<String> initCryptoIdentity({
  1. String? passphrase,
  2. String? reuseExistingStorageRecoveryKeyOrPassphrase,
  3. String? keyIdentifier,
  4. bool selfSign = true,
  5. bool wipeSecureStorage = true,
  6. bool wipeKeyBackup = true,
  7. bool wipeCrossSigning = true,
  8. bool setupMasterKey = true,
  9. bool setupSelfSigningKey = true,
  10. bool setupUserSigningKey = true,
  11. bool setupOnlineKeyBackup = true,
  12. String? keyName,
})

Bootstraps a crypto identity for the client. Creates secret storage and cross-signing keys and optionally online key backup. Returns the recovery key for the secret storage key in use — newly generated, or the existing key when reuseExistingStorageRecoveryKeyOrPassphrase is set.

passphrase lets users remember a human-readable phrase from which a new recovery key is derived using PBKDF2. It must not be combined with reuseExistingStorageRecoveryKeyOrPassphrase.

When reuseExistingStorageRecoveryKeyOrPassphrase is set, the existing secret storage key is kept and unlocked with that credential (secure storage is never wiped). Use this to heal a partially provisioned identity without invalidating the user's recovery key. keyIdentifier selects a specific key when several exist. selfSign then signs this device with the unlocked secrets when cross-signing is available.

When wipeSecureStorage or wipeKeyBackup or wipeCrossSigning are true, existing data is wiped during setup. wipeSecureStorage is ignored when reusing an existing storage key. The setup* flags control which cross-signing keys and key backup are provisioned. keyName can label a newly generated secret storage key.

Throws BootstrapBadStateException when reuse is requested but there is no usable existing secret storage key, or bootstrap would otherwise create a new key. Callers should fall back to a destructive initCryptoIdentity() without the reuse parameter.

Implementation

Future<String> initCryptoIdentity({
  String? passphrase,
  String? reuseExistingStorageRecoveryKeyOrPassphrase,
  String? keyIdentifier,
  bool selfSign = true,
  bool wipeSecureStorage = true,
  bool wipeKeyBackup = true,
  bool wipeCrossSigning = true,
  bool setupMasterKey = true,
  bool setupSelfSigningKey = true,
  bool setupUserSigningKey = true,
  bool setupOnlineKeyBackup = true,
  String? keyName,
}) async {
  final encryption = this.encryption;
  if (encryption == null) {
    throw Exception('End to end encryption not available!');
  }

  final reuseCredential = reuseExistingStorageRecoveryKeyOrPassphrase;
  final reuseExisting = reuseCredential != null;
  if (reuseExisting && passphrase != null) {
    throw ArgumentError(
      'Cannot set both passphrase and reuseExistingStorageRecoveryKeyOrPassphrase.',
    );
  }

  if (reuseExisting) {
    final ssss = encryption.ssss;
    final keyToValidate = keyIdentifier ?? ssss.defaultKeyId;
    if (keyToValidate == null || !ssss.isKeyValid(keyToValidate)) {
      throw BootstrapBadStateException(
        'No usable existing secret storage key. Use `Client.initCryptoIdentity()` without reuse.',
      );
    }
    // Reuse mode never replaces the secret storage key or wipes existing components.
    wipeSecureStorage = false;
    wipeCrossSigning = false;
    wipeKeyBackup = false;
  }

  String? recoveryKey;
  OpenSSSS? openedSsss;
  final completer = Completer();
  encryption.bootstrap(
    onUpdate: (bootstrap) async {
      try {
        recoveryKey ??= bootstrap.newSsssKey?.recoveryKey;
        switch (bootstrap.state) {
          case BootstrapState.loading:
            break;
          case BootstrapState.askWipeSsss:
            bootstrap.wipeSsss(wipeSecureStorage);
            break;
          case BootstrapState.askUseExistingSsss:
            bootstrap.useExistingSsss(
              reuseExisting,
              keyIdentifier: keyIdentifier,
            );
            break;
          case BootstrapState.askUnlockSsss:
            if (reuseExisting) {
              throw BootstrapBadStateException(
                'Cannot reuse existing storage from ${bootstrap.state}; use `Client.initCryptoIdentity()` without reuse.',
              );
            }
            bootstrap.unlockedSsss();
            break;
          case BootstrapState.askBadSsss:
            if (reuseExisting) {
              throw BootstrapBadStateException(
                'Cannot reuse existing storage from ${bootstrap.state}; use `Client.initCryptoIdentity()` without reuse.',
              );
            }
            bootstrap.ignoreBadSecrets(true);
            break;
          case BootstrapState.askWipeCrossSigning:
            await bootstrap.wipeCrossSigning(wipeCrossSigning);
            break;
          case BootstrapState.askWipeOnlineKeyBackup:
            bootstrap.wipeOnlineKeyBackup(wipeKeyBackup);
            break;
          case BootstrapState.askSetupOnlineKeyBackup:
            await bootstrap.askSetupOnlineKeyBackup(setupOnlineKeyBackup);
            break;
          case BootstrapState.askSetupCrossSigning:
            await bootstrap.askSetupCrossSigning(
              setupMasterKey: setupMasterKey,
              setupSelfSigningKey: setupSelfSigningKey,
              setupUserSigningKey: setupUserSigningKey,
              selfSign: selfSign,
            );
            break;
          case BootstrapState.askNewSsss:
            if (reuseExisting) {
              throw BootstrapBadStateException(
                'Cannot reuse existing storage from ${bootstrap.state}; use `Client.initCryptoIdentity()` without reuse.',
              );
            }
            await bootstrap.newSsss(passphrase, keyName);
            break;
          case BootstrapState.openExistingSsss:
            if (!reuseExisting) {
              throw Exception(
                'Bootstrap state ${bootstrap.state} should not happen!',
              );
            }
            await bootstrap.newSsssKey!.unlock(
              keyOrPassphrase: reuseCredential,
            );
            recoveryKey ??= bootstrap.newSsssKey?.recoveryKey;
            openedSsss = bootstrap.newSsssKey;
            await bootstrap.openExistingSsss();
            break;
          case BootstrapState.error:
            throw bootstrap.errorResult ?? Exception('Bootstrap error!');
          case BootstrapState.done:
            completer.complete();
            break;
        }
      } catch (e, s) {
        if (completer.isCompleted) {
          return Logs().e('Bootstrap error after completed', e, s);
        }
        return completer.completeError(e, s);
      }
    },
  );

  await completer.future;

  if (reuseExisting && selfSign && (encryption.crossSigning.enabled)) {
    await encryption.crossSigning.selfSign(
      keyOrPassphrase: reuseCredential,
      openSsss: openedSsss,
    );
  }

  return recoveryKey!;
}