Skip to content

Commit 7b803be

Browse files
committed
- 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 - 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`
1 parent 6a2e506 commit 7b803be

31 files changed

+455
-40
lines changed

.rubocop.yml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,19 @@ Style/PercentLiteralDelimiters:
1818

1919
Naming/MethodParameterName:
2020
AllowedNames: ["iv", "b", "j", "a", "r", "t"]
21-
21+
22+
Metrics/AbcSize:
23+
Exclude:
24+
- 'lib/jwe.rb'
25+
26+
Metrics/ParameterLists:
27+
Exclude:
28+
- 'lib/jwe.rb'
29+
30+
Lint/MissingSuper:
31+
Exclude:
32+
- 'lib/jwe/alg/dir.rb'
33+
- 'lib/jwe/alg/rsa15.rb'
34+
- 'lib/jwe/alg/rsa_oaep.rb'
35+
- 'lib/jwe/alg/rsa_oaep_256.rb'
36+

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
# Changelog
22

3+
## [v1.2.0](https://github.com/jwt/ruby-jwe/tree/v1.2.0) (2025-10-08)
4+
5+
[Full Changelog](https://github.com/jwt/ruby-jwe/compare/v1.1.1...v1.2.0)
6+
7+
**Features:**
8+
9+
- Add support for AES-GCM key wrap algorithms (A128GCMKW, A192GCMKW, A256GCMKW)
10+
- Add new internal architecture with `Base`, `Validator`, `Header`, and `NameResolver` classes
11+
- Improve code organization with refactored module structure
12+
- RuboCop compliance improvements
13+
14+
**Deprecations:**
15+
16+
- Deprecated `JWE.check_params`, `JWE.check_alg`, `JWE.check_enc`, `JWE.check_zip`, `JWE.check_key` (use `JWE::Validator` instead)
17+
- Deprecated `JWE.param_to_class_name` (use `JWE::NameResolver` instead)
18+
- Deprecated internal methods `JWE.apply_zip`, `JWE.generate_header`, `JWE.generate_serialization`
19+
20+
**Notes:**
21+
22+
All deprecated methods remain functional with deprecation warnings for backward compatibility.
23+
324
## [v1.1.1](https://github.com/jwt/ruby-jwe/tree/v1.1.1) (2025-08-07)
425

526
[Full Changelog](https://github.com/jwt/ruby-jwe/compare/v1.1.0...v1.1.1)

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ source 'https://rubygems.org'
44

55
gemspec
66

7+
gem 'pry'
78
gem 'rake'
89
gem 'rspec'
910
gem 'rubocop'

README.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,21 @@ encrypted = JWE.encrypt(payload, key, copyright: 'This is my stuff! All rights r
8686
puts encrypted
8787
```
8888

89+
This example uses AES-GCM key wrap algorithm (A128GCMKW, A192GCMKW, or A256GCMKW).
90+
91+
```ruby
92+
require 'jwe'
93+
94+
key = SecureRandom.random_bytes(16) # 16 bytes for A128GCMKW, 24 for A192GCMKW, 32 for A256GCMKW
95+
payload = "The quick brown fox jumps over the lazy dog."
96+
97+
encrypted = JWE.encrypt(payload, key, alg: 'A128GCMKW')
98+
puts encrypted
99+
100+
plaintext = JWE.decrypt(encrypted, key)
101+
puts plaintext #"The quick brown fox jumps over the lazy dog."
102+
```
103+
89104
## Available Algorithms
90105

