legion-crypt 1.4.29 → 1.5.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: f1f8d20fa675c493f158a7c62b4950eed82ce6109b810f40d8b928b810e7adce
4
- data.tar.gz: f12368e4084c501dddefdae4760c079b3c9c161e7da6f80b79a4b51ddead1a49
3
+ metadata.gz: 5b7ffab5835f3a3ac600a9bfe87e1e015e362d07d5814c956c13cc7552d23775
4
+ data.tar.gz: 18179a5915360c9f22151ec6a08ac813e580c2c2fb36dffd95b6f42aa9a4c242
5
5
  SHA512:
6
- metadata.gz: 7c2d68f6d5e1d20d28129054c578cad1ac4ce793e1c4f4d798a5ef6e90be6fe4d753cade29f654501e721a4fd40066798cf2ed255d6e2fdd9ff7888261cec0f5
7
- data.tar.gz: aeca51c126756fc86e7b92dd08e0dcb5f02a60ba3b0167cd69510dc0dd6abe016180a19ac811546899605e0ac42feec747199f77c45dcc8076cae79cc75c4b4e
6
+ metadata.gz: cce5f0d26e384c890c7117f0e710a9ff0f27da5794e8c2c9f05b3bdd81cacf3a551ffaaa9f31a75fea68e512918370b6deb8870c30cd010ea2da622a87c04611
7
+ data.tar.gz: acac22b254e94ccfd8ead507e014c7a619b715f8aab8000454d2e428633096ea3b251e21ab51ba67b31c879d6b07733670eb4cff1bf657ec553892262f72d199
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Legion::Crypt
2
2
 
3
+ ## [1.5.0] - 2026-04-02
4
+
5
+ ### Fixed
6
+ - Cluster-secret Vault synchronization now uses the documented `push_cluster_secret` setting, stores the new secret before pushing it, aligns Vault read/write path and field names, and invalidates cached derived key material on rotation
7
+ - External JWT/JWKS verification now fails closed on missing issuer or audience expectations, keeps reserved claims library-controlled, requires HTTPS JWKS transport, and avoids repeated fresh-cache re-fetches for unknown `kid` values
8
+ - Shared symmetric encryption now emits authenticated AES-256-GCM payloads for new ciphertexts while preserving decrypt compatibility with legacy AES-256-CBC payloads
9
+ - RSA keypair helper encryption now uses explicit OAEP padding for new ciphertexts while preserving decrypt compatibility with legacy PKCS#1 v1.5 payloads
10
+ - Multi-cluster Vault auth and helper routing now update live LDAP client tokens, keep per-cluster LDAP state local to the addressed cluster, sync the top-level Vault connected flag from cluster connectivity checks, allow explicit cluster-targeted helper calls, refresh lease metadata on renewal, and schedule short-lived token renewals before expiry
11
+ - SPIFFE X.509 fetch now fails closed by default, uses an explicit `allow_x509_fallback` development switch for self-signed fallback SVIDs, tags returned SVIDs with their source, and sends a valid 9-byte HTTP/2 SETTINGS frame during Workload API connection setup
12
+ - Ed25519 helper key persistence now uses the supported `Legion::Crypt` API with normalized KV paths, and tenant erasure verification no longer reports success when Vault access itself failed
13
+ - Background worker lifecycle is now serialized and cooperative: repeated `Legion::Crypt.start` calls no longer spawn duplicate workers, renewal/rotation threads no longer use `Thread#kill`, and timed-out joins keep their live thread references instead of dropping them
14
+
15
+ ### Changed
16
+ - Adopted `Legion::Logging::Helper` across `lib/` so library logs use structured component tagging instead of direct `Legion::Logging.*` calls
17
+ - Expanded `info`/`debug`/`error` coverage across crypt, Vault, JWT, lease, mTLS, SPIFFE, and auth flows to make background actions and failures visible without exposing secrets
18
+ - Replaced manual rescue logging with `handle_exception(...)` across library code paths and left Sinatra/API integration untouched for a later pass
19
+ - Removed remaining `log_info`/`log_warn`/`log_debug` wrapper methods in `lib/` so helper-backed logging is used directly throughout the library
20
+
21
+ ### Added
22
+ - Runtime dependency on `legion-logging`
23
+ - Compatibility shim for `Legion::Logging::Helper` so `handle_exception` and shared `log` access are available consistently during the uplift
24
+
3
25
  ## [1.4.29] - 2026-03-31
