415 lines
17 KiB
Dart
415 lines
17 KiB
Dart
// Integration tests for the CccCrypto public API.
|
|
//
|
|
// These exercise every bridge entry point through the clean Dart façade.
|
|
// All tests require the native library, so they run as integration tests
|
|
// via `flutter test integration_test/ccc_crypto_test.dart -d macos`.
|
|
|
|
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:ccc_cryptography/ccc_cryptography.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:integration_test/integration_test.dart';
|
|
|
|
/// Helper: hex string → Uint8List.
|
|
Uint8List _hex(String hex) {
|
|
final bytes = <int>[];
|
|
for (var i = 0; i < hex.length; i += 2) {
|
|
bytes.add(int.parse(hex.substring(i, i + 2), radix: 16));
|
|
}
|
|
return Uint8List.fromList(bytes);
|
|
}
|
|
|
|
/// Helper: Uint8List → lowercase hex string.
|
|
String _toHex(Uint8List data) =>
|
|
data.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
|
|
|
void main() {
|
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
setUpAll(() async {
|
|
await CccCrypto.init();
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// 5.1 AEAD roundtrip — AES-256-GCM
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
test('5.1 AEAD roundtrip AES-256-GCM', () async {
|
|
final key = Uint8List(32); // all zeros — fine for a test
|
|
final nonce = Uint8List(12);
|
|
final plaintext = Uint8List(1024); // 1 KB of zeros
|
|
for (var i = 0; i < plaintext.length; i++) {
|
|
plaintext[i] = i & 0xff;
|
|
}
|
|
|
|
final ciphertext = await CccCrypto.encryptAead(
|
|
algorithm: CccAeadAlgorithm.aesGcm256,
|
|
key: key,
|
|
nonce: nonce,
|
|
plaintext: plaintext,
|
|
);
|
|
|
|
// Ciphertext must be longer than plaintext (appended auth tag).
|
|
expect(ciphertext.length, greaterThan(plaintext.length));
|
|
|
|
final decrypted = await CccCrypto.decryptAead(
|
|
algorithm: CccAeadAlgorithm.aesGcm256,
|
|
key: key,
|
|
nonce: nonce,
|
|
ciphertext: ciphertext,
|
|
);
|
|
|
|
expect(decrypted, equals(plaintext));
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// 5.2 AEAD roundtrip — ChaCha20-Poly1305
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
test('5.2 AEAD roundtrip ChaCha20-Poly1305', () async {
|
|
final key = Uint8List(32);
|
|
final nonce = Uint8List(12);
|
|
final plaintext = Uint8List(1024);
|
|
for (var i = 0; i < plaintext.length; i++) {
|
|
plaintext[i] = (i * 3) & 0xff;
|
|
}
|
|
|
|
final ciphertext = await CccCrypto.encryptAead(
|
|
algorithm: CccAeadAlgorithm.chaCha20Poly1305,
|
|
key: key,
|
|
nonce: nonce,
|
|
plaintext: plaintext,
|
|
);
|
|
|
|
expect(ciphertext.length, greaterThan(plaintext.length));
|
|
|
|
final decrypted = await CccCrypto.decryptAead(
|
|
algorithm: CccAeadAlgorithm.chaCha20Poly1305,
|
|
key: key,
|
|
nonce: nonce,
|
|
ciphertext: ciphertext,
|
|
);
|
|
|
|
expect(decrypted, equals(plaintext));
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// 5.3 AEAD tamper test — modifying ciphertext fails authentication
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
test('5.3 AEAD tamper test throws CccAuthenticationFailed', () async {
|
|
final key = Uint8List(32);
|
|
final nonce = Uint8List(12);
|
|
final plaintext = Uint8List.fromList(utf8.encode('Hello, tamper test!'));
|
|
|
|
final ciphertext = await CccCrypto.encryptAead(
|
|
algorithm: CccAeadAlgorithm.aesGcm256,
|
|
key: key,
|
|
nonce: nonce,
|
|
plaintext: plaintext,
|
|
);
|
|
|
|
// Flip a byte in the ciphertext body (not the tag).
|
|
final tampered = Uint8List.fromList(ciphertext);
|
|
tampered[0] ^= 0xff;
|
|
|
|
expect(
|
|
() => CccCrypto.decryptAead(
|
|
algorithm: CccAeadAlgorithm.aesGcm256,
|
|
key: key,
|
|
nonce: nonce,
|
|
ciphertext: tampered,
|
|
),
|
|
throwsA(isA<CccAuthenticationFailed>()),
|
|
);
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// 5.4 KDF known-vector — HKDF-SHA256 (RFC 5869 Test Case 1)
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
test('5.4 KDF HKDF-SHA256 RFC 5869 Test Case 1', () async {
|
|
final ikm = _hex('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b');
|
|
final salt = _hex('000102030405060708090a0b0c');
|
|
final info = _hex('f0f1f2f3f4f5f6f7f8f9');
|
|
const length = 42;
|
|
final expectedOkm = _hex(
|
|
'3cb25f25faacd57a90434f64d0362f2a'
|
|
'2d2d0a90cf1a5a4c5db02d56ecc4c5bf'
|
|
'34007208d5b887185865',
|
|
);
|
|
|
|
final okm = await CccCrypto.deriveKey(
|
|
algorithm: CccKdfAlgorithm.sha256,
|
|
ikm: ikm,
|
|
salt: salt,
|
|
info: info,
|
|
length: length,
|
|
);
|
|
|
|
expect(okm.length, equals(length));
|
|
expect(_toHex(okm), equals(_toHex(expectedOkm)));
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// 5.5 MAC roundtrip — HMAC-SHA256 compute then verify
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
test('5.5 MAC roundtrip HMAC-SHA256', () async {
|
|
final key = Uint8List(32);
|
|
for (var i = 0; i < key.length; i++) {
|
|
key[i] = i & 0xff;
|
|
}
|
|
final data = Uint8List.fromList(utf8.encode('MAC roundtrip test data'));
|
|
|
|
final mac = await CccCrypto.computeMac(
|
|
algorithm: CccMacAlgorithm.hmacSha256,
|
|
key: key,
|
|
data: data,
|
|
);
|
|
|
|
// HMAC-SHA256 produces 32-byte tag.
|
|
expect(mac.length, equals(32));
|
|
|
|
final valid = await CccCrypto.verifyMac(
|
|
algorithm: CccMacAlgorithm.hmacSha256,
|
|
key: key,
|
|
data: data,
|
|
mac: mac,
|
|
);
|
|
|
|
expect(valid, isTrue);
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// 5.6 MAC tamper test — modified data fails verification
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
test('5.6 MAC tamper test returns false', () async {
|
|
final key = Uint8List(32);
|
|
final data = Uint8List.fromList(utf8.encode('original data'));
|
|
|
|
final mac = await CccCrypto.computeMac(
|
|
algorithm: CccMacAlgorithm.hmacSha256,
|
|
key: key,
|
|
data: data,
|
|
);
|
|
|
|
// Modify the data after MAC was computed.
|
|
final tamperedData = Uint8List.fromList(utf8.encode('tampered data'));
|
|
|
|
final valid = await CccCrypto.verifyMac(
|
|
algorithm: CccMacAlgorithm.hmacSha256,
|
|
key: key,
|
|
data: tamperedData,
|
|
mac: mac,
|
|
);
|
|
|
|
expect(valid, isFalse);
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// 5.7 Hash known-vector — SHA-256 / SHA-384 / SHA-512
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
test('5.7a Hash SHA-256 known vector', () async {
|
|
final data = Uint8List.fromList(utf8.encode('abc'));
|
|
final expected = _hex(
|
|
'ba7816bf8f01cfea414140de5dae2223'
|
|
'b00361a396177a9cb410ff61f20015ad',
|
|
);
|
|
|
|
final digest = await CccCrypto.hash(
|
|
algorithm: CccHashAlgorithm.sha256,
|
|
data: data,
|
|
);
|
|
|
|
expect(digest.length, equals(32));
|
|
expect(_toHex(digest), equals(_toHex(expected)));
|
|
});
|
|
|
|
test('5.7b Hash SHA-384 known vector', () async {
|
|
final data = Uint8List.fromList(utf8.encode('abc'));
|
|
final expected = _hex(
|
|
'cb00753f45a35e8bb5a03d699ac65007'
|
|
'272c32ab0eded1631a8b605a43ff5bed'
|
|
'8086072ba1e7cc2358baeca134c825a7',
|
|
);
|
|
|
|
final digest = await CccCrypto.hash(
|
|
algorithm: CccHashAlgorithm.sha384,
|
|
data: data,
|
|
);
|
|
|
|
expect(digest.length, equals(48));
|
|
expect(_toHex(digest), equals(_toHex(expected)));
|
|
});
|
|
|
|
test('5.7c Hash SHA-512 known vector', () async {
|
|
final data = Uint8List.fromList(utf8.encode('abc'));
|
|
final expected = _hex(
|
|
'ddaf35a193617abacc417349ae204131'
|
|
'12e6fa4e89a97ea20a9eeee64b55d39a'
|
|
'2192992a274fc1a836ba3c23a3feebbd'
|
|
'454d4423643ce80e2a9ac94fa54ca49f',
|
|
);
|
|
|
|
final digest = await CccCrypto.hash(
|
|
algorithm: CccHashAlgorithm.sha512,
|
|
data: data,
|
|
);
|
|
|
|
expect(digest.length, equals(64));
|
|
expect(_toHex(digest), equals(_toHex(expected)));
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// 5.8 KEM roundtrip — X25519
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
test('5.8 KEM roundtrip X25519', () async {
|
|
final kp = await CccCrypto.kemGenerateKeypair(
|
|
algorithm: CccKemAlgorithm.x25519,
|
|
);
|
|
|
|
expect(kp.publicKey.length, equals(32));
|
|
expect(kp.privateKey.length, equals(32));
|
|
|
|
final encap = await CccCrypto.kemEncapsulate(
|
|
algorithm: CccKemAlgorithm.x25519,
|
|
publicKey: kp.publicKey,
|
|
);
|
|
|
|
expect(encap.sharedSecret.length, equals(32));
|
|
expect(encap.ciphertext.length, equals(32));
|
|
|
|
final decapSecret = await CccCrypto.kemDecapsulate(
|
|
algorithm: CccKemAlgorithm.x25519,
|
|
privateKey: kp.privateKey,
|
|
ciphertext: encap.ciphertext,
|
|
);
|
|
|
|
expect(decapSecret, equals(encap.sharedSecret));
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// 5.9 KEM roundtrip — X448
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
test('5.9 KEM roundtrip X448', () async {
|
|
final kp = await CccCrypto.kemGenerateKeypair(
|
|
algorithm: CccKemAlgorithm.x448,
|
|
);
|
|
|
|
expect(kp.publicKey.length, equals(56));
|
|
expect(kp.privateKey.length, equals(56));
|
|
|
|
final encap = await CccCrypto.kemEncapsulate(
|
|
algorithm: CccKemAlgorithm.x448,
|
|
publicKey: kp.publicKey,
|
|
);
|
|
|
|
expect(encap.sharedSecret.length, equals(56));
|
|
expect(encap.ciphertext.length, equals(56));
|
|
|
|
final decapSecret = await CccCrypto.kemDecapsulate(
|
|
algorithm: CccKemAlgorithm.x448,
|
|
privateKey: kp.privateKey,
|
|
ciphertext: encap.ciphertext,
|
|
);
|
|
|
|
expect(decapSecret, equals(encap.sharedSecret));
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// 5.10 Provider catalog
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
test('5.10 Provider catalog', () async {
|
|
final providers = CccCrypto.listProviders();
|
|
expect(providers, isNotEmpty);
|
|
expect(providers, contains('wolfssl'));
|
|
|
|
final catalog = await CccCrypto.getCapabilities();
|
|
expect(catalog.providerName, equals('wolfssl'));
|
|
|
|
// Each category should have at least one algorithm.
|
|
expect(catalog.aead, isNotEmpty);
|
|
expect(catalog.kdf, isNotEmpty);
|
|
expect(catalog.mac, isNotEmpty);
|
|
expect(catalog.hash, isNotEmpty);
|
|
expect(catalog.kem, isNotEmpty);
|
|
|
|
// Check that availability and scores are populated.
|
|
for (final algo in catalog.availableAead) {
|
|
expect(algo.name, isNotEmpty);
|
|
expect(algo.efficiencyScore, greaterThanOrEqualTo(0));
|
|
expect(algo.reliabilityScore, greaterThanOrEqualTo(0));
|
|
}
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// 5.11 Self-test validation
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
test('5.11 Self-test validation', () async {
|
|
final result = await CccCrypto.runSelfTest();
|
|
|
|
expect(result.providerName, equals('wolfssl'));
|
|
expect(result.results, isNotEmpty);
|
|
expect(result.allPassed, isTrue,
|
|
reason: 'Failures: ${result.failures.map((r) => '${r.algoName}: ${r.errorMessage}').join(', ')}');
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// 5.12 Error handling — typed exceptions
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
test('5.12a Error: invalid key length throws CccInvalidKey', () async {
|
|
// AES-256-GCM requires 32-byte key; provide 16 bytes.
|
|
final shortKey = Uint8List(16);
|
|
final nonce = Uint8List(12);
|
|
final plaintext = Uint8List.fromList(utf8.encode('test'));
|
|
|
|
expect(
|
|
() => CccCrypto.encryptAead(
|
|
algorithm: CccAeadAlgorithm.aesGcm256,
|
|
key: shortKey,
|
|
nonce: nonce,
|
|
plaintext: plaintext,
|
|
),
|
|
throwsA(isA<CccException>()),
|
|
);
|
|
});
|
|
|
|
test('5.12b Error: invalid nonce length throws CccException', () async {
|
|
final key = Uint8List(32);
|
|
// AES-GCM nonce should be 12 bytes; provide 8.
|
|
final shortNonce = Uint8List(8);
|
|
final plaintext = Uint8List.fromList(utf8.encode('test'));
|
|
|
|
expect(
|
|
() => CccCrypto.encryptAead(
|
|
algorithm: CccAeadAlgorithm.aesGcm256,
|
|
key: key,
|
|
nonce: shortNonce,
|
|
plaintext: plaintext,
|
|
),
|
|
throwsA(isA<CccException>()),
|
|
);
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// 5.13 Algorithm ID mapping — enum discriminants match Rust repr(u32)
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
test('5.13 Algorithm ID mapping', () async {
|
|
// Verify the catalog reports algorithm IDs matching Rust enum discriminants.
|
|
final catalog = await CccCrypto.getCapabilities();
|
|
|
|
// AEAD: AesGcm256 = 12, ChaCha20Poly1305 = 13, XChaCha20Poly1305 = 14
|
|
final aeadIds = {for (final a in catalog.aead) a.name: a.id};
|
|
expect(aeadIds['AES-256-GCM'], equals(12));
|
|
expect(aeadIds['ChaCha20-Poly1305'], equals(13));
|
|
|
|
// Hash: Sha256 = 40, Sha384 = 41, Sha512 = 42
|
|
final hashIds = {for (final a in catalog.hash) a.name: a.id};
|
|
expect(hashIds['SHA-256'], equals(40));
|
|
expect(hashIds['SHA-384'], equals(41));
|
|
expect(hashIds['SHA-512'], equals(42));
|
|
|
|
// KEM: X25519 = 50, X448 = 51
|
|
final kemIds = {for (final a in catalog.kem) a.name: a.id};
|
|
expect(kemIds['X25519'], equals(50));
|
|
expect(kemIds['X448'], equals(51));
|
|
});
|
|
}
|