legion-crypt 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8dc8ac30453ea23fbdd8f8b2a11b0168fcd1d5d2d1bb8cfd289f96bd6fdc2620
4
- data.tar.gz: fa52ef3ddeb5da0f0ea151c178612832f46da404fd1c5c145dcc86f53570250c
3
+ metadata.gz: a7018b9c084879712ba6a2fdf0b64f19b1c78de8cb8b9377063f824a0f43314c
4
+ data.tar.gz: 0d449cbe402ffbe5a45256801ea442ac03d7663f234f4678f52147e55214c0c0
5
5
  SHA512:
6
- metadata.gz: a538b27d572615fc9a14dcb026b8641a4459438c24e689584166a82c97fd234ea19d5d55600b60c0e7c5eb688cb8c28838b1c1b30bb275691a124544ce93ea50
7
- data.tar.gz: 1cfd861ed4f57d538dc25640e69178cfa638c1e529dab1b24402b202173488f42447fb3553e08516593b64ff7f39be6bfdf1f944683d7a2a5c22f3d59f7692a4
6
+ metadata.gz: 64e62d71d928dbd378aa7ce371af98b79c0a70cd6724461db87c7753f073014e8d0973975b947c31cdcda34f138f473afcf11d5026ec5332e7b5d619b91e72f8
7
+ data.tar.gz: 42b44f3d2b203db36197dc399732790fe3e6cc6f9f00b9e3c3660fc1283cf6bbd519fbaab718ddaa44a404f5332e3d8f0eb24120d17ac41f2071df637fc61634
@@ -0,0 +1,16 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+
7
+ jobs:
8
+ ci:
9
+ uses: LegionIO/.github/.github/workflows/ci.yml@main
10
+
11
+ release:
12
+ needs: ci
13
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
14
+ uses: LegionIO/.github/.github/workflows/release.yml@main
15
+ secrets:
16
+ rubygems-api-key: ${{ secrets.RUBYGEMS_API_KEY }}
data/.rubocop.yml CHANGED
@@ -1,26 +1,53 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.4
3
+ NewCops: enable
4
+ SuggestExtensions: false
5
+
1
6
  Layout/LineLength:
2
- Max: 140
7
+ Max: 160
8
+
9
+ Layout/SpaceAroundEqualsInParameterDefault:
10
+ EnforcedStyle: space
11
+
12
+ Layout/HashAlignment:
13
+ EnforcedHashRocketStyle: table
14
+ EnforcedColonStyle: table
15
+
3
16
  Metrics/MethodLength:
4
17
  Max: 50
18
+
5
19
  Metrics/ClassLength:
6
20
  Max: 1500
21
+
22
+ Metrics/ModuleLength:
23
+ Max: 1500
24
+
7
25
  Metrics/BlockLength:
8
- Max: 50
9
- Metrics/CyclomaticComplexity:
10
- Max: 14
26
+ Max: 40
27
+ Exclude:
28
+ - 'spec/**/*'
29
+
11
30
  Metrics/AbcSize:
12
- Max: 17
31
+ Max: 60
32
+
33
+ Metrics/CyclomaticComplexity:
34
+ Max: 15
35
+
13
36
  Metrics/PerceivedComplexity:
14
- Max: 16
15
- Naming/MethodParameterName:
16
- Enabled: false
37
+ Max: 17
38
+
17
39
  Style/Documentation:
18
40
  Enabled: false
19
- AllCops:
20
- TargetRubyVersion: 2.6
21
- NewCops: enable
22
- SuggestExtensions: false
41
+
42
+ Style/SymbolArray:
43
+ Enabled: true
44
+
23
45
  Style/FrozenStringLiteralComment:
46
+ Enabled: true
47
+ EnforcedStyle: always
48
+
49
+ Naming/FileName:
50
+ Enabled: false
51
+
52
+ Naming/PredicateMethod:
24
53
  Enabled: false
