|
14 | 14 |
|
15 | 15 | part of 'webcrypto.dart';
|
16 | 16 |
|
| 17 | +/// AES secret for symmetric encryption and decryption using AES in |
| 18 | +/// _Galois/Counter Mode_ (GCM-mode), as described in [NIST SP 800-38D][1]. |
| 19 | +/// |
| 20 | +/// An [AesGcmSecretKey] can be imported from: |
| 21 | +/// * Raw bytes using [AesGcmSecretKey.importRawKey], and, |
| 22 | +/// * [JWK][2] format using [AesGcmSecretKey.importJsonWebKey]. |
| 23 | +/// |
| 24 | +/// A random [AesGcmSecretKey] can be generated using |
| 25 | +/// [AesGcmSecretKey.generateKey]. |
| 26 | +/// |
| 27 | +/// AES in GCM-mode is an [authenticated encryption][3] cipher, this means that |
| 28 | +/// that it includes checks that the ciphertext has not been modified. |
| 29 | +/// |
| 30 | +/// {@macro AesGcmSecretKey-encryptBytes/decryptBytes:example} |
| 31 | +/// |
| 32 | +/// [1]: https://csrc.nist.gov/pubs/sp/800/38/d/final |
| 33 | +/// [2]: https://tools.ietf.org/html/rfc7517 |
| 34 | +/// [3]: https://en.wikipedia.org/wiki/Authenticated_encryption |
17 | 35 | @sealed
|
18 | 36 | abstract class AesGcmSecretKey {
|
19 | 37 | AesGcmSecretKey._(); // keep the constructor private.
|
20 | 38 |
|
| 39 | + /// Import an [AesGcmSecretKey] from raw [keyData]. |
| 40 | + /// |
| 41 | + /// [KeyData] must be either: |
| 42 | + /// * 16 bytes (128 bit) for AES-128, or, |
| 43 | + /// * 32 bytes (256 bit) for AES-256. |
| 44 | + /// |
| 45 | + /// {@macro AES:no-support-for-AES-192} |
| 46 | + /// |
| 47 | + /// **Example** |
| 48 | + /// ```dart |
| 49 | + /// import 'dart:convert' show utf8; |
| 50 | + /// import 'dart:typed_data' show Uint8List; |
| 51 | + /// import 'package:webcrypto/webcrypto.dart'; |
| 52 | + /// |
| 53 | + /// final rawKey = Uint8List(16); |
| 54 | + /// fillRandomBytes(rawKey); |
| 55 | + /// |
| 56 | + /// // Import key from raw bytes |
| 57 | + /// final k = await AesGcmSecretKey.importRawKey(rawKey); |
| 58 | + /// |
| 59 | + /// // Use a unique IV for each message. |
| 60 | + /// final iv = Uint8List(16); |
| 61 | + /// fillRandomBytes(iv); |
| 62 | + /// |
| 63 | + /// // Encrypt a message |
| 64 | + /// final c = await k.encryptBytes(utf8.encode('hello world'), iv); |
| 65 | + /// |
| 66 | + /// // Decrypt message (requires the same iv) |
| 67 | + /// print(utf8.decode(await k.decryptBytes(c, iv))); // hello world |
| 68 | + /// ``` |
21 | 69 | static Future<AesGcmSecretKey> importRawKey(List<int> keyData) {
|
22 | 70 | return impl.aesGcm_importRawKey(keyData);
|
23 | 71 | }
|
24 | 72 |
|
| 73 | + /// Import an [AesGcmSecretKey] from [JSON Web Key][1]. |
| 74 | + /// |
| 75 | + /// JSON Web Keys imported using [AesGcmSecretKey.importJsonWebKey] |
| 76 | + /// must have `"kty": "oct"`, and the `"alg"` property of the imported [jwk] |
| 77 | + /// must be either: |
| 78 | + /// * `"alg": "A128GCM"` for AES-128, or |
| 79 | + /// * `"alg": "A256GCM"` for AES-256. |
| 80 | + /// |
| 81 | + /// {@macro AES:no-support-for-AES-192} |
| 82 | + /// |
| 83 | + /// If specified the `"use"` property of the imported [jwk] must be |
| 84 | + /// `"use": "sig"`. |
| 85 | + /// |
| 86 | + /// {@macro importJsonWebKey:throws-FormatException-if-jwk} |
| 87 | + /// |
| 88 | + /// **Example** |
| 89 | + /// ```dart |
| 90 | + /// import 'dart:convert' show jsonEncode, jsonDecode; |
| 91 | + /// import 'package:webcrypto/webcrypto.dart'; |
| 92 | + /// |
| 93 | + /// // JSON Web Key as a string containing JSON. |
| 94 | + /// final jwk = '{"kty": "oct", "alg": "A256GCM", "k": ...}'; |
| 95 | + /// |
| 96 | + /// // Import secret key from decoded JSON. |
| 97 | + /// final key = await AesGcmSecretKey.importJsonWebKey(jsonDecode(jwk)); |
| 98 | + /// |
| 99 | + /// // Export the key (print it in same format as it was given). |
| 100 | + /// Map<String, dynamic> keyData = await key.exportJsonWebKey(); |
| 101 | + /// print(jsonEncode(keyData)); |
| 102 | + /// ``` |
| 103 | + /// |
| 104 | + /// [1]: https://tools.ietf.org/html/rfc7517 |
25 | 105 | static Future<AesGcmSecretKey> importJsonWebKey(Map<String, dynamic> jwk) {
|
26 | 106 | return impl.aesGcm_importJsonWebKey(jwk);
|
27 | 107 | }
|
28 | 108 |
|
| 109 | + /// Generate a random [AesGcmSecretKey]. |
| 110 | + /// |
| 111 | + /// The [length] is given in bits, and implies the AES variant to be used. |
| 112 | + /// The [length] can be either: |
| 113 | + /// * 128 for AES-128, or, |
| 114 | + /// * 256 for AES-256. |
| 115 | + /// |
| 116 | + /// {@macro AES:no-support-for-AES-192} |
| 117 | + /// |
| 118 | + /// **Example** |
| 119 | + /// ```dart |
| 120 | + /// import 'package:webcrypto/webcrypto.dart'; |
| 121 | + /// |
| 122 | + /// // Generate a new random AES-GCM secret key for AES-256. |
| 123 | + /// final key = await AesGcmSecretKey.generate(256); |
| 124 | + /// ``` |
29 | 125 | static Future<AesGcmSecretKey> generateKey(int length) {
|
30 | 126 | return impl.aesGcm_generateKey(length);
|
31 | 127 | }
|
32 | 128 |
|
33 |
| - // TODO: Document that this does not provide a streaming interface because |
34 |
| - // access to the decrypted bytes before verification of the |
35 |
| - // authentication tag defeats the purpose of authenticated-encryption. |
| 129 | + /// Encrypt [data] with this [AesCbcSecretKey] using AES in |
| 130 | + /// _Galois/Counter Mode_ (GCM-mode), as specified in [NIST SP 800-38D][1]. |
| 131 | + /// |
| 132 | + /// This operation requires an _initalization vector_ [iv]. The [iv] |
| 133 | + /// needs not be secret, but it must unique for each invocation. |
| 134 | + /// In particular the same (key, [iv]) pair must **not** be used more than once. |
| 135 | + /// For detailed discussion of the initialization vector requirements for |
| 136 | + /// AES-GCM, see [Appendix A of NIST SP 800-38D][1]. |
| 137 | + /// |
| 138 | + /// The [additionalData] parameter is optional, and is used to provide |
| 139 | + /// _additional authenticated data_ (also called _associated data_) for |
| 140 | + /// the encryption operation. Unlike the plaintext [data], the |
| 141 | + /// [additionalData] is not encrypted. But integrity of the [additionalData] |
| 142 | + /// is protected. Meaning that decryption will not succeed if [additionalData] |
| 143 | + /// has be modified. |
| 144 | + /// In an [authenticated encryption][2] scheme [additionalData] is typically |
| 145 | + /// used to encode a IP, port, headers, date-time, a sequence number or |
| 146 | + /// similar data that indicates how the ciphertext should be used. |
| 147 | + /// As such [additionalData] aims to stop the ciphertext from being used |
| 148 | + /// out of context. |
| 149 | + /// |
| 150 | + /// This operation requires a [tagLength], which specifies the bit-length |
| 151 | + /// of the resulting authentication tag. |
| 152 | + /// The permitted values for [tagLength] are: |
| 153 | + /// * `32` bits, |
| 154 | + /// * `64` bits, |
| 155 | + /// * `96` bits, |
| 156 | + /// * `104` bits, |
| 157 | + /// * `112` bits, |
| 158 | + /// * `120` bits, or, |
| 159 | + /// * `128` bits (default). |
| 160 | + /// |
| 161 | + /// This tag ensures the authenticity of the plaintext and the |
| 162 | + /// [additionalData]. A short tag, may decrease these assurances. |
| 163 | + /// For a discussion of [tagLength] and security assurances, |
| 164 | + /// see [Appendix B of NIST SP 800-38D][1]. |
| 165 | + /// |
| 166 | + /// This methods returns a [Uint8List] that is the concatenation of the |
| 167 | + /// _ciphertext_ and the _authentication tag_. |
| 168 | + /// That is, if you use a default [tagLength] of `128`, then the last 16 bytes |
| 169 | + /// of the return value makes up the _authentication tag_. |
| 170 | + /// |
| 171 | + /// {@template AesGcmSecretKey-encryptBytes/decryptBytes:example} |
| 172 | + /// **Example** |
| 173 | + /// ```dart |
| 174 | + /// import 'dart:convert' show utf8; |
| 175 | + /// import 'dart:typed_data' show Uint8List; |
| 176 | + /// import 'package:webcrypto/webcrypto.dart'; |
| 177 | + /// |
| 178 | + /// // Generate a new random AES-GCM secret key for AES-256. |
| 179 | + /// final k = await AesGcmSecretKey.generate(256); |
| 180 | + /// |
| 181 | + /// // Use a unique IV for each message. |
| 182 | + /// final iv = Uint8List(16); |
| 183 | + /// fillRandomBytes(iv); |
| 184 | + /// |
| 185 | + /// // Specify optional additionalData |
| 186 | + /// final ad = utf8.encode('my-test-message'); |
| 187 | + /// |
| 188 | + /// // Encrypt a message |
| 189 | + /// final c = await k.encryptBytes( |
| 190 | + /// utf8.encode('hello world'), |
| 191 | + /// iv, |
| 192 | + /// additionalData: ad, |
| 193 | + /// ); |
| 194 | + /// |
| 195 | + /// // Decrypt message (requires the same iv) |
| 196 | + /// print(utf8.decode(await k.decryptBytes( |
| 197 | + /// c, |
| 198 | + /// iv, |
| 199 | + /// additionalData: ad, |
| 200 | + /// ))); // hello world |
| 201 | + /// ``` |
| 202 | + /// {@endtemplate} |
| 203 | + /// |
| 204 | + /// {@template AesGcmSecretKey-remark:no-stream-api} |
| 205 | + /// **Remark** this package does not offer a streaming API for |
| 206 | + /// encryption / decryption using AES-GCM, because reading deciphered |
| 207 | + /// plaintext prior to complete verification of the tag breaks the |
| 208 | + /// authenticity assurances. Specifically, until the entire message is |
| 209 | + /// decrypted it is not possible to know if it is authentic, which would |
| 210 | + /// defeat the purpose of _authenticated encryption_. |
| 211 | + /// {@endtemplate} |
| 212 | + /// |
| 213 | + /// [1]: https://csrc.nist.gov/pubs/sp/800/38/d/final |
| 214 | + /// [2]: https://en.wikipedia.org/wiki/Authenticated_encryption |
36 | 215 | Future<Uint8List> encryptBytes(
|
37 | 216 | List<int> data,
|
38 | 217 | List<int> iv, {
|
39 | 218 | List<int>? additionalData,
|
40 | 219 | int? tagLength = 128,
|
41 | 220 | });
|
42 | 221 |
|
| 222 | + // TODO: Document this method, notice that [data] must be concatenation of |
| 223 | + // ciphertext and authentication tag. |
| 224 | + // TODO: Document what happens if the authenticity validation fails? Some Exception? |
43 | 225 | Future<Uint8List> decryptBytes(
|
44 | 226 | List<int> data,
|
45 | 227 | List<int> iv, {
|
46 | 228 | List<int>? additionalData,
|
47 | 229 | int? tagLength = 128,
|
48 | 230 | });
|
49 | 231 |
|
| 232 | + /// Export [AesGcmSecretKey] as raw bytes. |
| 233 | + /// |
| 234 | + /// This returns raw bytes making up the secret key. |
| 235 | + /// |
| 236 | + /// **Example** |
| 237 | + /// ```dart |
| 238 | + /// import 'package:webcrypto/webcrypto.dart'; |
| 239 | + /// |
| 240 | + /// // Generate a new random AES-256 secret key. |
| 241 | + /// final key = await AesGcmSecretKey.generate(256); |
| 242 | + /// |
| 243 | + /// // Extract the secret key. |
| 244 | + /// final secretBytes = await key.exportRawKey(); |
| 245 | + /// |
| 246 | + /// // Print the key as base64 |
| 247 | + /// print(base64.encode(secretBytes)); |
| 248 | + /// |
| 249 | + /// // If we wanted to we could import the key as follows: |
| 250 | + /// // key = await AesGcmSecretKey.importRawKey(secretBytes); |
| 251 | + /// ``` |
50 | 252 | Future<Uint8List> exportRawKey();
|
51 | 253 |
|
| 254 | + /// Export [AesGcmSecretKey] as [JSON Web Key][1]. |
| 255 | + /// |
| 256 | + /// {@macro exportJsonWebKey:returns} |
| 257 | + /// |
| 258 | + /// **Example** |
| 259 | + /// ```dart |
| 260 | + /// import 'package:webcrypto/webcrypto.dart'; |
| 261 | + /// import 'dart:convert' show jsonEncode; |
| 262 | + /// |
| 263 | + /// // Generate a new random AES-256 secret key. |
| 264 | + /// final key = await AesGcmSecretKey.generate(256); |
| 265 | + /// |
| 266 | + /// // Export the secret key. |
| 267 | + /// final jwk = await key.exportJsonWebKey(); |
| 268 | + /// |
| 269 | + /// // The Map returned by `exportJsonWebKey()` can be converted to JSON with |
| 270 | + /// // `jsonEncode` from `dart:convert`, this will print something like: |
| 271 | + /// // {"kty": "oct", "alg": "A256GCM", "k": ...} |
| 272 | + /// print(jsonEncode(jwk)); |
| 273 | + /// ``` |
| 274 | + /// |
| 275 | + /// [1]: https://tools.ietf.org/html/rfc7517 |
52 | 276 | Future<Map<String, dynamic>> exportJsonWebKey();
|
53 | 277 | }
|
0 commit comments