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 +4 -4
- data/.github/workflows/ci.yml +16 -0
- data/.rubocop.yml +41 -14
- data/CHANGELOG.md +28 -1
- data/CLAUDE.md +149 -0
- data/Gemfile +4 -0
- data/LICENSE +5 -15
- data/README.md +65 -24
- data/legion-crypt.gemspec +13 -15
- data/lib/legion/crypt/cipher.rb +4 -2
- data/lib/legion/crypt/cluster_secret.rb +10 -4
- data/lib/legion/crypt/jwt.rb +75 -0
- data/lib/legion/crypt/lease_manager.rb +199 -0
- data/lib/legion/crypt/settings.rb +27 -12
- data/lib/legion/crypt/vault.rb +3 -1
- data/lib/legion/crypt/vault_jwt_auth.rb +92 -0
- data/lib/legion/crypt/vault_renewer.rb +2 -0
- data/lib/legion/crypt/version.rb +1 -1
- data/lib/legion/crypt.rb +49 -0
- metadata +26 -47
- data/.github/workflows/rubocop-analysis.yml +0 -41
- data/.github/workflows/sourcehawk-scan.yml +0 -20
- data/CODE_OF_CONDUCT.md +0 -75
- data/CONTRIBUTING.md +0 -55
- data/INDIVIDUAL_CONTRIBUTOR_LICENSE.md +0 -30
- data/NOTICE.txt +0 -9
- data/SECURITY.md +0 -9
- data/attribution.txt +0 -1
- data/sourcehawk.yml +0 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a7018b9c084879712ba6a2fdf0b64f19b1c78de8cb8b9377063f824a0f43314c
|
|
4
|
+
data.tar.gz: 0d449cbe402ffbe5a45256801ea442ac03d7663f234f4678f52147e55214c0c0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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:
|
|
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:
|
|
9
|
-
|
|
10
|
-
|
|
26
|
+
Max: 40
|
|
27
|
+
Exclude:
|
|
28
|
+
- 'spec/**/*'
|
|
29
|
+
|
|
11
30
|
Metrics/AbcSize:
|
|
12
|
-
Max:
|
|
31
|
+
Max: 60
|
|
32
|
+
|
|
33
|
+
Metrics/CyclomaticComplexity:
|
|
34
|
+
Max: 15
|
|
35
|
+
|
|
13
36
|
Metrics/PerceivedComplexity:
|
|
14
|
-
Max:
|
|
15
|
-
|
|
16
|
-
Enabled: false
|
|
37
|
+
Max: 17
|
|
38
|
+
|
|
17
39
|
Style/Documentation:
|
|
18
40
|
Enabled: false
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
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
data/LICENSE
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
|
|
1
2
|
Apache License
|
|
2
3
|
Version 2.0, January 2004
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2
|
-
=====
|
|
1
|
+
# legion-crypt
|
|
3
2
|
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":
|
|
49
|
-
"read_private_key":
|
|
72
|
+
"save_private_key": true,
|
|
73
|
+
"read_private_key": true
|
|
50
74
|
}
|
|
51
75
|
```
|
|
52
76
|
|
|
53
|
-
|
|
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
|
-
|
|
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 =
|
|
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/
|
|
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 = '>=
|
|
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.
|
|
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'
|
|
21
|
-
'changelog_uri'
|
|
22
|
-
'documentation_uri'
|
|
23
|
-
'homepage_uri'
|
|
24
|
-
'source_code_uri'
|
|
25
|
-
'wiki_uri'
|
|
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 '
|
|
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
|
data/lib/legion/crypt/cipher.rb
CHANGED
|
@@ -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,
|
|
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(
|
|
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
|
|
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.
|
|
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
|