25
- Gemspec/RequiredRubyVersion:
26
- Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,4 +1,31 @@
1
1
  # Legion::Crypt
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ## [1.3.0] - 2026-03-16
6
+
7
+ ### Added
8
+ - `LeaseManager` singleton for dynamic Vault secret lease management
9
+ - Named lease definitions in `crypt.vault.leases` settings
10
+ - Boot-time lease fetch with data caching
11
+ - Background renewal thread with rotation detection
12
+ - Settings push-back on credential rotation via reverse index
13
+ - `lease://name#key` URI references resolved by Settings resolver
14
+
15
+ ## v1.2.1
16
+
17
+ ### Fixed
18
+ - `validate_hex` and `set_cluster_secret` now handle leading zeros correctly by padding the
19
+ base-32 round-trip result back to the original string length. Previously, secrets whose
20
+ hex representation started with one or more zero bytes would fail validation and cause
21
+ `find_cluster_secret` to return nil non-deterministically.
22
+
23
+ ### Added
24
+ - Comprehensive spec coverage for `Legion::Crypt::VaultJwtAuth` (`.login`, `.login!`,
25
+ `.worker_login`, `AuthError`, constants).
26
+ - `after` hook in `cluster_secret_spec` to restore `Legion::Settings[:crypt][:cluster_secret]`
27
+ between examples, eliminating ordering-dependent state pollution.
28
+ - TODO comments in `vault_spec` for tests that require live Vault connectivity.
29
+
3
30
  ## v1.2.0
