Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
21 changes: 20 additions & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,23 @@ Style/PercentLiteralDelimiters:

Naming/MethodParameterName:
AllowedNames: ["iv", "b", "j", "a", "r", "t"]


Metrics/AbcSize:
Exclude:
- 'lib/jwe.rb'

Metrics/ParameterLists:
Exclude:
- 'lib/jwe.rb'

Metrics/MethodLength:
Exclude:
- 'lib/jwe.rb'

Lint/MissingSuper:
Exclude:
- 'lib/jwe/alg/dir.rb'
- 'lib/jwe/alg/rsa15.rb'
- 'lib/jwe/alg/rsa_oaep.rb'
- 'lib/jwe/alg/rsa_oaep_256.rb'

19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# Changelog

## Add support for AES-GCM key wrap algorithms(https://github.com/jwt/ruby-jwe/pull/38) (2025-10-08)

**Features:**
- Add support for AES-GCM key wrap algorithms (A128GCMKW, A192GCMKW, A256GCMKW)
- Add new internal architecture with `Base`, `Validator`, `Header`, and `NameResolver` classes
- Improve code organization with refactored module structure
- RuboCop compliance improvements

**Deprecations:**

- Deprecated `JWE.check_params`, `JWE.check_alg`, `JWE.check_enc`, `JWE.check_zip`, `JWE.check_key`
(use `JWE::Validator` instead)
- Deprecated `JWE.param_to_class_name` (use `JWE::NameResolver` instead)
- Deprecated internal methods `JWE.apply_zip`, `JWE.generate_header`, `JWE.generate_serialization`

**Notes:**

All deprecated methods remain functional with deprecation warnings for backward compatibility.

## [v1.1.1](https://github.com/jwt/ruby-jwe/tree/v1.1.1) (2025-08-07)