91106
The RFC 7518 JSON Web Algorithms (JWA) spec defines the algorithms for [encryption](https://tools.ietf.org/html/rfc7518#section-5.1)
@@ -105,9 +120,9 @@ Key management:
105120
* ~~ECDH-ES+A128KW~~
106121
* ~~ECDH-ES+A192KW~~
107122
* ~~ECDH-ES+A256KW~~
108-
* ~~A128GCMKW~~
109-
* ~~A192GCMKW~~
110-
* ~~A256GCMKW~~
123+
* A128GCMKW
124+
* A192GCMKW
125+
* A256GCMKW
111126
* ~~PBES2-HS256+A128KW~~
112127
* ~~PBES2-HS384+A192KW~~
113128
* ~~PBES2-HS512+A256KW~~

lib/jwe.rb

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77

88
require 'jwe/base64'
99
require 'jwe/serialization/compact'
10+
require 'jwe/name_resolver'
1011
require 'jwe/alg'
1112
require 'jwe/enc'
1213
require 'jwe/zip'
14+
require 'jwe/validator'
15+
require 'jwe/header'
1316

1417
# A ruby implementation of the RFC 7516 JSON Web Encryption (JWE) standard.
1518
module JWE
@@ -18,68 +21,84 @@ class NotImplementedError < RuntimeError; end
1821
class BadCEK < RuntimeError; end
1922
class InvalidData < RuntimeError; end
2023

21-
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
22-
VALID_ENC = %w[A128CBC-HS256 A192CBC-HS384 A256CBC-HS512 A128GCM A192GCM A256GCM].freeze
23-
VALID_ZIP = ['DEF'].freeze
24-
2524
class << self
26-
def encrypt(payload, key, alg: 'RSA-OAEP', enc: 'A128GCM', **more_headers)
27-
header = generate_header(alg, enc, more_headers)
28-
check_params(header, key)
25+
def encrypt(payload, key, alg: 'RSA-OAEP', enc: 'A128GCM', zip: nil, **more_headers)
26+
Validator.new.check_params(alg, enc, zip, key)
27+
payload = Zip.for(zip).new.compress(payload) if zip
28+
29+
enc_cipher = Enc.for(enc)
30+
enc_cipher.cek = key if alg == 'dir'
2931

30-
payload = apply_zip(header, payload, :compress)
32+
alg_cipher = Alg.for(alg).new(key)
33+
encrypted_cek = alg_cipher.encrypt(enc_cipher.cek)
3134

32-
cipher = Enc.for(enc)
33-
cipher.cek = key if alg == 'dir'
35+
header = Header.new.generate_header(alg_cipher, enc_cipher, zip, more_headers)
3436

35-
json_hdr = header.to_json
36-
ciphertext = cipher.encrypt(payload, Base64.jwe_encode(json_hdr))
37+
ciphertext = enc_cipher.encrypt(payload, Base64.jwe_encode(header))
3738

38-
generate_serialization(json_hdr, Alg.encrypt_cek(alg, key, cipher.cek), ciphertext, cipher)
39+
Serialization::Compact.encode(header, encrypted_cek, enc_cipher.iv, ciphertext, enc_cipher.tag)
3940
end
4041

4142
def decrypt(payload, key)
4243
header, enc_key, iv, ciphertext, tag = Serialization::Compact.decode(payload)
4344
header = JSON.parse(header)
44-
check_params(header, key)
45+
alg, enc, zip = header.values_at('alg', 'enc', 'zip')
4546

46-
cek = Alg.decrypt_cek(header['alg'], key, enc_key)
47-
cipher = Enc.for(header['enc'], cek, iv, tag)
47+
Validator.new.check_params(alg, enc, zip, key)
48+
49+
cek = Alg.decrypt_cek(alg, key, enc_key)
50+
cipher = Enc.for(enc, cek, iv, tag)
4851

4952
plaintext = cipher.decrypt(ciphertext, payload.split('.').first)
5053

51-
apply_zip(header, plaintext, :decompress)
54+
return plaintext unless zip
55+
56+
Zip.for(zip).new.decompress(plaintext)
5257
end
5358

59+
# @deprecated Use Validator.new.check_params instead
5460
def check_params(header, key)
61+
warn '[DEPRECATION] `JWE.check_params` is deprecated. Use `JWE::Validator.new.check_params` instead.'
5562
check_alg(header[:alg] || header['alg'])
5663
check_enc(header[:enc] || header['enc'])
5764
check_zip(header[:zip] || header['zip'])
5865
check_key(key)
5966
end
6067

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

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

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

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

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

99+
# @deprecated Internal method, do not use
82100
def apply_zip(header, data, direction)
101+
warn '[DEPRECATION] `JWE.apply_zip` is deprecated. This is an internal method and should not be used externally.'
83102
zip = header[:zip] || header['zip']
84103
if zip
85104
Zip.for(zip).new.send(direction, data)
@@ -88,13 +107,17 @@ def apply_zip(header, data, direction)
88107
end
89108
end
90109

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

118+
# @deprecated Use Serialization::Compact.encode instead
97119
def generate_serialization(hdr, cek, content, cipher)
120+
warn '[DEPRECATION] `JWE.generate_serialization` is deprecated. Use `JWE::Serialization::Compact.encode` instead.'
98121
Serialization::Compact.encode(hdr, cek, cipher.iv, content, cipher.tag)
99122
end
100123
end

lib/jwe/alg.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# frozen_string_literal: true
22

3+
require 'jwe/alg/base'
34
require 'jwe/alg/a128_kw'
45
require 'jwe/alg/a192_kw'
56
require 'jwe/alg/a256_kw'
@@ -11,8 +12,10 @@
1112
module JWE
1213
# Key encryption algorithms namespace
1314
module Alg
15+
extend JWE::NameResolver
16+
1417
def self.for(alg)
15-
const_get(JWE.param_to_class_name(alg))
18+
const_get(param_to_class_name(alg))
1619
rescue NameError
1720
raise NotImplementedError.new("Unsupported alg type: #{alg}")
1821
end

lib/jwe/alg/a128_kw.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
module JWE
66
module Alg
77
# AES-128 Key Wrapping algorithm
8-
class A128kw
8+
class A128kw < Base
99
include AesKw
1010

1111
def cipher_name

lib/jwe/alg/a128gcmkw.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# frozen_string_literal: true
2+
3+
require 'jwe/alg/aes_gcm'
4+
5+
module JWE
6+
module Alg
7+
# AES-128 key wrap with GCM algorithm
8+
class A128gcmkw < Base
9+
include AesGcm
10+
11+
def initialize(key, iv = nil)
12+
super
13+
end
14+
15+
private
16+
17+
def key_length
18+
16
19+
end
20+
21+
def cipher_name
22+
'aes-128-gcm'
23+
end
24+
25+
def required_additional_header_parameters?
26+
true
27+
end
28+
end
29+
end
30+
end

lib/jwe/alg/a192_kw.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
module JWE
66
module Alg
77
# AES-192 Key Wrapping algorithm
8-
class A192kw
8+
class A192kw < Base
99
include AesKw
1010

1111
def cipher_name

lib/jwe/alg/a192gcmkw.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# frozen_string_literal: true
2+
3+
require 'jwe/alg/aes_gcm'
4+
5+
module JWE
6+
module Alg
7+
# AES-192 key wrap with GCM algorithm
8+
class A192gcmkw < Base
9+
include AesGcm
10+
11+
def initialize(key, iv = nil)
12+
super
13+
end
14+
15+
private
16+
17+
def key_length
18+
24
19+
end
20+
21+
def cipher_name
22+
'aes-192-gcm'
23+
end
24+
25+
def required_additional_header_parameters?
26+
true
27+
end
28+
end
29+
end
30+
end

0 commit comments

Comments
 (0)