Module: Clacky::AesGcm

Defined in:
lib/clacky/aes_gcm.rb

Overview

Pure-Ruby AES-256-GCM implementation.

Why this exists:

macOS ships Ruby 2.6 linked against LibreSSL 3.3.x which has a known
bug: AES-GCM encrypt/decrypt raises CipherError even for valid inputs.
This implementation uses AES-256-ECB (which LibreSSL supports correctly)
as the single block-cipher primitive and builds GCM on top:

  - CTR mode   → keystream for encryption / decryption
  - GHASH      → authentication tag

The output is 100% compatible with OpenSSL / standard AES-256-GCM:

ciphertext, iv, and auth_tag produced here can be decrypted by OpenSSL
and vice-versa.

Reference: NIST SP 800-38D

Usage:

ct, tag = AesGcm.encrypt(key, iv, plaintext, aad)
pt      = AesGcm.decrypt(key, iv, ciphertext, tag, aad)

Constant Summary collapse

BLOCK_SIZE =
16
TAG_LENGTH =
16
R =

Galois Field GF(2^128) multiplication. Reduction polynomial: x^128 + x^7 + x^2 + x + 1 Uses the reflected bit order per GCM spec.

0xe1000000000000000000000000000000

Class Method Summary collapse

Class Method Details

.decrypt(key, iv, ciphertext, tag, aad = "") ⇒ String

Decrypt ciphertext with AES-256-GCM and verify auth tag.

Parameters:

  • key (String)

    32-byte binary key

  • iv (String)

    12-byte binary IV

  • ciphertext (String)

    binary ciphertext

  • tag (String)

    16-byte binary auth tag

  • aad (String) (defaults to: "")

    additional authenticated data (may be empty)

Returns:

  • (String)

    plaintext (UTF-8)

Raises:

  • (OpenSSL::Cipher::CipherError)

    on authentication failure



56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/clacky/aes_gcm.rb', line 56

def self.decrypt(key, iv, ciphertext, tag, aad = "")
  aes       = aes_ecb(key)
  h         = aes.call("\x00" * BLOCK_SIZE)
  j0        = build_j0(iv, h)
  exp_tag   = compute_tag(aes, h, j0, ciphertext, aad.b)

  unless secure_compare(exp_tag, tag)
    raise OpenSSL::Cipher::CipherError, "bad decrypt (authentication tag mismatch)"
  end

  ctr_crypt(aes, inc32(j0), ciphertext).force_encoding("UTF-8")
end

.encrypt(key, iv, plaintext, aad = "") ⇒ Array<String, String>

Encrypt plaintext with AES-256-GCM.

Parameters:

  • key (String)

    32-byte binary key

  • iv (String)

    12-byte binary IV (recommended for GCM)

  • plaintext (String)

    binary or UTF-8 plaintext

  • aad (String) (defaults to: "")

    additional authenticated data (may be empty)

Returns:

  • (Array<String, String>)
    ciphertext, auth_tag

    both binary strings



38
39
40
41
42
43
44
45
# File 'lib/clacky/aes_gcm.rb', line 38

def self.encrypt(key, iv, plaintext, aad = "")
  aes  = aes_ecb(key)
  h    = aes.call("\x00" * BLOCK_SIZE)              # H = E(K, 0^128)
  j0   = build_j0(iv, h)
  ct   = ctr_crypt(aes, inc32(j0), plaintext.b)
  tag  = compute_tag(aes, h, j0, ct, aad.b)
  [ct, tag]
end