[Full Changelog](https://github.com/jwt/ruby-jwe/compare/v1.1.0...v1.1.1)
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ source 'https://rubygems.org'

gemspec

gem 'pry'
gem 'rake'
gem 'rspec'
gem 'rubocop'
Expand Down
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,21 @@ encrypted = JWE.encrypt(payload, key, copyright: 'This is my stuff! All rights r
puts encrypted
```

This example uses AES-GCM key wrap algorithm (A128GCMKW, A192GCMKW, or A256GCMKW).

```ruby
require 'jwe'

key = SecureRandom.random_bytes(16) # 16 bytes for A128GCMKW, 24 for A192GCMKW, 32 for A256GCMKW
payload = "The quick brown fox jumps over the lazy dog."

encrypted = JWE.encrypt(payload, key, alg: 'A128GCMKW')
puts encrypted

plaintext = JWE.decrypt(encrypted, key)
puts plaintext #"The quick brown fox jumps over the lazy dog."
```

## Available Algorithms

The RFC 7518 JSON Web Algorithms (JWA) spec defines the algorithms for [encryption](https://tools.ietf.org/html/rfc7518#section-5.1)
Expand All @@ -105,9 +120,9 @@ Key management:
* ~~ECDH-ES+A128KW~~
* ~~ECDH-ES+A192KW~~
* ~~ECDH-ES+A256KW~~
* ~~A128GCMKW~~
* ~~A192GCMKW~~
* ~~A256GCMKW~~
* A128GCMKW
* A192GCMKW
* A256GCMKW
* ~~PBES2-HS256+A128KW~~
* ~~PBES2-HS384+A192KW~~
* ~~PBES2-HS512+A256KW~~
Expand Down
71 changes: 50 additions & 21 deletions lib/jwe.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@

require 'jwe/base64'
require 'jwe/serialization/compact'
require 'jwe/name_resolver'
require 'jwe/alg'
require 'jwe/enc'
require 'jwe/zip'
require 'jwe/validator'
require 'jwe/header'

# A ruby implementation of the RFC 7516 JSON Web Encryption (JWE) standard.
module JWE
Expand All @@ -18,68 +21,90 @@ class NotImplementedError < RuntimeError; end
class BadCEK < RuntimeError; end
class InvalidData < RuntimeError; end

VALID_ALG = ['RSA1_5', 'RSA-OAEP', 'RSA-OAEP-256', 'A128KW', 'A192KW', 'A256KW', 'dir', 'ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW', 'A128GCMKW', 'A192GCMKW', 'A256GCMKW', 'PBES2-HS256+A128KW', 'PBES2-HS384+A192KW', 'PBES2-HS512+A256KW'].freeze
VALID_ENC = %w[A128CBC-HS256 A192CBC-HS384 A256CBC-HS512 A128GCM A192GCM A256GCM].freeze
VALID_ZIP = ['DEF'].freeze

class << self
def encrypt(payload, key, alg: 'RSA-OAEP', enc: 'A128GCM', **more_headers)
header = generate_header(alg, enc, more_headers)
check_params(header, key)
def encrypt(payload, key, alg: 'RSA-OAEP', enc: 'A128GCM', zip: nil, **more_headers)
Validator.new.check_params(alg, enc, zip, key)
payload = Zip.for(zip).new.compress(payload) if zip

enc_cipher = Enc.for(enc)
enc_cipher.cek = key if alg == 'dir'

payload = apply_zip(header, payload, :compress)
alg_cipher = Alg.for(alg).new(key)
encrypted_cek = alg_cipher.encrypt(enc_cipher.cek)

cipher = Enc.for(enc)
cipher.cek = key if alg == 'dir'
header = Header.new.generate_header(alg_cipher, enc_cipher, zip, more_headers)

json_hdr = header.to_json
ciphertext = cipher.encrypt(payload, Base64.jwe_encode(json_hdr))
ciphertext = enc_cipher.encrypt(payload, Base64.jwe_encode(header))

generate_serialization(json_hdr, Alg.encrypt_cek(alg, key, cipher.cek), ciphertext, cipher)
Serialization::Compact.encode(header, encrypted_cek, enc_cipher.iv, ciphertext, enc_cipher.tag)
end

def decrypt(payload, key)
header, enc_key, iv, ciphertext, tag = Serialization::Compact.decode(payload)
header = JSON.parse(header)
check_params(header, key)
alg, enc, zip = header.values_at('alg', 'enc', 'zip')

Validator.new.check_params(alg, enc, zip, key)

alg_cipher = Alg.for(alg).new(key)
if alg_cipher.need_additional_header_parameters?
alg_cipher.iv = Base64.jwe_decode(header['iv'])
alg_cipher.tag = Base64.jwe_decode(header['tag'])
end

cek = alg_cipher.decrypt(enc_key)
enc_cipher = Enc.for(enc, cek, iv, tag)

cek = Alg.decrypt_cek(header['alg'], key, enc_key)
cipher = Enc.for(header['enc'], cek, iv, tag)
plaintext = enc_cipher.decrypt(ciphertext, payload.split('.').first)

plaintext = cipher.decrypt(ciphertext, payload.split('.').first)
return plaintext unless zip

apply_zip(header, plaintext, :decompress)
Zip.for(zip).new.decompress(plaintext)
end

# @deprecated Use Validator.new.check_params instead
def check_params(header, key)
warn '[DEPRECATION] `JWE.check_params` is deprecated. Use `JWE::Validator.new.check_params` instead.'
check_alg(header[:alg] || header['alg'])
check_enc(header[:enc] || header['enc'])
check_zip(header[:zip] || header['zip'])
check_key(key)
end

# @deprecated Use Validator.new.check_params instead
def check_alg(alg)
raise ArgumentError.new("\"#{alg}\" is not a valid alg method") unless VALID_ALG.include?(alg)
warn '[DEPRECATION] `JWE.check_alg` is deprecated. Please validate parameters manually.'
raise ArgumentError.new("\"#{alg}\" is not a valid alg method") unless Validator::VALID_ALG.include?(alg)
end

# @deprecated Use Validator.new.check_params instead
def check_enc(enc)
raise ArgumentError.new("\"#{enc}\" is not a valid enc method") unless VALID_ENC.include?(enc)
warn '[DEPRECATION] `JWE.check_enc` is deprecated. Please validate parameters manually.'
raise ArgumentError.new("\"#{enc}\" is not a valid enc method") unless Validator::VALID_ENC.include?(enc)
end

# @deprecated Use Validator.new.check_params instead
def check_zip(zip)
raise ArgumentError.new("\"#{zip}\" is not a valid zip method") unless zip.nil? || zip == '' || VALID_ZIP.include?(zip)
warn '[DEPRECATION] `JWE.check_zip` is deprecated. Please validate parameters manually.'
raise ArgumentError.new("\"#{zip}\" is not a valid zip method") unless zip.nil? || zip == '' || Validator::VALID_ZIP.include?(zip)
end

# @deprecated Use Validator.new.check_params instead
def check_key(key)
warn '[DEPRECATION] `JWE.check_key` is deprecated. Please validate parameters manually.'
raise ArgumentError.new('The key must not be nil or blank') if key.nil? || (key.is_a?(String) && key.strip == '')
end

# @deprecated Use NameResolver#param_to_class_name instead
def param_to_class_name(param)
warn '[DEPRECATION] `JWE.param_to_class_name` is deprecated. Use `JWE::NameResolver#param_to_class_name` instead.'
klass = param.gsub(/[-+]/, '_').downcase.sub(/^[a-z\d]*/) { ::Regexp.last_match(0).capitalize }
klass.gsub(/_([a-z\d]*)/i) { Regexp.last_match(1).capitalize }
end

# @deprecated Internal method, do not use
def apply_zip(header, data, direction)
warn '[DEPRECATION] `JWE.apply_zip` is deprecated. This is an internal method and should not be used externally.'
zip = header[:zip] || header['zip']
if zip
Zip.for(zip).new.send(direction, data)
Expand All @@ -88,13 +113,17 @@ def apply_zip(header, data, direction)
end
end

# @deprecated Use Header.new.generate_header instead
def generate_header(alg, enc, more)
warn '[DEPRECATION] `JWE.generate_header` is deprecated. This is an internal method and should not be used externally.'
header = { alg: alg, enc: enc }.merge(more)
header.delete(:zip) if header[:zip] == ''
header
end

# @deprecated Use Serialization::Compact.encode instead
def generate_serialization(hdr, cek, content, cipher)
warn '[DEPRECATION] `JWE.generate_serialization` is deprecated. Use `JWE::Serialization::Compact.encode` instead.'
Serialization::Compact.encode(hdr, cek, cipher.iv, content, cipher.tag)
end
end
Expand Down
8 changes: 7 additions & 1 deletion lib/jwe/alg.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# frozen_string_literal: true

require 'jwe/alg/base'
require 'jwe/alg/a128_kw'
require 'jwe/alg/a192_kw'
require 'jwe/alg/a256_kw'
require 'jwe/alg/a128gcmkw'
require 'jwe/alg/a192gcmkw'
require 'jwe/alg/a256gcmkw'
require 'jwe/alg/dir'
require 'jwe/alg/rsa_oaep'
require 'jwe/alg/rsa_oaep_256' if OpenSSL::VERSION >= '3.0'
Expand All @@ -11,8 +15,10 @@
module JWE
# Key encryption algorithms namespace
module Alg
extend JWE::NameResolver

def self.for(alg)
const_get(JWE.param_to_class_name(alg))
const_get(param_to_class_name(alg))
rescue NameError
raise NotImplementedError.new("Unsupported alg type: #{alg}")
end
Expand Down
2 changes: 1 addition & 1 deletion lib/jwe/alg/a128_kw.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
module JWE
module Alg
# AES-128 Key Wrapping algorithm
class A128kw
class A128kw < Base
include AesKw

def cipher_name
Expand Down
30 changes: 30 additions & 0 deletions lib/jwe/alg/a128gcmkw.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

require 'jwe/alg/aes_gcm'

module JWE
module Alg
# AES-128 key wrap with GCM algorithm
class A128gcmkw < Base
include AesGcm

def initialize(key, iv = nil)
super
end

private

def key_length
16
end

def cipher_name
'aes-128-gcm'
end

def required_additional_header_parameters?
true
end
end
end
end
2 changes: 1 addition & 1 deletion lib/jwe/alg/a192_kw.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
module JWE
module Alg
# AES-192 Key Wrapping algorithm
class A192kw
class A192kw < Base
include AesKw

def cipher_name
Expand Down
30 changes: 30 additions & 0 deletions lib/jwe/alg/a192gcmkw.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

require 'jwe/alg/aes_gcm'

module JWE
module Alg
# AES-192 key wrap with GCM algorithm
class A192gcmkw < Base
include AesGcm

def initialize(key, iv = nil)
super
end

private

def key_length
24
end

def cipher_name
'aes-192-gcm'
end

def required_additional_header_parameters?
true
end
end
end
end
30 changes: 30 additions & 0 deletions lib/jwe/alg/a256gcmkw.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

require 'jwe/alg/aes_gcm'

module JWE
module Alg
# AES-256 key wrap with GCM algorithm
class A256gcmkw < Base
include AesGcm

def initialize(key, iv = nil)
super
end

private

def key_length
32
end

def cipher_name
'aes-256-gcm'
end

def required_additional_header_parameters?
true
end
end
end
end
Loading