Module: RosettAi::Mcp::KeyHasher

Defined in:
lib/rosett_ai/mcp/key_hasher.rb

Overview

Cryptographic key hashing with per-key random salt.

Uses SHA-256 with 16-byte random salt. Verification uses constant-time comparison via Rack::Utils.secure_compare.

Author:

  • hugo

  • claude

Constant Summary collapse

SALT_BYTES =
16
KEY_PREFIX =
'nncc_'

Class Method Summary collapse

Class Method Details

.generate_keyString

Generate a new random API key with the Rosett-AI_ prefix.

Returns:

  • (String)

    a new API key



50
51
52
# File 'lib/rosett_ai/mcp/key_hasher.rb', line 50

def generate_key
  "#{KEY_PREFIX}#{SecureRandom.hex(24)}"
end

.hash_key(plaintext) ⇒ String

Hash a plaintext API key with a random salt.

Parameters:

  • plaintext (String)

    the plaintext key

Returns:

  • (String)

    salted hash in format "salt$digest"



28
29
30
31
32
# File 'lib/rosett_ai/mcp/key_hasher.rb', line 28

def hash_key(plaintext)
  salt = SecureRandom.hex(SALT_BYTES)
  digest = OpenSSL::Digest::SHA256.hexdigest("#{salt}#{plaintext}")
  "#{salt}$#{digest}"
end

.secure_compare(a_str, b_str) ⇒ Boolean

Constant-time string comparison to prevent timing attacks.

Parameters:

  • a (String)

    first string

  • b (String)

    second string

Returns:

  • (Boolean)

    true if strings match



59
60
61
62
63
# File 'lib/rosett_ai/mcp/key_hasher.rb', line 59

def secure_compare(a_str, b_str)
  return false unless a_str.bytesize == b_str.bytesize

  OpenSSL.fixed_length_secure_compare(a_str, b_str)
end

.verify_key(plaintext, stored_hash) ⇒ Boolean

Verify a plaintext key against a stored hash.

Parameters:

  • plaintext (String)

    the plaintext key to verify

  • stored_hash (String)

    the stored "salt$digest" hash

Returns:

  • (Boolean)

    true if the key matches



39
40
41
42
43
44
45
# File 'lib/rosett_ai/mcp/key_hasher.rb', line 39

def verify_key(plaintext, stored_hash)
  salt, expected_digest = stored_hash.split('$', 2)
  return false unless salt && expected_digest

  actual_digest = OpenSSL::Digest::SHA256.hexdigest("#{salt}#{plaintext}")
  secure_compare(actual_digest, expected_digest)
end