4
- Moving from BitBucket to GitHub inside the Optum org. All git history is reset from this point on
31
+ Moving from BitBucket to GitHub. All git history is reset from this point on
data/CLAUDE.md ADDED
@@ -0,0 +1,149 @@
1
+ # legion-crypt: Encryption and Vault Integration for LegionIO
2
+
3
+ **Repository Level 3 Documentation**
4
+ - **Parent**: `/Users/miverso2/rubymine/legion/CLAUDE.md`
5
+
6
+ ## Purpose
7
+
8
+ Handles encryption, decryption, secrets management, JWT token management, and HashiCorp Vault connectivity for the LegionIO framework. Provides AES-256-CBC message encryption, RSA key pair generation, cluster secret management, JWT issue/verify operations, and Vault token lifecycle management.
9
+
10
+ **GitHub**: https://github.com/LegionIO/legion-crypt
11
+ **License**: Apache-2.0
12
+
13
+ ## Architecture
14
+
15
+ ```
16
+ Legion::Crypt (singleton module)
17
+ ├── .start # Initialize: generate keys, connect to Vault
18
+ ├── .encrypt(string) # AES-256-CBC encryption
19
+ ├── .decrypt(message) # AES-256-CBC decryption
20
+ ├── .shutdown # Stop Vault renewer, close sessions
21
+
22
+ ├── Cipher # OpenSSL cipher operations (AES-256-CBC)
23
+ │ ├── .encrypt # Encrypt with cluster secret
24
+ │ ├── .decrypt # Decrypt with cluster secret
25
+ │ ├── .private_key # RSA private key (generated or loaded)
26
+ │ └── .public_key # RSA public key
27
+
28
+ ├── Vault # HashiCorp Vault integration
29
+ │ ├── .connect_vault # Establish Vault session
30
+ │ ├── .read(path) # Read secret from Vault
31
+ │ ├── .write(path) # Write secret to Vault
32
+ │ └── .renew_token # Token renewal
33
+
34
+ ├── JWT # JSON Web Token operations
35
+ │ ├── .issue # Create signed JWT (HS256 or RS256)
36
+ │ ├── .verify # Verify and decode JWT
37
+ │ └── .decode # Decode without verification (inspection)
38
+
39
+ ├── ClusterSecret # Cluster-wide shared secret management
40
+ │ └── .cs # Generate/distribute cluster secret
41
+
42
+ ├── VaultJwtAuth # Vault JWT auth backend integration
43
+ │ ├── .login # Authenticate to Vault using a JWT token, returns Vault token hash
44
+ │ ├── .login! # Authenticate and set ::Vault.token for subsequent operations
45
+ │ └── .worker_login # Issue a Legion JWT and authenticate to Vault in one step
46
+
47
+ ├── VaultRenewer # Background Vault token renewal thread
48
+ ├── LeaseManager # Dynamic Vault lease lifecycle: fetch, cache, renew, rotate, push-back
49
+ ├── Settings # Default crypt config
50
+ └── Version
51
+ ```
52
+
53
+ ### Key Design Patterns
54
+
55
+ - **Dynamic Keys**: By default, generates new RSA key pair per process start (no persistent keys)
56
+ - **Cluster Secret**: Shared AES key distributed across Legion nodes for inter-node encrypted communication
57
+ - **Vault Conditional**: Vault module is only included if the `vault` gem is available
58
+ - **Token Lifecycle**: VaultRenewer runs background thread for automatic token renewal
59
+ - **JWT Dual Algorithm**: HS256 (symmetric, cluster secret) for intra-cluster tokens; RS256 (asymmetric, RSA keypair) for tokens verifiable without sharing the signing key
60
+
61
+ ## Default Settings
62
+
63
+ ```json
64
+ {
65
+ "vault": { "..." : "see vault settings" },
66
+ "jwt": {
67
+ "enabled": true,
68
+ "default_algorithm": "HS256",
69
+ "default_ttl": 3600,
70
+ "issuer": "legion",
71
+ "verify_expiration": true,
72
+ "verify_issuer": true
73
+ },
74
+ "cs_encrypt_ready": false,
75
+ "dynamic_keys": true,
76
+ "cluster_secret": null,
77
+ "save_private_key": true,
78
+ "read_private_key": true
79
+ }
80
+ ```
81
+
82
+ ## Dependencies
83
+
84
+ | Gem | Purpose |
85
+ |-----|---------|
86
+ | `jwt` (>= 2.7) | JSON Web Token encoding/decoding |
87
+ | `vault` (>= 0.17) | HashiCorp Vault Ruby client |
88
+
89
+ Dev dependencies: `legion-logging`, `legion-settings`
90
+
91
+ ## File Map
92
+
93
+ | Path | Purpose |
94
+ |------|---------|
95
+ | `lib/legion/crypt.rb` | Module entry, start/shutdown lifecycle |
96
+ | `lib/legion/crypt/cipher.rb` | AES-256-CBC encrypt/decrypt, RSA key generation |
97
+ | `lib/legion/crypt/jwt.rb` | JWT issue/verify/decode operations |
98
+ | `lib/legion/crypt/vault.rb` | Vault read/write/connect/renew operations |
99
+ | `lib/legion/crypt/cluster_secret.rb` | Cluster-wide shared secret management |
100
+ | `lib/legion/crypt/vault_jwt_auth.rb` | Vault JWT auth backend: `.login`, `.login!`, `.worker_login`; raises `AuthError` on failure |
101
+ | `lib/legion/crypt/vault_renewer.rb` | Background Vault token renewal |
102
+ | `lib/legion/crypt/lease_manager.rb` | Dynamic Vault lease lifecycle management |
103
+ | `lib/legion/crypt/settings.rb` | Default configuration |
104
+ | `lib/legion/crypt/version.rb` | VERSION constant |
105
+
106
+ ## Role in LegionIO
107
+
108
+ First service-level module initialized during `Legion::Service` startup (before transport). Provides:
109
+ 1. Vault token for `legion-transport` to fetch RabbitMQ credentials
110
+ 2. Message encryption for `legion-transport` (optional `transport.messages.encrypt`)
111
+ 3. Cluster secret for inter-node encrypted communication
112
+ 4. JWT tokens for node authentication and task authorization
113
+
114
+ ### Vault JWT Auth Usage
115
+
116
+ ```ruby
117
+ # Authenticate to Vault using a JWT (Vault must have JWT auth method enabled)
118
+ result = Legion::Crypt::VaultJwtAuth.login(jwt: token, role: 'legion-worker')
119
+ # => { token: '...', lease_duration: 3600, renewable: true, policies: [...], metadata: {} }
120
+
121
+ # Authenticate and set Vault client token in one step
122
+ Legion::Crypt::VaultJwtAuth.login!(jwt: token)
123
+
124
+ # Issue a Legion JWT and use it to authenticate to Vault (convenience for workers)
125
+ result = Legion::Crypt::VaultJwtAuth.worker_login(worker_id: 'abc', owner_msid: 'user@example.com')
126
+ ```
127
+
128
+ Vault prerequisites: `vault auth enable jwt` + configure `auth/jwt/config` with JWKS URL or bound issuer.
129
+
130
+ ### JWT Usage
131
+
132
+ ```ruby
133
+ # Convenience methods (auto-selects keys from settings)
134
+ token = Legion::Crypt.issue_token({ node_id: 'abc' }, ttl: 3600)
135
+ claims = Legion::Crypt.verify_token(token)
136
+
137
+ # Direct module usage (explicit keys)
138
+ token = Legion::Crypt::JWT.issue(payload, signing_key: key, algorithm: 'RS256')
139
+ claims = Legion::Crypt::JWT.verify(token, verification_key: pub_key, algorithm: 'RS256')
140
+ decoded = Legion::Crypt::JWT.decode(token) # no verification, inspection only
141
+ ```
142
+
143
+ **Algorithms:**
144
+ - `HS256` (default): Uses cluster secret. All cluster nodes can issue and verify.
145
+ - `RS256`: Uses RSA keypair. Only the issuing node can sign; anyone with the public key can verify.
146
+
147
+ ---
148
+
149
+ **Maintained By**: Matthew Iverson (@Esity)
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  gemspec
@@ -8,3 +10,5 @@ group :test do
8
10
  gem 'rubocop'
