-
-
Notifications
You must be signed in to change notification settings - Fork 95
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Is there a support for decrypting AES-GCM stream? #125
Comments
I've satisfied this requirement using Pointycastle. It would be nice to know if it's possible to achieve this using |
Hi @tomekit, could you provide some info on how you implemented this in |
Hi @elliotsayes, |
Thanks for your reply @tomekit. For encrypting, I've implemented something along the lines of what you suggested, using final encryptedStream = blockStream.transform<Uint8List>(
StreamTransformer.fromHandlers(
handleData: (data, sink) {
final encryptedBlockBuffer = Uint8List(encrypter.blockSize);
final processedBytes = encrypter.processBlock(data, 0, encryptedBlockBuffer, 0);
sink.add(encryptedBlockBuffer.sublist(0, processedBytes));
},
handleDone: (sink) {
var macBuffer = Uint8List(encrypter.macSize);
encrypter.doFinal(macBuffer, 0);
sink.add(macBuffer);
sink.close();
},
),
); |
Nice approach @elliotsayes. I couldn't really use such approach, since my stream isn't continuous, that is, I receive e.g. 5000 bytes in first iteration, then 1000 in another. That means that I often have the ending block which isn't full 16 bytes. If I used I am wondering if you had any issues with this approach? ... but given that you seem to have continuous stream I guess it all works alright. |
Thanks for the comment @tomekit.
My scenario is actually very similar, I was just preprocessing the stream to split it into chunks of 16 bytes using ChunkedStreamReader. However it turns out that reading in such small chunks causes a lot of overhead, so I had to change this approach to reading larger chunks and process them piecewise using the offsets in encrypter.processBlock(sourceBuffer, bufferOffset, sinkBuffer, bufferOffset);
This is interesting, AFAICT I have had no such problems with the authentication tag (this is what determines the MAC at the end of the ciphertext?). Bear in mind here that I have thus far only played around with encryption and not decryption, but based on my testing the output of my
Although the output (for encryption) appears to be correct, the performance is lacking to say the least. In a This is in contrast to e.g. I think my only remaining option to improve performance is something like https://pub.dev/packages/flutter_rust_bridge ? Curious on your experience/thoughts related to performance |
It's interesting. Yes authentication tag and MAC are the same thing in this context.
Yep, that's something similar to what I've did in my fixes: bcgit/pc-dart@master...tomekit:pc-dart:master unfortunately performance for Web is a pure tragedy.
I don't have experience with Rust and bindings to Rust, however I've used FFI in context of libsodium and performance was great: https://pub.dev/packages/sodium_libs What Rust library do you have in mind when used with the Flutter bridge? |
I wrote comment earlier but github ate it... oops. Here goes again
I barely do either. But from my investigation, the rust-in-flutter story is a bit messy right now, especially when you bring web into the mix. Some issues below:
I've been playing with
What kind of numbers were you getting for AES256-GCM? FYR my testing with
Based on my research, the term "stream cipher" (i.e. as opposed to "block cipher") means something different to whether the API lets you encrypt chunks incrementally or not. Read more: https://en.wikipedia.org/wiki/Stream_cipher
Does this mean writing JS for web and native code for each additional platform? If so that is a serious undertaking.
Just bear in mind that although XChaCha20 is faster in software, AES has optimised instructions on modern processors ( |
By no means this is scientific (please find test code at the bottom of this post), but on a 100MiB file I get:
You're right. However in this case, the API (https://doc.libsodium.org/advanced/stream_ciphers) gives you low-level building blocks (e.g. Conversely if an API advertises streaming support: https://doc.libsodium.org/secret-key_cryptography/secretstream it doesn't necessarily mean that it's actually conventional streaming that you would expect it to be (More about that: Skycoder42/libsodium_dart_bindings#26 (comment)).
My assumption is, that most of the stuff (streaming and authentication - Poly1305) is already there. The C library will have to be modified and I hope it won't be any low-level stuff, but mostly using existing functions and connecting them really. Then it will have to be compiled to WASM/JS - https://github.com/jedisct1/libsodium.js/ Test / benchmark code: test('libsodiumXChaCha20Bench', () async {
final libsodium = DynamicLibrary.open('/usr/lib/x86_64-linux-gnu/libsodium.so'); // dpkg -L libsodium-dev | grep "\.so"
// final libsodium = DynamicLibrary.open('/opt/homebrew/Cellar/libsodium/1.0.18_1/lib/libsodium.23.dylib'); // brew info libsodium
final sodium = await SodiumInit.init(libsodium);
final base64MasterKey = "+Hv/rT8HPG+Qmk3zhV2NDA==";
final encryptedKey = "8IK5l6NGSudK/b57goLjZ6ePvfHj+w29D7rle8ShLCLdl0Yy5irmtw==";
final iv = "T4jMtxyX/+s60T3rT4jMtxyX/+s60T3r"; // For AES use half of that, e.g.: `T4jMtxyX/+s60T3r`
final nonce = base64Decode(iv);
// final plaintext = Uint8List(1024*1024*100);
final plaintext = await File('/home/tomek/Downloads/rand100MiB.bin').readAsBytes(); // dd if=/dev/random of=/home/tomek/Downloads/rand100MiB.bin bs=1024 count=102400
final unwrappedKey = AesKwRfc3394.unwrap(encryptedKey, base64MasterKey);
final secureKey = SecureKey.fromList(sodium, Uint8List.fromList(unwrappedKey));
final timer = Stopwatch()..start();
final encryptedOutputBinary = sodium.crypto.aead.encrypt(message: plaintext, nonce: nonce, key: secureKey); // Change from `aead` to `aeadAes256Gcm` for AES
timer.stop();
print('Encrypted after ${timer.elapsed.inMilliseconds}ms');
final timer2 = Stopwatch()..start();
final decryptedOutputBinary = sodium.crypto.aead.decrypt(cipherText: encryptedOutputBinary, nonce: nonce, key: secureKey); // Change from `aead` to `aeadAes256Gcm` for AES
timer2.stop();
print('Decrypted after ${timer2.elapsed.inMilliseconds}ms');
}); |
Regarding your benchmarks, it looks like you are running those on the linux dart runtime which explains the speed. Did you take any measurements on the web runtime? Also note that if AES-GCM is is faster than XChaCha20 then it probably means your CPU has the optimised
Ah that makes sense, I didn't realise the JS impl- is just compiled from C to WASM. That should simplify things.
Yes, I have noticed this too, and I believe it is because we are dealing with Authenticated Encryption (AE). It seems you can't just expect to take a buffer based AE implementation and turn it into a stream based one without modifying the protocol itself. E.g. If you want to keep compatibility with the standard protocol by skipping the MAC after each chunk, then it goes against the security model of AE (exposing unauthenticated data to the application), and to some extent defeats the purpose of using AE (over unauthenticated encryption) at all. So IMO the answer is either:
I think this is preferable to hacking an AEAD implementation to defer Authentication to the end of the message, unless you really need that and are uncomfortable with committing to a custom protocol. In my case I believe that it is possible to do Authentication at a higher level in the protocol (i.e. the AES-GCM MAC is essentially redundant), so I'll see if I can get by with the latter option. |
Nope. I've only assumed it will be "enough" having chance to see the XChaCha20 results in general.
If I didn't have a requirement of providing preview of encrypted video files, than I would probably keep files encrypted in e.g. 5MB independently signed chunks. That proves to be difficult if you aim your decrypted plaintext match 1:1 (position-wise) with the ciphertext: jedisct1/libsodium#1255 (comment)
You're entirely right. It's just I've assumed that "delayed verification" will almost always be better than none. Finally, I've wanted my solution to be cross-compatible and I've followed exactly the same implementation that AWS initially proposed: https://docs.aws.amazon.com/general/latest/gr/aws_sdk_cryptography.html#crypto_features and https://aws.amazon.com/blogs/developer/amazon-s3-client-side-authenticated-encryption/ Thank you for having this great discussion. It's really useful to reiterate and validate this all again. |
Thinking about this requirement as I may want to implement it myself at some point... intuitively it would be possible to do this with a gateway service that proxies a given HTTP request, mapping the to the correct
Yes, this is essentially leaking the security abstraction by offloading some of the security burden to your application code. I'd put it in the "probably a bad idea" category, but you could get away with it if you are careful.
Same here, as I'm working on an open standard (ArFS), and don't really want to include a custom protocol (e.g. libsodium's Personally I'm going to wait for confirmation of whether we can remove Authentication entirely from this layer of the protocol, and if so go with
Likewise, appreciate the exchange :) |
I am trying to decrypt an AES-GCM stream.
This is for purposes of previewing the big video file. The other use case is decrypting big files (e.g. 10GB) without necessity of allocating that much RAM.
I would still like to perform the authentication, once I get to the end of stream (last 16 bytes contain MAC).
I understand the security implication of e.g. playing "unauthenticated" video preview to the user.
It seems it's not something that's supported out of the box, as GCM cipher is only supported in block mode currently.
I've managed to decrypt AES-GCM stream using Pointycastle AES-CTR streaming cipher, but... I can't find an easy way of authenticating such stream. Is it feasible to calculate GMAC on top of streamed AES-CTR (e.g. using DartGcm) to get the GCM auth properties?
Streamed AES-GCM with authentication is possible with e.g. Python. Please find this example:
You can play with it here: https://trinket.io/python3/445dfc8d86 (please note that sometimes you need to click Run button multiple times before the ModuleNotFoundError disappears)
Example decryption code for AES-GCM. How to implement streaming?
The text was updated successfully, but these errors were encountered: