Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions example/webcrypto_demo_flutter_app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,10 @@ To run the Android JNI/JCA AES-GCM smoke test:
flutter test integration_test/jni_aesgcm_test.dart \
-d emulator-name
```

To run the Android JNI/JCA AES-CBC smoke test:

```sh
flutter test integration_test/jni_aescbc_test.dart \
-d emulator-name
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import 'dart:convert';

import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:webcrypto/webcrypto.dart';

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

testWidgets('public API AES-128-CBC encryption matches known vector', (
_,
) async {
final key = await AesCbcSecretKey.importRawKey(
base64Decode('nJ0IrxKwen1VN2/rfLsmmA=='),
);
final ciphertext = await key.encryptBytes(
base64Decode(
'dmVzdGlidWx1bSBsdWN0dXMgZGlhbSwgcXVpcwppbnRlcmR1bSBsZW8gYWxpcXVh'
'bSBhYy4gTnVuYyBhYyBtaSBpbiBs',
),
base64Decode('AAEECRAZJDFAUWR5kKnE4Q=='),
);

expect(
base64Encode(ciphertext),
'MlBdzmsDQSRORkwayz7U9P7v87lgsVRRTrWsZi3qnWiqTW+m6K3KRQ4B1I1u+W7r'
'/kBCBQt404253SV0DeIHNe/HUesVja7CB5jvJUQ6GmQ=',
);
});
}
173 changes: 166 additions & 7 deletions lib/src/impl_jni/impl_jni.aescbc.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2020 Google LLC
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -14,18 +14,177 @@

part of 'impl_jni.dart';

const _aesCbcTransformation = 'AES/CBC/PKCS5Padding';

Cipher _createAesCbcCipher(Uint8List keyData, Uint8List iv, bool isEncrypt) {
if (iv.length != 16) {
throw ArgumentError.value(iv, 'iv', 'must be 16 bytes');
}

return jni.using((arena) {
final algorithm = 'AES'.toJString()..releasedBy(arena);
final transformation = _aesCbcTransformation.toJString()..releasedBy(arena);
final keyBytes = arena.copyToJByteArray(keyData);
final secretKey = SecretKeySpec(keyBytes, algorithm)..releasedBy(arena);
final ivBytes = arena.copyToJByteArray(iv);
final parameters = IvParameterSpec(ivBytes)..releasedBy(arena);

final cipher = Cipher.getInstance(transformation);
if (cipher == null) {
throw operationError('JCA Cipher($_aesCbcTransformation) is unavailable');
}

var initialized = false;
try {
cipher.init$2(
isEncrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE,
secretKey,
parameters,
);
initialized = true;
} finally {
if (!initialized) {
cipher.release();
}
}

return cipher;
});
}

Future<Uint8List> _aesCbcEncryptDecryptBytes(
Uint8List keyData,
List<int> data,
List<int> iv,
bool isEncrypt,
) async {
Cipher? cipher;
try {
cipher = _createAesCbcCipher(keyData, _asUint8List(iv), isEncrypt);
return jni.using((arena) {
final bytes = _asUint8List(data);
final input = arena.copyToJByteArray(bytes);
final output = jni.JByteArray(cipher!.getOutputSize(bytes.length))
..releasedBy(arena);
final outputLength = cipher.doFinal$4(input, 0, bytes.length, output);
return output.copyToDartBytes(length: outputLength);
});
} on OperationError {
rethrow;
} on jni.JThrowable catch (e) {
throw _aesCbcOperationError(e);
} finally {
cipher?.release();
}
}

