decryptSymmetricCredential function

CrossmintVerifiableCredential decryptSymmetricCredential(
  1. CrossmintEncryptedVerifiableCredential credential,
  2. Uint8List secretKey
)

Decrypts an AES-256-GCM encrypted Verifiable Credential.

The encrypted payload is base64-encoded with the following layout: IV (12 bytes) + auth tag (16 bytes) + ciphertext

secret is the 256-bit key as a hex string or base64 string.

Throws FormatException if the decrypted data is not a valid VC.

Implementation

CrossmintVerifiableCredential decryptSymmetricCredential(
  CrossmintEncryptedVerifiableCredential credential,
  Uint8List secretKey,
) {
  final encryptedBytes = base64Decode(credential.payload);

  // Layout: IV (12) + AuthTag (16) + Ciphertext
  if (encryptedBytes.length < 28) {
    throw const FormatException('Encrypted payload too short');
  }

  final iv = Uint8List.fromList(encryptedBytes.sublist(0, 12));
  final authTag = Uint8List.fromList(encryptedBytes.sublist(12, 28));
  final ciphertext = Uint8List.fromList(encryptedBytes.sublist(28));

  // AES-256-GCM decryption
  final cipher = GCMBlockCipher(AESEngine())
    ..init(
      false, // decrypt
      AEADParameters(
        KeyParameter(secretKey),
        authTag.length * 8, // tag length in bits
        iv,
        Uint8List(0), // no AAD
      ),
    );

  // Append auth tag to ciphertext for pointycastle's GCM implementation
  final input = Uint8List(ciphertext.length + authTag.length)
    ..setRange(0, ciphertext.length, ciphertext)
    ..setRange(ciphertext.length, ciphertext.length + authTag.length, authTag);

  final decrypted = Uint8List(cipher.getOutputSize(input.length));
  final len = cipher.processBytes(input, 0, input.length, decrypted, 0);
  cipher.doFinal(decrypted, len);

  final plaintext = utf8.decode(decrypted.sublist(0, len));
  final Object? json = jsonDecode(plaintext);

  if (json is! Map<String, Object?>) {
    throw const FormatException('Decrypted data is not a JSON object');
  }

  final vc = tryParseVerifiableCredential(json);
  if (vc == null) {
    throw const FormatException(
      'Decrypted data is not a valid Verifiable Credential',
    );
  }

  return vc;
}