encode method

Future<String> encode(
  1. String plaintext,
  2. String passPhrase, {
  3. HashingAlgoType hashingAlgoType = HashingAlgoType.argon2id,
  4. HashParams? hashParams,
  5. bool legacyUnsaltedDerivation = false,
})

Encrypts plaintext with a key derived from passPhrase and returns the envelope as a JSON string.

Set legacyUnsaltedDerivation only to produce a file an older client can still open. It writes the pre-v form, whose derivation is deterministic in the passphrase — see the class doc.

Implementation

Future<String> encode(
  String plaintext,
  String passPhrase, {
  HashingAlgoType hashingAlgoType = HashingAlgoType.argon2id,
  HashParams? hashParams,
  bool legacyUnsaltedDerivation = false,
}) async {
  if (legacyUnsaltedDerivation) {
    final aes = await _encryptorForPassphrase(passPhrase, hashingAlgoType,
        hashParams: hashParams);
    final iv = InitialisationVector.random(16);
    return jsonEncode({
      'content': aes.encrypt(plaintext, iv: iv),
      'iv': base64Encode(iv.ivBytes),
      'hashingAlgoType': hashingAlgoType.name,
    });
  }

  if (hashingAlgoType != HashingAlgoType.argon2id) {
    throw AtException('a version $currentVersion envelope is Argon2id only; '
        '${hashingAlgoType.name} is a single unsalted pass and gives a '
        'passphrase almost no protection, so it can be read for old files '
        'but never written');
  }

  final salt = InitialisationVector.random(saltLength).ivBytes;
  final params = _expectArgonHashParams(hashParams) ??
      ArgonHashParams.owaspMinimum(salt: salt);
  // A caller-supplied params object still gets this envelope's salt: the two
  // have to agree, and the salt is generated here.
  params.salt = salt;

  // The envelope persists the salt and the three costs, and `decode` reads
  // each of them back; `hashLength` is not among them, so decode always
  // derives at whatever ArgonHashParams itself defaults to. Writing a file
  // under a different length would therefore produce one that cannot be
  // opened, reported as an incorrect passphrase rather than as the
  // parameter that actually differed — so refuse it here, where the reason
  // is still known. Persisting it instead would add a field nothing in this
  // tree ever varies, and a stored parameter no caller sets is a format
  // that cannot be exercised.
  final lengthDecodeWillUse = ArgonHashParams().hashLength;
  if (params.hashLength != lengthDecodeWillUse) {
    throw AtException(
        'a version $currentVersion envelope does not persist hashLength, so '
        'it is always read back at $lengthDecodeWillUse bytes; encoding at '
        '${params.hashLength} would write a file that fails to open and '
        'blames the passphrase');
  }

  final aes = await _encryptorForPassphrase(passPhrase, hashingAlgoType,
      hashParams: params);
  final iv = InitialisationVector.random(16);
  return jsonEncode({
    'v': currentVersion,
    'content': aes.encrypt(plaintext, iv: iv),
    'iv': base64Encode(iv.ivBytes),
    'hashingAlgoType': hashingAlgoType.name,
    'salt': base64Encode(salt),
    'kdfParams': {
      'memory': params.memory,
      'iterations': params.iterations,
      'parallelism': params.parallelism,
    },
  });
}