4
26
 
5
27
  ### Changed
data/Gemfile CHANGED
@@ -8,6 +8,7 @@ group :test do
8
8
  gem 'rspec'
9
9
  gem 'rspec_junit_formatter'
10
10
  gem 'rubocop'
11
+ gem 'rubocop-legion'
11
12
  gem 'simplecov'
12
13
  end
13
14
  gem 'legion-logging'
data/legion-crypt.gemspec CHANGED
@@ -27,5 +27,6 @@ Gem::Specification.new do |spec|
27
27
 
28
28
  spec.add_dependency 'ed25519', '~> 1.3'
29
29
  spec.add_dependency 'jwt', '>= 2.7'
30
+ spec.add_dependency 'legion-logging', '>= 1.5.0'
30
31
  spec.add_dependency 'vault', '>= 0.17'
31
32
  end
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'securerandom'
4
+ require 'legion/logging/helper'
4
5
 
5
6
  module Legion
6
7
  module Crypt
7
8
  module Attestation
9
+ extend Legion::Logging::Helper
10
+
8
11
  class << self
9
12
  def create(agent_id:, capabilities:, state:, private_key:)
10
13
  claim = {
@@ -17,30 +20,37 @@ module Legion
17
20
 
18
21
  payload = Legion::JSON.dump(claim)
19
22
  signature = Legion::Crypt::Ed25519.sign(payload, private_key)
20
- Legion::Logging.debug "Attestation created for agent #{agent_id}, state=#{state}" if defined?(Legion::Logging)
23
+ log.info "Attestation created for agent #{agent_id}, state=#{state}"
21
24
 
22
25
  { claim: claim, signature: signature.unpack1('H*'), payload: payload }
26
+ rescue StandardError => e
27
+ handle_exception(e, level: :error, operation: 'crypt.attestation.create', agent_id: agent_id, state: state)
28
+ raise
23
29
  end
24
30
 
25
31
  def verify(claim_hash:, signature_hex:, public_key:)
26
32
  payload = Legion::JSON.dump(claim_hash)
27
33
  signature = [signature_hex].pack('H*')
28
34
  result = Legion::Crypt::Ed25519.verify(payload, signature, public_key)
29
- if defined?(Legion::Logging)
30
- if result
31
- Legion::Logging.debug "Attestation verified for agent #{claim_hash[:agent_id]}"
32
- else
33
- Legion::Logging.warn "Attestation verification failed for agent #{claim_hash[:agent_id]}"
34
- end
35
+ agent_id = claim_hash[:agent_id] || claim_hash['agent_id']
36
+ if result
37
+ log.info "Attestation verified for agent #{agent_id}"
38
+ else
39
+ log.warn "Attestation verification failed for agent #{agent_id}"
35
40
  end
36
41
  result
42
+ rescue StandardError => e
43
+ handle_exception(e, level: :warn, operation: 'crypt.attestation.verify',
44
+ agent_id: claim_hash[:agent_id] || claim_hash['agent_id'])
45
+ raise
37
46
  end
38
47
 
39
48
  def fresh?(claim_hash, max_age_seconds: 300)
40
49
  timestamp = Time.parse(claim_hash[:timestamp])
41
50
  Time.now.utc - timestamp < max_age_seconds
42
51
  rescue StandardError => e
43
- Legion::Logging.warn("Legion::Crypt::Attestation#fresh? failed: #{e.message}") if defined?(Legion::Logging)
52
+ handle_exception(e, level: :warn, operation: 'crypt.attestation.fresh?',
53
+ agent_id: claim_hash[:agent_id] || claim_hash['agent_id'])
44
54
  false
45
55
  end
46
56
  end
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'legion/logging/helper'
4
+
3
5
  module Legion
4
6
  module Crypt
5
7
  class CertRotation
8
+ include Legion::Logging::Helper
9
+
6
10
  DEFAULT_CHECK_INTERVAL = 43_200 # 12 hours
7
11
 
8
12
  attr_reader :check_interval, :current_cert, :issued_at
@@ -13,6 +17,7 @@ module Legion
13
17
  @issued_at = nil
14
18
  @running = false
15
19
  @thread = nil
20
+ @mutex = Mutex.new
16
21
  end
17
22
 
18
23
  def start
@@ -21,17 +26,24 @@ module Legion
21
26
 
22
27
  @running = true
23
28
  @thread = Thread.new { rotation_loop }
24
- log_info('[mTLS] CertRotation started')
29
+ log.info('[mTLS] CertRotation started')
25
30
  end
26
31
 
27
32
  def stop
28
33
  @running = false
34
+ begin
35
+ @thread&.wakeup
36
+ rescue ThreadError => e
37
+ handle_exception(e, level: :debug, operation: 'crypt.cert_rotation.stop')
38
+ nil
39
+ end
40
+ @thread&.join(2)
29
41
  if @thread&.alive?
30
- @thread.kill
31
- @thread.join(2)
42
+ log.warn '[mTLS] CertRotation thread did not stop within timeout'
43
+ else
44
+ @thread = nil
32
45
  end
33
- @thread = nil
34
- log_debug('[mTLS] CertRotation stopped')
46
+ log.info('[mTLS] CertRotation stopped')
35
47
  end
36
48
 
37
49
  def running?
@@ -41,18 +53,26 @@ module Legion
41
53
  def rotate!
42
54
  node_name = node_common_name
43
55
  new_cert = Legion::Crypt::Mtls.issue_cert(common_name: node_name)
44
- @current_cert = new_cert
45
- @issued_at = Time.now
46
- log_info("[mTLS] Certificate rotated: serial=#{new_cert[:serial]} expiry=#{new_cert[:expiry]}")
56
+ @mutex.synchronize do
57
+ @current_cert = new_cert
58
+ @issued_at = Time.now
59
+ end
60
+ log.info("[mTLS] Certificate rotated: serial=#{new_cert[:serial]} expiry=#{new_cert[:expiry]}")
47
61
  emit_rotated_event(new_cert)
48
62
  new_cert
49
63
  end
50
64
 
51
65
  def needs_renewal?
52
- return false if @current_cert.nil? || @issued_at.nil?
66
+ current_cert = nil
67
+ issued_at = nil
68
+ @mutex.synchronize do
69
+ current_cert = @current_cert
70
+ issued_at = @issued_at
71
+ end
72
+ return false if current_cert.nil? || issued_at.nil?
53
73
 
54
- expiry = @current_cert[:expiry]
55
- total = expiry - @issued_at
74
+ expiry = current_cert[:expiry]
75
+ total = expiry - issued_at
56
76
  return true if total <= 0
57
77
 
58
78
  remaining = expiry - Time.now
@@ -65,27 +85,40 @@ module Legion
65
85
  def rotation_loop
66
86
  rotate!
67
87
  rescue StandardError => e
68
- log_warn("[mTLS] Initial rotation failed: #{e.message}")
88
+ handle_exception(e, level: :error, operation: 'crypt.cert_rotation.rotation_loop')
89
+ log.error("[mTLS] Initial rotation failed: #{e.message}")
69
90
  ensure
70
91
  loop_check
71
92
  end
72
93
 
73
94
  def loop_check
74
95
  while @running
75
- sleep(@check_interval)
96
+ interruptible_sleep(@check_interval)
76
97
  next unless @running && needs_renewal?
77
98
 
78
99
  begin
79
100
  rotate!
80
101
  rescue StandardError => e
81
- log_warn("[mTLS] Rotation check failed: #{e.message}")
102
+ handle_exception(e, level: :error, operation: 'crypt.cert_rotation.loop_check')
103
+ log.error("[mTLS] Rotation check failed: #{e.message}")
82
104
  end
83
105
  end
84
106
  rescue StandardError => e
85
- log_warn("[mTLS] CertRotation loop error: #{e.message}")
107
+ handle_exception(e, level: :error, operation: 'crypt.cert_rotation.loop_check')
108
+ log.error("[mTLS] CertRotation loop error: #{e.message}")
86
109
  retry if @running
87
110
  end
88
111
 
112
+ def interruptible_sleep(seconds)
113
+ deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + seconds
114
+ loop do
115
+ remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
116
+ break if remaining <= 0 || !@running
117
+
118
+ sleep([remaining, 1.0].min)
119
+ end
120
+ end
121
+
89
122
  def renewal_window
90
123
  return 0.5 unless defined?(Legion::Settings)
91
124
 
@@ -94,7 +127,8 @@ module Legion
94
127
 
95
128
  mtls = security[:mtls] || security['mtls'] || {}
96
129
  mtls[:renewal_window] || mtls['renewal_window'] || 0.5
97
- rescue StandardError
130
+ rescue StandardError => e
131
+ handle_exception(e, level: :debug, operation: 'crypt.cert_rotation.renewal_window')
98
132
  0.5
99
133
  end
100
134
 
@@ -103,7 +137,8 @@ module Legion
103
137
 
104
138
  name = Legion::Settings[:client]&.dig(:name) || Legion::Settings[:client]&.dig('name')
105
139
  name || 'legion.internal'
106
- rescue StandardError
140
+ rescue StandardError => e
141
+ handle_exception(e, level: :debug, operation: 'crypt.cert_rotation.node_common_name')
107
142
  'legion.internal'
108
143
  end
109
144
 
@@ -112,31 +147,8 @@ module Legion
112
147
 
113
148
  Legion::Events.emit('cert.rotated', serial: cert[:serial], expiry: cert[:expiry])
114
149
  rescue StandardError => e
115
- log_debug("[mTLS] Event emit failed: #{e.message}")
116
- end
117
-
118
- def log_info(msg)
119
- if defined?(Legion::Logging)
120
- Legion::Logging.info(msg)
121
- else
122
- $stdout.puts(msg)
123
- end
124
- end
125
-
126
- def log_debug(msg)
127
- if defined?(Legion::Logging)
128
- Legion::Logging.debug(msg)
129
- else
130
- $stdout.puts("[DEBUG] #{msg}")
131
- end
132
- end
133
-
134
- def log_warn(msg)
135
- if defined?(Legion::Logging)
136
- Legion::Logging.warn(msg)
137
- else
138
- warn("[WARN] #{msg}")
139
- end
150
+ handle_exception(e, level: :warn, operation: 'crypt.cert_rotation.emit_rotated_event')
151
+ log.warn("[mTLS] Event emit failed: #{e.message}")
140
152
  end
141
153
  end
142
154
  end
@@ -1,43 +1,78 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'securerandom'
4
+ require 'legion/logging/helper'
4
5
  require 'legion/crypt/cluster_secret'
5
6
 
6
7
  module Legion
7
8
  module Crypt
8
9
  module Cipher
10
+ AUTHENTICATED_CIPHER = 'aes-256-gcm'
11
+ LEGACY_CIPHER = 'aes-256-cbc'
12
+ AUTHENTICATED_PREFIX = 'gcm'
13
+ RSA_OAEP_PREFIX = 'oaep'
14
+ RSA_OAEP_PADDING = OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
15
+ RSA_LEGACY_PADDING = OpenSSL::PKey::RSA::PKCS1_PADDING
16
+
9
17
  include Legion::Crypt::ClusterSecret
18
+ include Legion::Logging::Helper
10
19
 
11
20
  def encrypt(message)
12
- cipher = OpenSSL::Cipher.new('aes-256-cbc')
21
+ cipher = OpenSSL::Cipher.new(AUTHENTICATED_CIPHER)
13
22
  cipher.encrypt
14
23
  cipher.key = cs
15
24
  iv = cipher.random_iv
16
- { enciphered_message: Base64.encode64(cipher.update(message) + cipher.final), iv: Base64.encode64(iv) }
25
+ ciphertext = cipher.update(message) + cipher.final
26
+ encoded_ciphertext = Base64.strict_encode64(ciphertext)
27
+ encoded_auth_tag = Base64.strict_encode64(cipher.auth_tag)
28
+ result = {
29
+ enciphered_message: "#{AUTHENTICATED_PREFIX}:#{encoded_ciphertext}:#{encoded_auth_tag}",
30
+ iv: Base64.strict_encode64(iv)
31
+ }
32
+ log.debug 'Cipher encrypt completed'
33
+ result
34
+ rescue StandardError => e
35
+ handle_exception(e, level: :error, operation: 'crypt.cipher.encrypt')
36
+ raise
17
37
  end
18
38
 
19
39
  def decrypt(message, init_vector)
20
- until cs.is_a?(String) || Legion::Settings[:client][:shutting_down]
21
- Legion::Logging.debug('sleeping Legion::Crypt.decrypt due to CS not being set')
22
- sleep(0.5)
23
- end
24
-
25
- decipher = OpenSSL::Cipher.new('aes-256-cbc')
26
- decipher.decrypt
27
- decipher.key = cs
28
- decipher.iv = Base64.decode64(init_vector)
29
- message = Base64.decode64(message)
30
- decipher.update(message) + decipher.final
40
+ secret = wait_for_cluster_secret
41
+ result = if authenticated_ciphertext?(message)
42
+ decrypt_authenticated(message, init_vector, secret)
43
+ else
44
+ decrypt_legacy(message, init_vector, secret)
45
+ end
46
+ log.debug 'Cipher decrypt completed'
47
+ result
48
+ rescue StandardError => e
49
+ handle_exception(e, level: :error, operation: 'crypt.cipher.decrypt')
50
+ raise
31
51
  end
32
52
 
33
53
  def encrypt_from_keypair(message:, pub_key: public_key)
34
54
  rsa_public_key = OpenSSL::PKey::RSA.new(pub_key)
35
55
 
36
- Base64.encode64(rsa_public_key.public_encrypt(message))
56
+ encrypted_message = rsa_public_key.public_encrypt(message, RSA_OAEP_PADDING)
57
+ encoded_message = "#{RSA_OAEP_PREFIX}:#{Base64.strict_encode64(encrypted_message)}"
58
+ log.debug 'Cipher keypair encryption completed'
59
+ encoded_message
60
+ rescue StandardError => e
61
+ handle_exception(e, level: :error, operation: 'crypt.cipher.encrypt_from_keypair')
62
+ raise
37
63
  end
38
64
 
39
65
  def decrypt_from_keypair(message:, **_opts)
40
- private_key.private_decrypt(Base64.decode64(message))
66
+ decrypted_message = if rsa_oaep_ciphertext?(message)
67
+ decrypt_oaep_from_keypair(message)
68
+ else
69
+ decrypt_legacy_from_keypair(message)
70
+ end
71
+ log.debug 'Cipher keypair decryption completed'
72
+ decrypted_message
73
+ rescue StandardError => e
74
+ handle_exception(e, level: :error, operation: 'crypt.cipher.decrypt_from_keypair')
75
+ raise
41
76
  end
42
77
 
43
78
  def public_key
@@ -46,10 +81,66 @@ module Legion
46
81
 
47
82
  def private_key
48
83
  @private_key ||= if Legion::Settings[:crypt][:read_private_key] && File.exist?('./legionio.key')
84
+ log.info 'Cipher loading RSA private key from disk'
49
85
  OpenSSL::PKey::RSA.new File.read './legionio.key'
50
86
  else
87
+ log.info 'Cipher generating RSA private key'
51
88
  OpenSSL::PKey::RSA.new 2048
52
89
  end
90
+ rescue StandardError => e
91
+ handle_exception(e, level: :error, operation: 'crypt.cipher.private_key')
92
+ raise
93
+ end
94
+
95
+ private
96
+
97
+ def wait_for_cluster_secret
98
+ loop do
99
+ secret = cs
100
+ return secret if secret.is_a?(String)
101
+ break if Legion::Settings[:client][:shutting_down]
102
+
103
+ log.debug('sleeping Legion::Crypt.decrypt due to CS not being set')
104
+ sleep(0.5)
105
+ end
106
+
107
+ cs
108
+ end
109
+
110
+ def authenticated_ciphertext?(message)
111
+ message.start_with?("#{AUTHENTICATED_PREFIX}:")
112
+ end
113
+
114
+ def decrypt_authenticated(message, init_vector, secret)
115
+ _, encoded_ciphertext, encoded_auth_tag = message.split(':', 3)
116
+
117
+ decipher = OpenSSL::Cipher.new(AUTHENTICATED_CIPHER)
118
+ decipher.decrypt
119
+ decipher.key = secret
120
+ decipher.iv = Base64.strict_decode64(init_vector)
121
+ decipher.auth_tag = Base64.strict_decode64(encoded_auth_tag)
122
+ decipher.update(Base64.strict_decode64(encoded_ciphertext)) + decipher.final
123
+ end
124
+
125
+ def decrypt_legacy(message, init_vector, secret)
126
+ decipher = OpenSSL::Cipher.new(LEGACY_CIPHER)
127
+ decipher.decrypt
128
+ decipher.key = secret
129
+ decipher.iv = Base64.decode64(init_vector)
130
+ decipher.update(Base64.decode64(message)) + decipher.final
131
+ end
132
+
133
+ def rsa_oaep_ciphertext?(message)
134
+ message.start_with?("#{RSA_OAEP_PREFIX}:")
135
+ end
136
+
137
+ def decrypt_oaep_from_keypair(message)
138
+ _, encoded_message = message.split(':', 2)
139
+ private_key.private_decrypt(Base64.strict_decode64(encoded_message), RSA_OAEP_PADDING)
140
+ end
141
+
142
+ def decrypt_legacy_from_keypair(message)
143
+ private_key.private_decrypt(Base64.decode64(message), RSA_LEGACY_PADDING)
53
144
  end
54
145
  end
55
146
  end
@@ -1,17 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'securerandom'
4
+ require 'legion/logging/helper'
4
5
 
5
6
  module Legion
6
7
  module Crypt
7
8
  module ClusterSecret
9
+ include Legion::Logging::Helper
10
+
8
11
  def find_cluster_secret
9
12
  %i[from_settings from_vault from_transport generate_secure_random].each do |method|
10
13
  result = send(method)
11
14
  next if result.nil?
12
15
 
13
16
  unless validate_hex(result)
14
- Legion::Logging.warn("Legion::Crypt.#{method} gave a value but it isn't a valid hex")
17
+ log.warn("Legion::Crypt.#{method} gave a value but it isn't a valid hex")
15
18
  next
16
19
  end
17
20
 
@@ -22,18 +25,22 @@ module Legion
22
25
 
23
26
  key = generate_secure_random
24
27
  set_cluster_secret(key)
28
+ log.info 'Cluster secret generated locally because this node is the only member'
25
29
  key
26
30
  end
27
31
 
28
32
  def from_vault
29
- return nil unless method_defined? :get
33
+ return nil unless Legion::Crypt.respond_to?(:get) && Legion::Crypt.respond_to?(:exist?)
30
34
  return nil unless Legion::Settings[:crypt][:vault][:read_cluster_secret]
31
35
  return nil unless Legion::Settings[:crypt][:vault][:connected]
32
- return nil unless Legion::Crypt.exist?('crypt')
36
+ return nil unless Legion::Crypt.exist?(cluster_secret_vault_path)
37
+
38
+ data = Legion::Crypt.get(cluster_secret_vault_path)
39
+ return nil unless data.is_a?(Hash)
33
40
 
34
- get('crypt')[:cluster_secret]
41
+ data[:cluster_secret] || data['cluster_secret']
35
42
  rescue StandardError => e
36
- Legion::Logging.warn("Legion::Crypt::ClusterSecret#from_vault failed: #{e.message}") if defined?(Legion::Logging)
43
+ handle_exception(e, level: :warn, operation: 'crypt.cluster_secret.from_vault')
37
44
  nil
38
45
  end
39
46
 
@@ -46,8 +53,8 @@ module Legion
46
53
  return nil unless Legion::Settings[:transport][:connected]
47
54
 
48
55
  require 'legion/transport/messages/request_cluster_secret'
49
- Legion::Logging.info 'Requesting cluster secret via public key'
50
- Legion::Logging.warn 'cluster_secret already set but we are requesting a new value' unless from_settings.nil?
56
+ log.info 'Requesting cluster secret via public key'
57
+ log.warn 'cluster_secret already set but we are requesting a new value' unless from_settings.nil?
51
58
  start = Time.now
52
59
  Legion::Transport::Messages::RequestClusterSecret.new.publish
53
60
  sleep_time = 0.001
@@ -57,20 +64,14 @@ module Legion
57
64
  end
58
65
 
59
66
  unless from_settings.nil?
60
- Legion::Logging.info "Received cluster secret in #{((Time.new - start) * 1000.0).round}ms"
67
+ log.info "Received cluster secret in #{((Time.new - start) * 1000.0).round}ms"
61
68
  return from_settings
62
69
  end
63
70
 
64
- Legion::Logging.error 'Cluster secret is still unknown!'
71
+ log.error 'Cluster secret is still unknown!'
65
72
  nil
66
73
  rescue StandardError => e
67
- if defined?(Legion::Logging) && Legion::Logging.respond_to?(:log_exception)
68
- Legion::Logging.log_exception(e, lex: 'crypt', component_type: :helper)
69
- elsif defined?(Legion::Logging) && Legion::Logging.respond_to?(:error)
70
- Legion::Logging.error "from_transport failed: #{e.class}=#{e.message}\n#{Array(e.backtrace).first(10).join("\n")}"
71
- else
72
- warn "from_transport failed: #{e.class}=#{e.message}\n#{Array(e.backtrace).first(10).join("\n")}"
73
- end
74
+ handle_exception(e, level: :error, operation: 'crypt.cluster_secret.from_transport')
74
75
  nil
75
76
  end
76
77
 
@@ -79,32 +80,36 @@ module Legion
79
80
  end
80
81
 
81
82
  def settings_push_vault
82
- Legion::Settings[:crypt][:vault].fetch(:push_cs_to_vault, false)
83
+ vault_settings = Legion::Settings[:crypt][:vault]
84
+ return vault_settings[:push_cluster_secret] unless vault_settings[:push_cluster_secret].nil?
85
+
86
+ vault_settings.fetch(:push_cs_to_vault, false)
83
87
  end
84
88
 
85
89
  def only_member?
86
90
  Legion::Transport::Queue.new('node.crypt', passive: true).consumer_count.zero?
87
91
  rescue StandardError => e
88
- Legion::Logging.warn("Legion::Crypt::ClusterSecret#only_member? failed: #{e.message}") if defined?(Legion::Logging)
92
+ handle_exception(e, level: :warn, operation: 'crypt.cluster_secret.only_member?')
89
93
  nil
90
94
  end
91
95
 
92
96
  def set_cluster_secret(value, push_to_vault = true) # rubocop:disable Style/OptionalBooleanParameter
93
- raise TypeError unless value.to_i(32).to_s(32).rjust(value.length, '0') == value.downcase
97
+ raise TypeError unless validate_hex(value)
94
98
 
99
+ Legion::Settings[:crypt][:cluster_secret] = value
100
+ @cs = nil
95
101
  Legion::Settings[:crypt][:cs_encrypt_ready] = true
96
102
  push_cs_to_vault if push_to_vault && settings_push_vault
97
-
98
- Legion::Settings[:crypt][:cluster_secret] = value
103
+ log.info "Cluster secret loaded into settings push_to_vault=#{push_to_vault}"
99
104
  end
100
105
 
101
106
  def push_cs_to_vault
102
107
  return false unless Legion::Settings[:crypt][:vault][:connected] && Legion::Settings[:crypt][:cluster_secret]
103
108
 
104
- Legion::Logging.info 'Pushing Cluster Secret to Vault'
105
- Legion::Crypt.write('cluster', secret: Legion::Settings[:crypt][:cluster_secret])
109
+ log.info 'Pushing Cluster Secret to Vault'
110
+ Legion::Crypt.write(cluster_secret_vault_path, cluster_secret: Legion::Settings[:crypt][:cluster_secret])
106
111
  rescue StandardError => e
107
- Legion::Logging.warn("push_cs_to_vault failed: #{e.message}") if defined?(Legion::Logging)
112
+ handle_exception(e, level: :warn, operation: 'crypt.cluster_secret.push_cs_to_vault')
108
113
  false
109
114
  end
110
115
 
@@ -113,7 +118,7 @@ module Legion
113
118
  end
114
119
 
115
120
  def secret_length
116
- Legion::Settings[:crypt][:cluster_lenth] || 32
121
+ Legion::Settings[:crypt][:cluster_length] || Legion::Settings[:crypt][:cluster_lenth] || 32
117
122
  end
118
123
 
119
124
  def generate_secure_random(length = secret_length)
@@ -123,26 +128,23 @@ module Legion
123
128
  def cs
124
129
  @cs ||= Digest::SHA256.digest(find_cluster_secret)
125
130
  rescue StandardError => e
126
- if defined?(Legion::Logging) && Legion::Logging.respond_to?(:log_exception)
127
- Legion::Logging.log_exception(e, lex: 'crypt', component_type: :helper)
128
- elsif defined?(Legion::Logging) && Legion::Logging.respond_to?(:error)
129
- backtrace = Array(e.backtrace).first(10).join("\n")
130
- Legion::Logging.error "Legion::Crypt::ClusterSecret#cs failed: #{e.class}: #{e.message}\n#{backtrace}"
131
- elsif defined?(Legion::Logging) && Legion::Logging.respond_to?(:warn)
132
- backtrace = Array(e.backtrace).first(10).join("\n")
133
- Legion::Logging.warn "Legion::Crypt::ClusterSecret#cs failed: #{e.class}: #{e.message}\n#{backtrace}"
134
- else
135
- backtrace = Array(e.backtrace).first(10).join("\n")
136
- ::Kernel.warn "Legion::Crypt::ClusterSecret#cs failed: #{e.class}: #{e.message}\n#{backtrace}"
137
- end
131
+ handle_exception(e, level: :error, operation: 'crypt.cluster_secret.cs')
138
132
  nil
139
133
  end
140
134
 
141
135
  def validate_hex(value, length = secret_length)
142
136
  return false unless value.is_a?(String)
143
137
  return false if value.empty?
138
+ return false unless value.match?(/\A\h+\z/)
139
+
140
+ expected_length = length.to_i * 2
141
+ return true if expected_length.zero?
142
+
143
+ value.length == expected_length
144
+ end
144
145
 
145
- value.to_i(length).to_s(length).rjust(value.length, '0') == value.downcase
146
+ def cluster_secret_vault_path
147
+ 'crypt'
146
148
  end
147
149
  end
148
150
  end