Stream<Uint8List> _aesCbcEncryptDecryptStream(
Uint8List keyData,
Stream<List<int>> data,
List<int> iv,
bool isEncrypt,
) async* {
final arena = jni.Arena();
Cipher? cipher;
try {
cipher = _createAesCbcCipher(keyData, _asUint8List(iv), isEncrypt);
final inputBuffer = jni.JByteArray(_defaultChunkSize)..releasedBy(arena);
final outputBuffer = jni.JByteArray(cipher.getOutputSize(_defaultChunkSize))
..releasedBy(arena);

await for (final chunk in data) {
final bytes = _asUint8List(chunk);
var offset = 0;
while (offset < bytes.length) {
final remaining = bytes.length - offset;
final length = remaining < _defaultChunkSize
? remaining
: _defaultChunkSize;

inputBuffer.setRange(0, length, bytes, offset);
final outputLength = cipher.update$2(
inputBuffer,
0,
length,
outputBuffer,
);
if (outputLength > 0) {
yield outputBuffer.copyToDartBytes(length: outputLength);
}
offset += length;
}
}

final outputLength = cipher.doFinal$4(inputBuffer, 0, 0, outputBuffer);
if (outputLength > 0) {
yield outputBuffer.copyToDartBytes(length: outputLength);
}
} on OperationError {
rethrow;
} on jni.JThrowable catch (e) {
throw _aesCbcOperationError(e);
} finally {
cipher?.release();
arena.releaseAll();
}
}

OperationError _aesCbcOperationError(jni.JThrowable throwable) {
late final String message;
try {
message = throwable.message;
} finally {
throwable.release();
}
return operationError('JCA Cipher($_aesCbcTransformation) failed: $message');
}

final class _StaticAesCbcSecretKeyImpl implements StaticAesCbcSecretKeyImpl {
const _StaticAesCbcSecretKeyImpl();

@override
Future<AesCbcSecretKeyImpl> importRawKey(List<int> keyData) =>
throw UnimplementedError('Not implemented');
Future<AesCbcSecretKeyImpl> importRawKey(List<int> keyData) async =>
_AesCbcSecretKeyImpl(_aesImportRawKey(keyData));

@override
Future<AesCbcSecretKeyImpl> importJsonWebKey(
Map<String, dynamic> jwk,
) async =>
_AesCbcSecretKeyImpl(_aesImportJwkKey(jwk, expectedJwkAlgSuffix: 'CBC'));

@override
Future<AesCbcSecretKeyImpl> generateKey(int length) async =>
_AesCbcSecretKeyImpl(_aesGenerateKey(length));
}

final class _AesCbcSecretKeyImpl implements AesCbcSecretKeyImpl {
_AesCbcSecretKeyImpl(this._keyData);

final Uint8List _keyData;

@override
String toString() => 'Instance of \'AesCbcSecretKey\'';

@override
Future<Uint8List> encryptBytes(List<int> data, List<int> iv) async =>
_aesCbcEncryptDecryptBytes(_keyData, data, iv, true);

@override
Future<Uint8List> decryptBytes(List<int> data, List<int> iv) async =>
_aesCbcEncryptDecryptBytes(_keyData, data, iv, false);

@override
Stream<Uint8List> encryptStream(Stream<List<int>> data, List<int> iv) =>
_aesCbcEncryptDecryptStream(_keyData, data, iv, true);

@override
Stream<Uint8List> decryptStream(Stream<List<int>> data, List<int> iv) =>
_aesCbcEncryptDecryptStream(_keyData, data, iv, false);

@override
Future<AesCbcSecretKeyImpl> importJsonWebKey(Map<String, dynamic> jwk) =>
throw UnimplementedError('Not implemented');
Future<Uint8List> exportRawKey() async => Uint8List.fromList(_keyData);

@override
Future<AesCbcSecretKeyImpl> generateKey(int length) =>
throw UnimplementedError('Not implemented');
Future<Map<String, dynamic>> exportJsonWebKey() async =>
_aesExportJwkKey(_keyData, jwkAlgSuffix: 'CBC');
}
6 changes: 4 additions & 2 deletions lib/src/impl_jni/impl_jni.utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,10 @@ extension _JniArenaByteArray on jni.Arena {

extension _JByteArrayCopy on jni.JByteArray {
/// Copies this JVM byte array into Dart-owned memory.
Uint8List copyToDartBytes() {
final bytes = getRange(0, length);
Uint8List copyToDartBytes({int? length}) {
final byteCount = length ?? this.length;
RangeError.checkValueInInterval(byteCount, 0, this.length, 'length');
final bytes = getRange(0, byteCount);
final view = Uint8List.sublistView(bytes);
return Uint8List.fromList(view);
}
Expand Down
Loading