decryptSymmetricCredential function
CrossmintVerifiableCredential
decryptSymmetricCredential(
- CrossmintEncryptedVerifiableCredential credential,
- 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;
}