Skip to content

Commit 1dc289c

Browse files
authored
Fix AES-CTR large chunk handling (#309)
* fix(aes-ctr): handle partial chunks larger than buffer * test(aes-ctr): refine large chunk coverage
1 parent c53fc00 commit 1dc289c

4 files changed

Lines changed: 130 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
Flutter plugin scaffolding.
99
* Improved web error handling when `window.crypto.subtle` is unavailable in
1010
insecure contexts by throwing a clearer `UnsupportedError` with guidance.
11+
* Fixed native AES-CTR encryption and decryption for inputs larger than the
12+
internal chunk buffer.
1113
* Added documentation for performing `deriveKey` using `deriveBits` and
1214
`importRaw`.
1315

lib/src/impl_ffi/impl_ffi.aesctr.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ Stream<Uint8List> _aesCtrEncryptOrDecrypt(
140140
// Consume the first M bytes from data.
141141
var i = 0; // Number of bytes consumed, after offset
142142
while (i < M) {
143-
final N = math.min(M, bufSize);
143+
final N = math.min(M - i, bufSize);
144144
inData.setAll(0, data.skip(offset + i).take(N));
145145

146146
_checkOpIsOne(ssl.EVP_CipherUpdate(ctx, outBuf, outLen, inBuf, N));

lib/src/testing/webcrypto/aesctr.dart

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
library;
1616

17+
import 'dart:typed_data';
18+
1719
import 'package:webcrypto/webcrypto.dart';
1820
import '../utils/utils.dart';
1921
import '../utils/testrunner.dart';
@@ -72,6 +74,9 @@ void main() async {
7274

7375
// Allow single quotes for hardcoded testData written as JSON:
7476
// ignore_for_file: prefer_single_quotes
77+
final _zeroAesCtrBlock = bytesToJson(Uint8List(16));
78+
final _largeZeroAesCtrPlaintext = bytesToJson(Uint8List(4097));
79+
7580
final _testData = [
7681
{
7782
"name": "A128CTR/64 generated on boringssl/linux at 2020-01-19T16:40:39",
@@ -160,11 +165,99 @@ final _testData = [
160165
"length": 9,
161166
},
162167
},
168+
{
169+
"name": "A128CTR/128 4097-byte zero plaintext",
170+
"privateRawKeyData": _zeroAesCtrBlock,
171+
"privateJsonWebKeyData": {
172+
"kty": "oct",
173+
"use": "enc",
174+
"alg": "A128CTR",
175+
"k": "AAAAAAAAAAAAAAAAAAAAAA",
176+
},
177+
"plaintext": _largeZeroAesCtrPlaintext,
178+
"ciphertext": _largeAesCtrCiphertext,
179+
"importKeyParams": {},
180+
"encryptDecryptParams": {"counter": _zeroAesCtrBlock, "length": 128},
181+
},
163182
// HACK: Exclude counter rollover test data on Firefox, where it is broken:
164183
// https://hg.mozilla.org/projects/nss/file/38f1c92a5e1175bb8388768a209ac0efdabd1bd7/lib/freebl/ctr.c#l86
165184
...(nullOnFirefox(_rolloverTestData) ?? <Map>[]),
166185
];
167186

187+
const _largeAesCtrCiphertext =
188+
"ZulL1O+KLDuITPpZyjQrLlji/M76fjBhNn8dV6TnRVoDiNrOYLajkvMowrlxsv5495Wqq0lLWSP3"
189+
"/Yn/lIvB4CACESFOc5TaIIm2rNCTq+DJTaIZEY4pfXt+vLzJw4jyit59hajuNWFvcSSp1ScCkZW4"
190+
"TRuWxpD/Ly3jC/LsieACU3huEmUE8Nq5DEijAyHeM0XmsEYefJ5sa3r+3eg/QN6z+meU+P2PVaiN"
191+
"y9qdaPITfMnINCAHfnzyirJpaw3wXRFFK1isUKous6GVthuH5cZabdXX96hAZdWhf/RicwhgAklt"
192+
"tj+kuRvuOH+jAwyVpz+NBDfgkV+85demLY2rCliyQxvAvt4CVQ9AI4lp7HgEEL78zeaUS2ndAH3r"
193+
"45qdvF4k9RmkvfR4sdnsC2cSXyiwbvqlXXlBKtYo1FCJw8ME+U2zoh32za9tLi47NVRB7/ZK2QUn"
194+
"51Kksuu00KEHDOLimC4nL9t89LWEsJWg+Vf9uCholDfjfcSLKtN5xvPG6Vfud6+4jGWUm6Eu7EXC"
195+
"KGXkkHrkKu6BOJis35Hi5MIdgo4Kdt4rtrtvhp5e7x9hje3SdWKBK5oU6JlqXDUt84F+YNbsIBGa"
196+
"UsgKYewZViJickAhLezKUV/qtj4nNFh5SKg2p94gXP7Awog1HDSLEXlB3Qpj2ZRwPmPZSkRoBCE6"
197+
"tPsdK3ujdlkKLCQdH1CNxqf0GKFFA964mxeq2ygG9z/Abl0U5nX17IgAI9T3MpYS3OSg5bx5K1ta"
198+
"VfnC8w4HrSCZ6awpAAeqXhJQa+2CcUDSYRRM3mbttbMn0/aUWMSSpOs1HnFAQwJLks22icmLgW5I"
199+
"pBeUpNOc+mtHpTQGJRiguYTtbZZ/p0XRYXipnjgDkRjEpPI8HyBRxh4Qvy70hSHX0NJ0ygS8VOB/"
200+
"SdcvILX6Byc/ZML4xDLjv7zgnuSacJJqDeK8HkysFm5f54cnt56GD35Fr19d7jGlWiBBpxiSi34i"
201+
"WQuPn+2xF2oCk6bkdwGGlCKx/QB8vjmmPKXqHg6nLLfnR14rk1x3P/kSdpJjQw/1pxuGFUnN6CBY"
202+
"A+aRGpuqsP95XHfBrMkfHdseaZyG/bjf7Bs8J9QQ2nramwb06pVBTv+gFVwC2NqjOV4x0uNfagS9"
203+
"FQR0+oI9JeUNrOJww9K7zsoy/YFe3pWJd07fKr0fgBWmzd1ngzGbVzayNV33kNCDN6B8ZsdAgubt"
204+
"3wmq31SAb2IcNZSSqkYlT4HnXdLrptBL5hnkRFljZ7sIzcQz27uSqLEcUcExZh96g1JzGlq/VetZ"
205+
"B5s9C4WOvPHrmq/Ud6PHpUmN6UlpJ42+CTj1naPZcI0kcxnwN5buW8po3ra2kjd8hfZRXSoz4V4H"
206+
"Ct5TPS4JDtD1vhO8CYMmWv1HBsrnJdoT7KohtOkruaC4kpgMHFcer3QALYT8gBbk/xl9mzV9xrDQ"
207+
"0F+RSgOnUZypzlrI4bXdndrRTT79Jp45+k5oTdH31I3C+BzzBVYhirPGEs7bKXMuX8w5+NdEiozU"
208+
"jvhz9vE8OVE/zQD+3Wu5e3ZCxRT2XwAfLXpgQJFU25FOLHVQxZF22yEvr+Wghj/cgo0d+dsfi6+u"
209+
"HCkQhgVLRmIrn0iYg3NOMlqN0XMYPg6POAyWicTfMZT1R2IUiD+Mz1p2rjYzALLz8+eX35ameNdU"
210+
"u7A/b5QQ957KcBIMY+0OPO2Wn9HgN4kGtIGJJMFLJMP2IQCRR3MaT74J9hPLSoTU+6FKvvVlahdV"
211+
"kR1d5E77V3m31vbcdZoJT1/gdNPf2slTmsg8GGBOUKcNYMbNJBcIEs6AozEvPmgQIF5b1IJKDlmj"
212+
"O/HrTkbqWeEyVXbFy5dUKtLdWPt9zIzjW8Eo6BiTPAw7xvKJq5nORjcvg9KUebeY5QMsWrnzsKr6"
213+
"UskO2MMX6FIwcFD4Hp1mFNpy6X8RM9wI2/+dUGY1Pkq24hS3Vvf6Fpwd+g3vhXZC1EJMiCh5OsIa"
214+
"EY9GTZfqx+zeZwyDxy5+ZzBzNkL2Jasows+BYNEFdxxJP8RZ2IsEQaORTRiAV56DT1zj1OTQG/qY"
215+
"oY3wTVBlOREFv+JCQOT+YJ/0/cVX/ScckDABKid0Fu8HYpq9xN1KyAuJSidprad/BhrHelt8tpFm"
216+
"VwJU8ijIsgTotamu8vw899YgjDwg8q8WF33QQetpsziPGFcKFuOVkOmw9wZOErY9/VRwd3d2lZ8W"
217+
"tXnUNNbjbM5yNYTA7uH6IvJZjBXjXFTdipYq62qsErqDpzsSwDDQXxHaZxj1ttiSXe0lDzcRCe3I"
218+
"FHkm5ofBtiuoPuJ4IT3q2z6tUMH5gL4Zxr+n5sgGAx9orSwyiBimN4bmQHx+IqD5W90HZVinK9r6"
219+
"oELs75XdqoAHpltkfD3PCFauyqN5meZgPnMyiHFYbfnoCevxLKahB6IKfAVmmRarieqN3qcHN1qB"
220+
"ZZz7HVNeXka2tqaekOUDV58MEQRyqEX3fmzoqx4nLC2VCeNcFzCjveG8GO6ODG8NZD9MOgbBeOfB"
221+
"13Xw75aR73Oc2LAKUx5og2NAfuwR0f9XTE5qT5458kEgHOMSmTDLk0kOywbQEBHkgHmO1UgC41zD"
222+
"FoE6FBmexK88wjp656nQXlwOT75JHdgDVvnm7w5UCaTvYCYfvw7jASHbAsH+6UHRAxn5aF4ko2jS"
223+
"yj8NA+Df0DwH/mLHxl1z4iHfajB53N07eLVi1nLkJGEH+9MqlzSb8u2Z6HJssXNDSeXFSZr/Sbjf"
224+
"SpfL4Dgz5mGXYg2t4I3SpR06ZIi9IQS2xaUcCbE+ONrH0S2lhSlSLzDE0hyeIxg1eCfv7ec8y/7x"
225+
"nsarDk2TwcGohbg3q4Oj/QQDp+YBYZd02sUm339vARlifP+5314AZjyTbRYB6f5M5M68qGngEBHa"
226+
"f4U/xQg9atUJJdUr2th9peebTbUpcAc7O/LA+w18NNsq88MJYtRjoaUXfxEfLNGpb9zBY56FGas7"
227+
"KhZ5jkQs2OP8xt7zaCdMaWYlQ4hzRU5/xsc305mqNV1/9HXAUSia6jwPld1KSlydGQqecNJjCcvu"
228+
"wx+AMXWELktavI+ABgLAe0/5WhwMDsOO4myjVK4kL5XJi1NTvpt866GmHAbZoD9Zqyq1QlAAkhFe"
229+
"cM4TXaAlflzxKPRuNFiaOsZOCf6Nb/r13Uv7fAfUt0QszvjEEu5O+ZOAYi2OVFqpYbcTXRlae5T/"
230+
"YbGjZWTi1KEWFWq8ii5ovegDcMs3/sy+ilHbm4nR/7p/BVF4YeBeGa1XAKOzbnJcpXD0qiaqZux7"
231+
"z6FFub5W8/eUoRIFy0ErwHUav6zeM8f+J8YLTX3EesjHTuiwt/C0kJgGiBBqaE32xRmEj98Vv7Ig"
232+
"0iXf3gW95eoiMHzyuh4fElRsZohb6bLbSWwPPEvWX1Q+IpsJyoPlfJW7xN+5nCrFw1wyR4lQ79xt"
233+
"OST3euuvfeBZJgYRDw1ZWFqRbd0v8WtHDJnRogDeHCGOkYPN/oEruho+uEIbukdcGTIxsyJ+28VG"
234+
"YSz29iZHrhZ0MzgnKQsrUvTWrzzCRadlL0PPGW4/W4QZi6QjzsV3vsnn6RmwyANQttQIckzNnIt/"
235+
"fX3cx+YK0ISMQAQ0TjeOqedR9/l9O3DKellm7bUt7iJ9R2lTOIGYtVVuuUz4vhHuYtpVz6xnKi+N"
236+
"0YC6Sx6QzpwCFk/6M3iZvzb5QMT7d1JQ0ZoNNw2eZsMGPNNjyLngmQ2YloxUK4QfLarqGos6TWvd"
237+
"fzO1SE/zLe+yDdJb5IVfGKcqkIKD/Jj9FBcJYJPCQZBdyn+yUUaTwZddxeVTgDRe9Z7HyxX9yutG"
238+
"dihhAUGfVG7wZ2c62yPVAp7aqAQY+pduOENQx8egXBnSoKgEhYWxNTzdvc3DrWksLQdsCqEZe6U6"
239+
"vydFA2jzylYD0FbQAmMG2l69MczK/iXvy4a6pewR86OB4mcNJdpOnuS9HFUMnbwNj40/19peh7/O"
240+
"a1Iw5i6da7oEydArGQWr2XX54E5MNh7Pc7X7BxLdN8P8rYQvBjV5hhEkb9ULLovKBWSvkKwLPhfj"
241+
"YyDSasf8tqKozdSRgt9IOBAVG7R1oadphlkwAUuG2ajG2N3/dKQWeuyHdkqnlYilE1g1xSfZwjf6"
242+
"unm8MP6rC2Ldr5ximRPh17yZEt/oCxWbuJJV+EYhMSrs8WCuccdZK9QkySutAC1vijR6r+uYZNmB"
243+
"us6Wh7OZvtoV8YEbAKyzUpI4Ep+9SRg7l8qUyw7SSuNwscgHBIgMMfuhDuwJuxtEq4g5iO4EqN6e"
244+
"oliSfYl7cVfD7oDSgmr2jK8xDUP1Lvl6juUT50mbI+0m7V5a86bGvSXAIC2xQ/BlSmmzyAxVYQ8M"
245+
"6llRTannZ2zhI5fyp5yn/tsySyXg+nm463OF17EVadOn3FLQ+Q+r8DNwWULw+CHscMSvoI5s8dz/"
246+
"UEi7AxJRiMdzYywFbMIwxnD79jNpPIpyYtf7y84dTjz0x/Yw7yIJhOmW05ZiVlarrqmkRp0veLBe"
247+
"LhVGqyZ+Ke/oxV/vcP++b9EqXYu3oMzKJSeHrMpjKHFKiR0wEtTdhZoUbAwOlIWEnPJJj0AG2PlE"
248+
"eG8oThbByLYx3AjS3Dw60UAHbi+FRn824oQJuJZSQzke5qc4gwjtA/LFbdi5f74JN3INX9O3eAvb"
249+
"kwJ6Fv6+GZsmZ9Yt+F9eRtaLHkD/CRzrhgH/R1Hl6/Ae433QsMVJ6RsjQCIjO42p/kv23R3qd/fe"
250+
"VfgnbNC3bN22Cl57PdBuJT/5j/xrgssXR7lhNKbhrgCT4a7jvfDvmQ2iCZgX6lnQrAD4UN+WcMpg"
251+
"2o3DqOkKwhL1VKIuY79J+o9nYWGo0zMmSXVt5+sc834WCGKP4XbTgsxITWcpFKDRs5wcAW+/q3Rk"
252+
"oKcFc75N1/E0uyehrVxT2QEWueyrBLfS4MnpslAMeO3TK6wJ5w14NQ+v9QDoQy01BFeHFShdDraj"
253+
"N8pe545akPaXdRUeDSuDlOd6JyncH1gqvBqJJGmq15UQQZeenMOiKfQV+iVoCs/8tRFqFcJZypJt"
254+
"jMHPHvcxUZo49lOOP9zKLOEcgcqGvdWt9Iqo9lL5phGJ2mkHpTE5XTFGKJ+8ws6Am4+QBvJTTJzq"
255+
"+7oT3/PAaAKXKY8DB7SlLZzxTqLeDDb4MXuJMWZieViADsSzvGojIC8a//4DSKs7riGnNRlLNmVb"
256+
"Fs6tRT8ecZqOTc4/p9wEiaJkMntmZ4pvJYVa0jt3UMK9O/nTAg/+oxMXgvrx13YhuCloL3Nsm8oR"
257+
"VrEpjZ+j0tAEo27Jn2tTy4edBVbxyR5ZPbH31BNnqmAzcwMEArUeXiBZsB/LeZGQ5NwsYm73P6vx"
258+
"cnIQDeyHT2aUy2jgeIr9Lh47XdVmXvr8Ug7Tr8qslNMaJ/4tqHAW3dR/MPoQNXRKmcT4xV1XEJoV"
259+
"CWzXkFhKUZhsGANsX5gUSIqj3om5lhLCfzXPIL+Wn8gD9w3e+TumJYgkKg5n0NZF4Ps=";
260+
168261
final _rolloverTestData = [
169262
{
170263
"name":

test/aes_ctr_counter_wrap_test.dart

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
import 'dart:async';
1516
import 'dart:typed_data';
1617

1718
import 'package:test/test.dart';
@@ -37,4 +38,37 @@ void main() {
3738

3839
expect(ciphertextA.sublist(16, 32), isNot(equals(ciphertextB)));
3940
});
41+
42+
test('AES-CTR keeps large byte encryption length stable', () async {
43+
final key = await AesCtrSecretKey.importRawKey(Uint8List(16));
44+
final plaintext = Uint8List(4097);
45+
final counter = Uint8List(16);
46+
47+
final ciphertext = await key.encryptBytes(plaintext, counter, 128);
48+
final decrypted = await key.decryptBytes(ciphertext, counter, 128);
49+
50+
expect(ciphertext, hasLength(plaintext.length));
51+
expect(decrypted, equals(plaintext));
52+
});
53+
54+
test('AES-CTR stream encryption matches large byte encryption', () async {
55+
final key = await AesCtrSecretKey.importRawKey(Uint8List(16));
56+
final plaintext = Uint8List(4097);
57+
final counter = Uint8List(16);
58+
59+
final ciphertext = await key.encryptBytes(plaintext, counter, 128);
60+
final streamedCiphertext = await _collectBytes(
61+
key.encryptStream(Stream.value(plaintext), counter, 128),
62+
);
63+
64+
expect(streamedCiphertext, equals(ciphertext));
65+
});
66+
}
67+
68+
Future<Uint8List> _collectBytes(Stream<List<int>> stream) async {
69+
final builder = BytesBuilder(copy: false);
70+
await for (final chunk in stream) {
71+
builder.add(chunk);
72+
}
73+
return builder.takeBytes();
4074
}

0 commit comments

Comments
 (0)