Class: NusaDB::Scram
- Inherits:
-
Object
- Object
- NusaDB::Scram
- Defined in:
- lib/nusadb/scram.rb
Overview
Client-side SCRAM-SHA-256 (RFC 5802 / RFC 7677), docs/wire-protocol.md §7.2. Uses Ruby's bundled OpenSSL / SecureRandom — no gems.
Base64 goes through Array#pack / String#unpack1 rather than the base64 library, which
strict_encode64 / strict_decode64 are themselves thin wrappers over ("m0" is strict:
no line breaks, and decoding rejects malformed input with ArgumentError). Ruby 3.4 demoted
base64 from a default gem to a bundled one, so require 'base64' raises LoadError under
Bundler unless the app declares it — which every Rails app using the ActiveRecord adapter
would have to. The core methods keep the gem genuinely dependency-free.
Constant Summary collapse
- GS2_HEADER =
'n,,'- CHANNEL_BINDING =
[GS2_HEADER].pack('m0')
Instance Attribute Summary collapse
-
#client_first_bare ⇒ Object
readonly
Returns the value of attribute client_first_bare.
-
#full_client_first ⇒ Object
readonly
Returns the value of attribute full_client_first.
Instance Method Summary collapse
- #client_final(password, server_first) ⇒ Object
-
#initialize(user) ⇒ Scram
constructor
A new instance of Scram.
- #verify_server_final(server_final) ⇒ Object
Constructor Details
#initialize(user) ⇒ Scram
Returns a new instance of Scram.
22 23 24 25 26 27 |
# File 'lib/nusadb/scram.rb', line 22 def initialize(user) nonce = [SecureRandom.random_bytes(18)].pack('m0') @client_first_bare = "n=#{user},r=#{nonce}" @full_client_first = GS2_HEADER + @client_first_bare @expected_signature = '' end |
Instance Attribute Details
#client_first_bare ⇒ Object (readonly)
Returns the value of attribute client_first_bare.
20 21 22 |
# File 'lib/nusadb/scram.rb', line 20 def @client_first_bare end |
#full_client_first ⇒ Object (readonly)
Returns the value of attribute full_client_first.
20 21 22 |
# File 'lib/nusadb/scram.rb', line 20 def full_client_first @full_client_first end |
Instance Method Details
#client_final(password, server_first) ⇒ Object
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/nusadb/scram.rb', line 29 def client_final(password, server_first) combined_nonce = nil salt = nil iterations = 0 server_first.split(',').each do |field| key, _, value = field.partition('=') next if value.nil? || key.empty? case key when 'r' then combined_nonce = value when 's' then salt = value.unpack1('m0') when 'i' then iterations = value.to_i end end if combined_nonce.nil? || salt.nil? || iterations <= 0 raise NusaError.new('nusadb: malformed server-first message', '08P01') end salted = OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iterations, 32, OpenSSL::Digest::SHA256.new) client_key = hmac(salted, 'Client Key') stored_key = OpenSSL::Digest::SHA256.digest(client_key) without_proof = "c=#{CHANNEL_BINDING},r=#{combined_nonce}" = "#{@client_first_bare},#{server_first},#{without_proof}" client_sig = hmac(stored_key, ) proof = xor(client_key, client_sig) final = "#{without_proof},p=#{[proof].pack('m0')}" server_key = hmac(salted, 'Server Key') @expected_signature = [hmac(server_key, )].pack('m0') final end |
#verify_server_final(server_final) ⇒ Object
62 63 64 65 66 67 68 69 70 |
# File 'lib/nusadb/scram.rb', line 62 def verify_server_final(server_final) got = '' server_final.split(',').each do |field| got = field[2..] if field.start_with?('v=') end OpenSSL.fixed_length_secure_compare(got, @expected_signature) rescue ArgumentError false # lengths differ end |