9
11
  gem 'simplecov'
10
12
  end
13
+ gem 'legion-logging'
14
+ gem 'legion-settings'
data/LICENSE CHANGED
@@ -1,6 +1,7 @@
1
+
1
2
  Apache License
2
3
  Version 2.0, January 2004
3
- http://www.apache.org/licenses/
4
+ https://www.apache.org/licenses/
4
5
 
5
6
  TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
7
 
@@ -175,27 +176,16 @@
175
176
 
176
177
  END OF TERMS AND CONDITIONS
177
178
 
178
- APPENDIX: How to apply the Apache License to your work.
179
-
180
- To apply the Apache License to your work, attach the following
181
- boilerplate notice, with the fields enclosed by brackets "[]"
182
- replaced with your own identifying information. (Don't include
183
- the brackets!) The text should be enclosed in the appropriate
184
- comment syntax for the file format. We also recommend that a
185
- file or class name and description of purpose be included on the
186
- same "printed page" as the copyright notice for easier
187
- identification within third-party archives.
188
-
189
- Copyright 2021 Optum
179
+ Copyright 2021 Esity
190
180
 
191
181
  Licensed under the Apache License, Version 2.0 (the "License");
192
182
  you may not use this file except in compliance with the License.
193
183
  You may obtain a copy of the License at
194
184
 
195
- http://www.apache.org/licenses/LICENSE-2.0
185
+ https://www.apache.org/licenses/LICENSE-2.0
196
186
 
197
187
  Unless required by applicable law or agreed to in writing, software
198
188
  distributed under the License is distributed on an "AS IS" BASIS,
199
189
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
190
  See the License for the specific language governing permissions and
201
- limitations under the License.
191
+ limitations under the License.
data/README.md CHANGED
@@ -1,26 +1,21 @@
1
- Legion::Crypt
2
- =====
1
+ # legion-crypt
3
2
 
