Skip to content
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

Documentation for most AES-GCM members #100

Merged
merged 2 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion lib/src/impl_ffi/impl_ffi.aesgcm.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Future<Uint8List> _aesGcmEncryptDecrypt(
throw _OperationError('tagLength must be 32, 64, 96, 104, 112, 120 or 128');
}

// TODO: Check iv length is less than EVP_AEAD_nonce_length
// TODO: Check iv length is less than EVP_AEAD_nonce_length, if this is a requirement!
// More importantly, add some test cases covering this, also consider
// what chrome does, how firefox passes tests. And check if other
// primitives that accept an iv/nonce has size limitations on it.
Expand Down
229 changes: 226 additions & 3 deletions lib/src/webcrypto/webcrypto.aesgcm.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,40 +14,263 @@

part of 'webcrypto.dart';

/// AES secret for symmetric encryption and decryption using AES in
/// _Galois/Counter Mode_ (GCM-mode), as described in [NIST SP 800-38D][1].
///
/// An [AesGcmSecretKey] can be imported from:
/// * Raw bytes using [AesGcmSecretKey.importRawKey], and,
/// * [JWK][2] format using [AesGcmSecretKey.importJsonWebKey].
///
/// A random [AesGcmSecretKey] can be generated using
/// [AesGcmSecretKey.generateKey].
///
/// AES in GCM-mode is an [authenticated encryption][3] cipher, this means that
/// that it includes checks that the ciphertext has not been modified.
///
/// {@macro AesGcmSecretKey-encryptBytes/decryptBytes:example}
///
/// [1]: https://csrc.nist.gov/pubs/sp/800/38/d/final
/// [2]: https://tools.ietf.org/html/rfc7517
/// [3]: https://en.wikipedia.org/wiki/Authenticated_encryption
@sealed
abstract class AesGcmSecretKey {
AesGcmSecretKey._(); // keep the constructor private.

/// Import an [AesGcmSecretKey] from raw [keyData].
///
/// [KeyData] must be either:
/// * 16 bytes (128 bit) for AES-128, or,
/// * 32 bytes (256 bit) for AES-256.
///
/// {@macro AES:no-support-for-AES-192}
///
/// **Example**
/// ```dart
/// import 'dart:convert' show utf8;
/// import 'dart:typed_data' show Uint8List;
/// import 'package:webcrypto/webcrypto.dart';
///
/// final rawKey = Uint8List(16);
/// fillRandomBytes(rawKey);
///
/// // Import key from raw bytes
/// final k = await AesGcmSecretKey.importRawKey(rawKey);
///
/// // Use a unique IV for each message.
/// final iv = Uint8List(16);
/// fillRandomBytes(iv);
///
/// // Encrypt a message
/// final c = await k.encryptBytes(utf8.encode('hello world'), iv);
///
/// // Decrypt message (requires the same iv)
/// print(utf8.decode(await k.decryptBytes(c, iv))); // hello world
/// ```
static Future<AesGcmSecretKey> importRawKey(List<int> keyData) {
return impl.aesGcm_importRawKey(keyData);
}

/// Import an [AesGcmSecretKey] from [JSON Web Key][1].
///
/// JSON Web Keys imported using [AesGcmSecretKey.importJsonWebKey]
/// must have `"kty": "oct"`, and the `"alg"` property of the imported [jwk]
/// must be either:
/// * `"alg": "A128GCM"` for AES-128, or
/// * `"alg": "A256GCM"` for AES-256.
///
/// {@macro AES:no-support-for-AES-192}
///
/// If specified the `"use"` property of the imported [jwk] must be
/// `"use": "sig"`.
///
/// {@macro importJsonWebKey:throws-FormatException-if-jwk}
///
/// **Example**
/// ```dart
/// import 'dart:convert' show jsonEncode, jsonDecode;
/// import 'package:webcrypto/webcrypto.dart';
///
/// // JSON Web Key as a string containing JSON.
/// final jwk = '{"kty": "oct", "alg": "A256GCM", "k": ...}';
///
/// // Import secret key from decoded JSON.
/// final key = await AesGcmSecretKey.importJsonWebKey(jsonDecode(jwk));
///
/// // Export the key (print it in same format as it was given).
/// Map<String, dynamic> keyData = await key.exportJsonWebKey();
/// print(jsonEncode(keyData));
/// ```
///
/// [1]: https://tools.ietf.org/html/rfc7517
static Future<AesGcmSecretKey> importJsonWebKey(Map<String, dynamic> jwk) {
return impl.aesGcm_importJsonWebKey(jwk);
}

/// Generate random [AesGcmSecretKey].
jonasfj marked this conversation as resolved.
Show resolved Hide resolved
///
/// The [length] is given in bits, and implies the AES variant to be used.
/// The [length] can be either:
/// * 128 for AES-128, or,
/// * 256 for AES-256.
///
/// {@macro AES:no-support-for-AES-192}
///
/// **Example**
/// ```dart
/// import 'package:webcrypto/webcrypto.dart';
///
/// // Generate a new random AES-GCM secret key for AES-256.
/// final key = await AesGcmSecretKey.generate(256);
/// ```
static Future<AesGcmSecretKey> generateKey(int length) {
return impl.aesGcm_generateKey(length);
}

// TODO: Document that this does not provide a streaming interface because
// access to the decrypted bytes before verification of the
// authentication tag defeats the purpose of authenticated-encryption.
/// Encrypt [data] with this [AesCbcSecretKey] using AES in
/// _Galois/Counter Mode_ (GCM-mode), as specified in [NIST SP 800-38D][1].
///
/// This operation requires an _initalization vector_ [iv]. The [iv]
/// needs not be secret, but it must unique for each invocation.
/// In particular the same (key, [iv]) pair must be used more than once.
jonasfj marked this conversation as resolved.
Show resolved Hide resolved
/// For detailed discussion of the initialization vector requirements for
/// AES-GCM, see [Appendix A of NIST SP 800-38D][1].
///
/// The [additionalData] parameter is optional, and is used to provide
/// _additional authenticated data_ (also called _associated data_) for
/// the encryption operation. Unlike the plaintext [data], the
/// [additionalData] is not encrypted. But integrity of the [additionalData]
/// is protected. Meaning that decryption will not succeed if [additionalData]
/// has be modified.
/// In an [authenticated encryption][2] scheme [additionalData] is typically
/// used to encode a IP, port, headers, date-time, a sequence number or
/// similar data that indicates how the ciphertext should be used.
/// As such [additionalData] aims to stop the ciphertext from being used
/// out of context.
///
/// This operation requires a [tagLength], which specifies the bit-length
/// of the resulting authentication tag.
/// The permitted values for [tagLength] are:
/// * `32` bits,
/// * `64` bits,
/// * `96` bits,
/// * `104` bits,
/// * `112` bits,
/// * `120` bits, or,
/// * `128` bits (default).
///
/// This tag ensures the authenticity of the plaintext and the
/// [additionalData]. A short tag, may decrease these assurances.
/// For a discussion of [tagLength] and security assurances,
/// see [Appendix B of NIST SP 800-38D][1].
///
/// This methods returns a [Uint8List] that is the concatenation of the
/// _ciphertext_ and the _authentication tag_.
/// That is, if you use a default [tagLength] of `128`, then the last 16 bytes
/// of the return value makes up the _authentication tag_.
///
/// {@template AesGcmSecretKey-encryptBytes/decryptBytes:example}
/// **Example**
/// ```dart
/// import 'dart:convert' show utf8;
/// import 'dart:typed_data' show Uint8List;
/// import 'package:webcrypto/webcrypto.dart';
///
/// // Generate a new random AES-GCM secret key for AES-256.
/// final k = await AesGcmSecretKey.generate(256);
///
/// // Use a unique IV for each message.
/// final iv = Uint8List(16);
/// fillRandomBytes(iv);
///
/// // Specify optional additionalData
/// final ad = utf8.encode('my-test-message');
///
/// // Encrypt a message
/// final c = await k.encryptBytes(
/// utf8.encode('hello world'),
/// iv,
/// additionalData: ad,
/// );
///
/// // Decrypt message (requires the same iv)
/// print(utf8.decode(await k.decryptBytes(
/// c,
/// iv,
/// additionalData: ad,
/// ))); // hello world
/// ```
/// {@endtemplate}
///
/// {@template AesGcmSecretKey-remark:no-stream-api}
/// **Remark** this package does not offer a streaming API for
/// encryption / decryption using AES-GCM, because reading deciphered
/// plaintext prior to complete verification of the tag breaks the
/// authenticity assurances. Specifically, until the entire message is
/// decrypted it is not possible to know if it is authentic, which would
/// defeat the purpose of _authenticated encryption_.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theoretically there could be a streaming API for computational reasons (ie. you don't have to keep everything in memory at once), ending up with the result being thrown away at the end.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically correct, but BoringSSL doesn't offer a streaming interface, I suspect I read in a bug somewhere that it doesn't because the authors thought it would be a bad idea.

So that is the opinion we're carrying into package:webcrypto.

Also Web Cryptography specification doesn't offer streaming APIs at all, so I only added them because BoringSSL does. And on web we always buffer up the entire stream before we do the operation 🤣

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd either remove the explanation or link to the discussion. I don't think we need to explain this.

I kind-of agree that a streaming interface would be hard to use correctly. But so is basically all crypto primitives. You really have to know what you are doing.

/// {@endtemplate}
///
/// [1]: https://csrc.nist.gov/pubs/sp/800/38/d/final
/// [2]: https://en.wikipedia.org/wiki/Authenticated_encryption
Future<Uint8List> encryptBytes(
List<int> data,
List<int> iv, {
List<int>? additionalData,
int? tagLength = 128,
});

// TODO: Document this method, notice that [data] must be concatenation of
// ciphertext and authentication tag.
jonasfj marked this conversation as resolved.
Show resolved Hide resolved
Future<Uint8List> decryptBytes(
List<int> data,
List<int> iv, {
List<int>? additionalData,
int? tagLength = 128,
});

/// Export [AesGcmSecretKey] as raw bytes.
///
/// This returns raw bytes making up the secret key.
///
/// **Example**
/// ```dart
/// import 'package:webcrypto/webcrypto.dart';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It nags me that the example is both top-level code, and statement-level code...
Ideally it would somehow be actual runnable code by some convention.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, maybe it's messy and we should add a main, or omit imports.

Feel free to file an issue on the repository.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok: #101

///
/// // Generate a new random AES-256 secret key.
/// final key = await AesGcmSecretKey.generate(256);
///
/// // Extract the secret key.
/// final secretBytes = await key.exportRawKey();
///
/// // Print the key as base64
/// print(base64.encode(secretBytes));
///
/// // If we wanted to we could import the key as follows:
/// // key = await AesGcmSecretKey.importRawKey(secretBytes);
/// ```
Future<Uint8List> exportRawKey();

/// Export [AesGcmSecretKey] as [JSON Web Key][1].
///
/// {@macro exportJsonWebKey:returns}
///
/// **Example**
/// ```dart
/// import 'package:webcrypto/webcrypto.dart';
/// import 'dart:convert' show jsonEncode;
///
/// // Generate a new random AES-256 secret key.
/// final key = await AesGcmSecretKey.generate(256);
///
/// // Export the secret key.
/// final jwk = await key.exportJsonWebKey();
///
/// // The Map returned by `exportJsonWebKey()` can be converted to JSON with
/// // `jsonEncode` from `dart:convert`, this will print something like:
/// // {"kty": "oct", "alg": "A256GCM", "k": ...}
/// print(jsonEncode(jwk));
/// ```
///
/// [1]: https://tools.ietf.org/html/rfc7517
Future<Map<String, dynamic>> exportJsonWebKey();
}