All files / crypto / unstable_aes_gcm.ts

100.00% Branches 16/16
100.00% Functions 2/2
100.00% Lines 47/47
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x4
x4
x4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x4
x4
x4
x4
 
x19
 
x19
x19
x19
x19
x19
x19
x3
x3
 
x19
x19
 
 
x19
x19
x19
x19
x19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x4
x4
x4
x4
 
x17
x17
x17
 
x17
x4
x4
 
x4
 
x13
x13
 
x13
x13
x13
x13
x13
x17
x2
x2
 
x17
x17
 
x17



































































































































































// Copyright 2018-2026 the Deno authors. MIT license.
// This module is browser compatible.

import type { Uint8Array_ } from "./_types.ts";
export type { Uint8Array_ };

/**
 * High-level AES-GCM authenticated encryption with automatic nonce generation.
 *
 * With random nonces, do not encrypt more than ~2^32 messages under the same
 * key. Beyond this limit, nonce collision probability becomes non-negligible.
 *
 * @example Usage
 * ```ts
 * import { encryptAesGcm, decryptAesGcm } from "@std/crypto/unstable-aes-gcm";
 * import { assertEquals } from "@std/assert";
 *
 * const key = await crypto.subtle.generateKey(
 *   { name: "AES-GCM", length: 256 },
 *   false,
 *   ["encrypt", "decrypt"],
 * );
 *
 * const plaintext = new TextEncoder().encode("hello world");
 * const encrypted = await encryptAesGcm(key, plaintext);
 * const decrypted = await decryptAesGcm(key, encrypted);
 *
 * assertEquals(decrypted, plaintext);
 * ```
 *
 * @module
 */

const NONCE_LENGTH = 12;
const TAG_LENGTH = 16;
const OVERHEAD = NONCE_LENGTH + TAG_LENGTH;

/** Options for {@linkcode encryptAesGcm} and {@linkcode decryptAesGcm}. */
export interface AesGcmOptions {
  /** Additional authenticated data. Authenticated but not encrypted. */
  additionalData?: BufferSource;
}

/**
 * Encrypts plaintext using AES-GCM with a random 96-bit nonce.
 *
 * Returns `nonce (12 bytes) || ciphertext || tag (16 bytes)`.
 *
 * @example Usage
 * ```ts
 * import { encryptAesGcm } from "@std/crypto/unstable-aes-gcm";
 * import { assertNotEquals } from "@std/assert";
 *
 * const key = await crypto.subtle.generateKey(
 *   { name: "AES-GCM", length: 256 },
 *   false,
 *   ["encrypt", "decrypt"],
 * );
 *
 * const encrypted = await encryptAesGcm(
 *   key,
 *   new TextEncoder().encode("hello world"),
 * );
 *
 * assertNotEquals(encrypted.length, 0);
 * ```
 *
 * @param key The AES-GCM `CryptoKey` to encrypt with.
 * @param plaintext The data to encrypt.
 * @param options Optional additional authenticated data.
 * @returns The concatenated nonce, ciphertext, and authentication tag.
 *
 * @remarks With random nonces, do not encrypt more than ~2^32 messages
 * under the same key. Beyond this limit, nonce collision probability
 * becomes non-negligible.
 *
 * @see {@link https://csrc.nist.gov/pubs/sp/800/38/d/final | NIST SP 800-38D} Section 8.3
 */
export async function encryptAesGcm(
  key: CryptoKey,
  plaintext: BufferSource,
  options?: AesGcmOptions,
): Promise<Uint8Array_> {
  const nonce = crypto.getRandomValues(new Uint8Array(NONCE_LENGTH));

  const params: AesGcmParams = {
    name: "AES-GCM",
    iv: nonce,
    tagLength: TAG_LENGTH * 8,
  };
  if (options?.additionalData !== undefined) {
    params.additionalData = options.additionalData;
  }

  const ciphertextAndTag = new Uint8Array(
    await crypto.subtle.encrypt(params, key, plaintext),
  );

  const result = new Uint8Array(NONCE_LENGTH + ciphertextAndTag.byteLength);
  result.set(nonce);
  result.set(ciphertextAndTag, NONCE_LENGTH);
  return result;
}

/**
 * Decrypts data produced by {@linkcode encryptAesGcm}.
 *
 * Expects input in the format `nonce (12 bytes) || ciphertext || tag (16 bytes)`.
 *
 * @example Usage
 * ```ts
 * import { decryptAesGcm, encryptAesGcm } from "@std/crypto/unstable-aes-gcm";
 * import { assertEquals } from "@std/assert";
 *
 * const key = await crypto.subtle.generateKey(
 *   { name: "AES-GCM", length: 256 },
 *   false,
 *   ["encrypt", "decrypt"],
 * );
 *
 * const plaintext = new TextEncoder().encode("hello world");
 * const encrypted = await encryptAesGcm(key, plaintext);
 *
 * assertEquals(await decryptAesGcm(key, encrypted), plaintext);
 * ```
 *
 * @param key The AES-GCM `CryptoKey` to decrypt with.
 * @param data The wire-format output from {@linkcode encryptAesGcm}: nonce (12 B) || ciphertext || tag (16 B).
 * @param options Optional additional authenticated data (must match what was used during encryption).
 * @returns The decrypted plaintext.
 * @throws {RangeError} If `data` is shorter than 28 bytes (12 nonce + 16 tag).
 * @throws {DOMException} If authentication fails (wrong key, tampered data, or
 * mismatched additional data).
 */
export async function decryptAesGcm(
  key: CryptoKey,
  data: BufferSource,
  options?: AesGcmOptions,
): Promise<Uint8Array_> {
  const bytes = ArrayBuffer.isView(data)
    ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
    : new Uint8Array(data);

  if (bytes.byteLength < OVERHEAD) {
    throw new RangeError(
      `Data is too short: expected at least ${OVERHEAD} bytes, got ${bytes.byteLength}`,
    );
  }

  const nonce = bytes.subarray(0, NONCE_LENGTH);
  const ciphertextAndTag = bytes.subarray(NONCE_LENGTH);

  const params: AesGcmParams = {
    name: "AES-GCM",
    iv: nonce,
    tagLength: TAG_LENGTH * 8,
  };
  if (options?.additionalData !== undefined) {
    params.additionalData = options.additionalData;
  }

  return new Uint8Array(
    await crypto.subtle.decrypt(params, key, ciphertextAndTag),
  );
}