4
- Legion::Crypt is the class responsible for encryption, managing secrets and connecting with Vault
3
+ Encryption, secrets management, JWT token management, and HashiCorp Vault integration for the [LegionIO](https://github.com/LegionIO/LegionIO) framework. Provides AES-256-CBC message encryption, RSA key pair generation, cluster secret management, JWT issue/verify operations, and Vault token lifecycle management.
5
4
 
6
- Supported Ruby versions and implementations
7
- ------------------------------------------------
8
-
9
- Legion::Crypt should work identically on:
10
-
11
- * JRuby 9.2+
12
- * Ruby 2.4+
13
-
14
-
15
- Installation and Usage
16
- ------------------------
17
-
18
- You can verify your installation using this piece of code:
5
+ ## Installation
19
6
 
20
7
  ```bash
21
8
  gem install legion-crypt
22
9
  ```
23
10
 
11
+ Or add to your Gemfile:
12
+
13
+ ```ruby
14
+ gem 'legion-crypt'
15
+ ```
16
+
17
+ ## Usage
18
+
24
19
  ```ruby
25
20
  require 'legion/crypt'
26
21
 
@@ -29,8 +24,24 @@ Legion::Crypt.encrypt('this is my string')
29
24
  Legion::Crypt.decrypt(message)
30
25
  ```
31
26
 
32
- Settings
33
- ----------
27
+ ### JWT Tokens
28
+
29
+ ```ruby
30
+ # Issue a token (defaults to HS256 using cluster secret)
31
+ token = Legion::Crypt.issue_token({ node_id: 'abc' }, ttl: 3600)
32
+
33
+ # Verify and decode a token
34
+ claims = Legion::Crypt.verify_token(token)
35
+
36
+ # Use RS256 (RSA keypair) instead
37
+ token = Legion::Crypt.issue_token({ node_id: 'abc' }, algorithm: 'RS256')
38
+ claims = Legion::Crypt.verify_token(token, algorithm: 'RS256')
39
+
40
+ # Inspect a token without verification
41
+ decoded = Legion::Crypt::JWT.decode(token)
42
+ ```
43
+
44
+ ## Configuration
34
45
 
35
46
  ```json
36
47
  {
@@ -40,17 +51,47 @@ Settings
40
51
  "address": "localhost",
41
52
  "port": 8200,
42
53
  "token": null,
43
- "connected": false
54
+ "connected": false,
55
+ "renewer_time": 5,
56
+ "renewer": true,
57
+ "push_cluster_secret": true,
58
+ "read_cluster_secret": true,
59
+ "kv_path": "legion"
60
+ },
61
+ "jwt": {
62
+ "enabled": true,
63
+ "default_algorithm": "HS256",
64
+ "default_ttl": 3600,
65
+ "issuer": "legion",
66
+ "verify_expiration": true,
67
+ "verify_issuer": true
44
68
  },
45
69
  "cs_encrypt_ready": false,
46
70
  "dynamic_keys": true,
47
71
  "cluster_secret": null,
48
- "save_private_key": false,
49
- "read_private_key": false
72
+ "save_private_key": true,
73
+ "read_private_key": true
50
74
  }
51
75
  ```
52
76
 
53
- Authors
54
- ----------
77
+ ### JWT Algorithms
78
+
79
+ | Algorithm | Key | Use Case |
80
+ |-----------|-----|----------|
81
+ | `HS256` (default) | Cluster secret (symmetric) | Intra-cluster tokens — all nodes can issue and verify |
82
+ | `RS256` | RSA key pair (asymmetric) | Tokens verifiable by external services without sharing the signing key |
83
+
84
+ ### Vault Integration
85
+
86
+ When `vault.token` is set (or via `VAULT_TOKEN_ID` env var), Crypt connects to Vault on `start`. The background `VaultRenewer` thread keeps the token alive. Vault is an optional runtime dependency — the Vault module is only included if the `vault` gem is available.
87
+
88
+ ## Requirements
89
+
90
+ - Ruby >= 3.4
91
+ - `jwt` gem (>= 2.7)
92
+ - `vault` gem (>= 0.17, optional)
93
+ - HashiCorp Vault (optional, for secrets management)
94
+
95
+ ## License
55
96
 
56
- * [Matthew Iverson](https://github.com/Esity) - current maintainer
97
+ Apache-2.0
data/legion-crypt.gemspec CHANGED
@@ -6,27 +6,25 @@ Gem::Specification.new do |spec|
6
6
  spec.name = 'legion-crypt'
7
7
  spec.version = Legion::Crypt::VERSION
8
8
  spec.authors = ['Esity']
9
- spec.email = %w[matthewdiverson@gmail.com ruby@optum.com]
9
+ spec.email = ['matthewdiverson@gmail.com']
10
10
  spec.summary = 'Handles requests for encrypt, decrypting, connecting to Vault, among other things'
11
11
  spec.description = 'A gem used by the LegionIO framework for encryption'
12
- spec.homepage = 'https://github.com/Optum/legion-crypt'
12
+ spec.homepage = 'https://github.com/LegionIO/legion-crypt'
13
13
  spec.license = 'Apache-2.0'
14
14
  spec.require_paths = ['lib']
15
- spec.required_ruby_version = '>= 2.4'
15
+ spec.required_ruby_version = '>= 3.4'
16
16
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
- spec.test_files = spec.files.select { |p| p =~ %r{^test/.*_test.rb} }
18
- spec.extra_rdoc_files = %w[README.md LICENSE CHANGELOG.md]
17
+ spec.extra_rdoc_files = %w[README.md LICENSE CHANGELOG.md]
19
18
  spec.metadata = {
20
- 'bug_tracker_uri' => 'https://github.com/Optum/legion-crypt/issues',
21
- 'changelog_uri' => 'https://github.com/Optum/legion-crypt/src/main/CHANGELOG.md',
22
- 'documentation_uri' => 'https://github.com/Optum/legion-crypt',
23
- 'homepage_uri' => 'https://github.com/Optum/LegionIO',
24
- 'source_code_uri' => 'https://github.com/Optum/legion-crypt',
25
- 'wiki_uri' => 'https://github.com/Optum/legion-crypt/wiki'
19
+ 'bug_tracker_uri' => 'https://github.com/LegionIO/legion-crypt/issues',
20
+ 'changelog_uri' => 'https://github.com/LegionIO/legion-crypt/blob/main/CHANGELOG.md',
21
+ 'documentation_uri' => 'https://github.com/LegionIO/legion-crypt',
22
+ 'homepage_uri' => 'https://github.com/LegionIO/LegionIO',
23
+ 'source_code_uri' => 'https://github.com/LegionIO/legion-crypt',
24
+ 'wiki_uri' => 'https://github.com/LegionIO/legion-crypt/wiki',
25
+ 'rubygems_mfa_required' => 'true'
26
26
  }
27
27
 
28
- spec.add_dependency 'vault', '>= 0.15.0'
29
-
30
- spec.add_development_dependency 'legion-logging'
31
- spec.add_development_dependency 'legion-settings'
28
+ spec.add_dependency 'jwt', '>= 2.7'
29
+ spec.add_dependency 'vault', '>= 0.17'
32
30
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'securerandom'
2
4
  require 'legion/crypt/cluster_secret'
3
5
 
@@ -14,7 +16,7 @@ module Legion
14
16
  { enciphered_message: Base64.encode64(cipher.update(message) + cipher.final), iv: Base64.encode64(iv) }
15
17
  end
16
18
 
17
- def decrypt(message, iv)
19
+ def decrypt(message, init_vector)
18
20
  until cs.is_a?(String) || Legion::Settings[:client][:shutting_down]
19
21
  Legion::Logging.debug('sleeping Legion::Crypt.decrypt due to CS not being set')
20
22
  sleep(0.5)
@@ -23,7 +25,7 @@ module Legion
23
25
  decipher = OpenSSL::Cipher.new('aes-256-cbc')
24
26
  decipher.decrypt
25
27
  decipher.key = cs
26
- decipher.iv = Base64.decode64(iv)
28
+ decipher.iv = Base64.decode64(init_vector)
27
29
  message = Base64.decode64(message)
28
30
  decipher.update(message) + decipher.final
29
31
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'securerandom'
2
4
 
3
5
  module Legion
@@ -39,7 +41,7 @@ module Legion
39
41
  end
40
42
  alias cluster_secret from_settings
41
43
 
42
- def from_transport # rubocop:disable Metrics/AbcSize
44
+ def from_transport
43
45
  return nil unless Legion::Settings[:transport][:connected]
44
46
 
45
47
  require 'legion/transport/messages/request_cluster_secret'
@@ -55,10 +57,11 @@ module Legion
55
57
 
56
58
  unless from_settings.nil?
57
59
  Legion::Logging.info "Received cluster secret in #{((Time.new - start) * 1000.0).round}ms"
58
- from_settings
60
+ return from_settings
59
61
  end
60
62
 
61
63
  Legion::Logging.error 'Cluster secret is still unknown!'
64
+ nil
62
65
  rescue StandardError => e
63
66
  Legion::Logging.error e.message
64
67
  Legion::Logging.error e.backtrace[0..10]
@@ -79,7 +82,7 @@ module Legion
79
82
  end
80
83
 
81
84
  def set_cluster_secret(value, push_to_vault = true) # rubocop:disable Style/OptionalBooleanParameter
82
- raise TypeError unless value.to_i(32).to_s(32) == value.downcase
85
+ raise TypeError unless value.to_i(32).to_s(32).rjust(value.length, '0') == value.downcase
83
86
 
84
87
  Legion::Settings[:crypt][:cs_encrypt_ready] = true
85
88
  push_cs_to_vault if push_to_vault && settings_push_vault
@@ -114,7 +117,10 @@ module Legion
114
117
  end
115
118
 
116
119
  def validate_hex(value, length = secret_length)
117
- value.to_i(length).to_s(length) == value.downcase
120
+ return false unless value.is_a?(String)
121
+ return false if value.empty?
122
+
123
+ value.to_i(length).to_s(length).rjust(value.length, '0') == value.downcase
118
124
  end
119
125
  end
120
126
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jwt'
4
+ require 'securerandom'
5
+
6
+ module Legion
7
+ module Crypt
8
+ module JWT
9
+ class Error < StandardError; end
10
+ class ExpiredTokenError < Error; end
11
+ class InvalidTokenError < Error; end
12
+ class DecodeError < Error; end
13
+
14
+ SUPPORTED_ALGORITHMS = %w[HS256 RS256].freeze
15
+
16
+ def self.issue(payload, signing_key:, algorithm: 'HS256', ttl: 3600, issuer: 'legion')
17
+ validate_algorithm!(algorithm)
18
+
19
+ now = Time.now.to_i
20
+ claims = {
21
+ iss: issuer,
22
+ iat: now,
23
+ exp: now + ttl,
24
+ jti: SecureRandom.uuid
25
+ }.merge(payload)
26
+
27
+ ::JWT.encode(claims, signing_key, algorithm)
28
+ end
29
+
30
+ def self.verify(token, verification_key:, **opts)
31
+ algorithm = opts.fetch(:algorithm, 'HS256')
32
+ verify_expiration = opts.fetch(:verify_expiration, true)
33
+ verify_issuer = opts.fetch(:verify_issuer, true)
34
+ issuer = opts.fetch(:issuer, 'legion')
35
+
36
+ validate_algorithm!(algorithm)
37
+
38
+ decode_opts = {
39
+ algorithm: algorithm,
40
+ verify_expiration: verify_expiration,
41
+ verify_iss: verify_issuer
42
+ }
43
+ decode_opts[:iss] = issuer if verify_issuer
44
+
45
+ payload, _header = ::JWT.decode(token, verification_key, true, decode_opts)
46
+ symbolize_keys(payload)
47
+ rescue ::JWT::ExpiredSignature
48
+ raise ExpiredTokenError, 'token has expired'
49
+ rescue ::JWT::VerificationError, ::JWT::IncorrectAlgorithm
50
+ raise InvalidTokenError, 'token signature verification failed'
51
+ rescue ::JWT::DecodeError => e
52
+ raise DecodeError, "failed to decode token: #{e.message}"
53
+ end
54
+
55
+ def self.decode(token)
56
+ payload, _header = ::JWT.decode(token, nil, false)
57
+ symbolize_keys(payload)
58
+ rescue ::JWT::DecodeError => e
59
+ raise DecodeError, "failed to decode token: #{e.message}"
60
+ end
61
+
62
+ def self.validate_algorithm!(algorithm)
63
+ return if SUPPORTED_ALGORITHMS.include?(algorithm)
64
+
65
+ raise ArgumentError, "unsupported algorithm: #{algorithm}. Supported: #{SUPPORTED_ALGORITHMS.join(', ')}"
66
+ end
67
+
68
+ def self.symbolize_keys(hash)
69
+ hash.transform_keys(&:to_sym)
70
+ end
71
+
72
+ private_class_method :validate_algorithm!, :symbolize_keys
73
+ end
74
+ end
75
+ end