diff --git a/lib/sirp/client.rb b/lib/sirp/client.rb index 25095fa..74aa46e 100644 --- a/lib/sirp/client.rb +++ b/lib/sirp/client.rb @@ -1,17 +1,18 @@ module SIRP class Client + include SIRP attr_reader :N, :g, :k, :a, :A, :S, :K, :M, :H_AMK, :hash def initialize(group = 2048) # select modulus (N) and generator (g) - @N, @g, @hash = SIRP.Ng(group) - @k = SIRP.calc_k(@N, @g, hash) + @N, @g, @hash = Ng(group) + @k = calc_k(@N, @g, hash) end def start_authentication # Generate a/A private and public components @a ||= SecureRandom.hex(32).hex - @A = SIRP.num_to_hex(SIRP.calc_A(@a, @N, @g)) + @A = num_to_hex(calc_A(@a, @N, @g)) end # Process initiated authentication challenge. @@ -23,21 +24,21 @@ def process_challenge(username, password, xsalt, xbb) # SRP-6a safety check return false if (bb % @N) == 0 - x = SIRP.calc_x(username, password, xsalt, hash) - u = SIRP.calc_u(@A, xbb, @N, hash) + x = calc_x(username, password, xsalt, hash) + u = calc_u(@A, xbb, @N, hash) # SRP-6a safety check return false if u == 0 # calculate session key - @S = SIRP.num_to_hex(SIRP.calc_client_S(bb, @a, @k, x, u, @N, @g)) - @K = SIRP.sha_hex(@S, hash) + @S = num_to_hex(calc_client_S(bb, @a, @k, x, u, @N, @g)) + @K = sha_hex(@S, hash) # calculate match - @M = SIRP.calc_M(@A, xbb, @K, hash) + @M = calc_M(@A, xbb, @K, hash) # calculate verifier - @H_AMK = SIRP.num_to_hex(SIRP.calc_H_AMK(@A, @M, @K, hash)) + @H_AMK = num_to_hex(calc_H_AMK(@A, @M, @K, hash)) @M end diff --git a/lib/sirp/parameters.rb b/lib/sirp/parameters.rb index 57c093a..32af648 100644 --- a/lib/sirp/parameters.rb +++ b/lib/sirp/parameters.rb @@ -1,5 +1,5 @@ module SIRP - def self.Ng(group) + def Ng(group) case group when 1024 @N = %w( diff --git a/lib/sirp/sirp.rb b/lib/sirp/sirp.rb index e59f0f9..80d3b1b 100644 --- a/lib/sirp/sirp.rb +++ b/lib/sirp/sirp.rb @@ -1,111 +1,109 @@ module SIRP - class << self - # http://stackoverflow.com/questions/3772410/convert-a-string-of-0-f-into-a-byte-array-in-ruby - def hex_to_bytes(str) - [str].pack('H*').unpack('C*') - end - - def num_to_hex(num) - hex_str = num.to_s(16) - even_hex_str = hex_str.length.odd? ? '0' + hex_str : hex_str - even_hex_str.downcase - end - - def sha_hex(h, hash_klass) - hash_klass.hexdigest([h].pack('H*')) - end - - def sha_str(s, hash_klass) - hash_klass.hexdigest(s) - end - - # Modular Exponentiation - # https://en.m.wikipedia.org/wiki/Modular_exponentiation - # http://rosettacode.org/wiki/Modular_exponentiation#Ruby - # - # a^b (mod m) - def mod_exp(a, b, m) - # Use OpenSSL::BN#mod_exp - a.to_bn.mod_exp(b, m) - end - - # Hashing function with padding. - # Input is prefixed with 0 to meet N hex width. - def H(hash_klass, n, *a) - nlen = 2 * ((('%x' % [n]).length * 4 + 7) >> 3) - - hashin = a.map do |s| - next unless s - shex = (s.class == String) ? s : num_to_hex(s) - if shex.length > nlen - raise 'Bit width does not match - client uses different prime' - end - '0' * (nlen - shex.length) + shex - end.join('') - - sha_hex(hashin, hash_klass).hex % n - end - - # Multiplier parameter - # k = H(N, g) (in SIRP-6a) - def calc_k(n, g, hash_klass) - H(hash_klass, n, n, g) - end - - # Private key (derived from username, raw password and salt) - # x = H(salt || H(username || ':' || password)) - def calc_x(username, password, salt, hash_klass) - spad = salt.length.odd? ? '0' : '' - sha_hex(spad + salt + sha_str([username, password].join(':'), hash_klass), hash_klass).hex - end - - # Random scrambling parameter - # u = H(A, B) - def calc_u(xaa, xbb, n, hash_klass) - H(hash_klass, n, xaa, xbb) - end - - # Password verifier - # v = g^x (mod N) - def calc_v(x, n, g) - mod_exp(g, x, n) - end - - # A = g^a (mod N) - def calc_A(a, n, g) - mod_exp(g, a, n) - end - - # B = g^b + k v (mod N) - def calc_B(b, k, v, n, g) - (mod_exp(g, b, n) + k * v) % n - end - - # Client secret - # S = (B - (k * g^x)) ^ (a + (u * x)) % N - def calc_client_S(bb, a, k, x, u, n, g) - mod_exp((bb - k * mod_exp(g, x, n)) % n, (a + x * u), n) - end - - # Server secret - # S = (A * v^u) ^ b % N - def calc_server_S(aa, b, v, u, n) - mod_exp((mod_exp(v, u, n) * aa), b, n) - end - - # M = H(A, B, K) - def calc_M(xaa, xbb, xkk, hash_klass) - digester = hash_klass.new - digester << hex_to_bytes(xaa).pack('C*') - digester << hex_to_bytes(xbb).pack('C*') - digester << hex_to_bytes(xkk).pack('C*') - digester.hexdigest - end - - # H(A, M, K) - def calc_H_AMK(xaa, xmm, xkk, hash_klass) - byte_string = hex_to_bytes([xaa, xmm, xkk].join('')).pack('C*') - sha_str(byte_string, hash_klass).hex - end + # http://stackoverflow.com/questions/3772410/convert-a-string-of-0-f-into-a-byte-array-in-ruby + def hex_to_bytes(str) + [str].pack('H*').unpack('C*') + end + + def num_to_hex(num) + hex_str = num.to_s(16) + even_hex_str = hex_str.length.odd? ? '0' + hex_str : hex_str + even_hex_str.downcase + end + + def sha_hex(h, hash_klass) + hash_klass.hexdigest([h].pack('H*')) + end + + def sha_str(s, hash_klass) + hash_klass.hexdigest(s) + end + + # Modular Exponentiation + # https://en.m.wikipedia.org/wiki/Modular_exponentiation + # http://rosettacode.org/wiki/Modular_exponentiation#Ruby + # + # a^b (mod m) + def mod_exp(a, b, m) + # Use OpenSSL::BN#mod_exp + a.to_bn.mod_exp(b, m) + end + + # Hashing function with padding. + # Input is prefixed with 0 to meet N hex width. + def H(hash_klass, n, *a) + nlen = 2 * ((('%x' % [n]).length * 4 + 7) >> 3) + + hashin = a.map do |s| + next unless s + shex = (s.class == String) ? s : num_to_hex(s) + if shex.length > nlen + raise 'Bit width does not match - client uses different prime' + end + '0' * (nlen - shex.length) + shex + end.join('') + + sha_hex(hashin, hash_klass).hex % n + end + + # Multiplier parameter + # k = H(N, g) (in SIRP-6a) + def calc_k(n, g, hash_klass) + H(hash_klass, n, n, g) + end + + # Private key (derived from username, raw password and salt) + # x = H(salt || H(username || ':' || password)) + def calc_x(username, password, salt, hash_klass) + spad = salt.length.odd? ? '0' : '' + sha_hex(spad + salt + sha_str([username, password].join(':'), hash_klass), hash_klass).hex + end + + # Random scrambling parameter + # u = H(A, B) + def calc_u(xaa, xbb, n, hash_klass) + H(hash_klass, n, xaa, xbb) + end + + # Password verifier + # v = g^x (mod N) + def calc_v(x, n, g) + mod_exp(g, x, n) + end + + # A = g^a (mod N) + def calc_A(a, n, g) + mod_exp(g, a, n) + end + + # B = g^b + k v (mod N) + def calc_B(b, k, v, n, g) + (mod_exp(g, b, n) + k * v) % n + end + + # Client secret + # S = (B - (k * g^x)) ^ (a + (u * x)) % N + def calc_client_S(bb, a, k, x, u, n, g) + mod_exp((bb - k * mod_exp(g, x, n)) % n, (a + x * u), n) + end + + # Server secret + # S = (A * v^u) ^ b % N + def calc_server_S(aa, b, v, u, n) + mod_exp((mod_exp(v, u, n) * aa), b, n) + end + + # M = H(A, B, K) + def calc_M(xaa, xbb, xkk, hash_klass) + digester = hash_klass.new + digester << hex_to_bytes(xaa).pack('C*') + digester << hex_to_bytes(xbb).pack('C*') + digester << hex_to_bytes(xkk).pack('C*') + digester.hexdigest + end + + # H(A, M, K) + def calc_H_AMK(xaa, xmm, xkk, hash_klass) + byte_string = hex_to_bytes([xaa, xmm, xkk].join('')).pack('C*') + sha_str(byte_string, hash_klass).hex end end diff --git a/lib/sirp/verifier.rb b/lib/sirp/verifier.rb index 0da3a44..44d16eb 100644 --- a/lib/sirp/verifier.rb +++ b/lib/sirp/verifier.rb @@ -1,11 +1,12 @@ module SIRP class Verifier + include SIRP attr_reader :N, :g, :k, :A, :B, :b, :S, :K, :M, :H_AMK, :hash def initialize(group = 2048) # select modulus (N) and generator (g) - @N, @g, @hash = SIRP.Ng(group) - @k = SIRP.calc_k(@N, @g, hash) + @N, @g, @hash = Ng(group) + @k = calc_k(@N, @g, hash) end # Initial user creation for the persistance layer. @@ -13,9 +14,9 @@ def initialize(group = 2048) # Returns { , , } def generate_userauth(username, password) @salt ||= SecureRandom.hex(10) - x = SIRP.calc_x(username, password, @salt, hash) - v = SIRP.calc_v(x, @N, @g) - { username: username, verifier: SIRP.num_to_hex(v), salt: @salt } + x = calc_x(username, password, @salt, hash) + v = calc_v(x, @N, @g) + { username: username, verifier: num_to_hex(v), salt: @salt } end # Authentication phase 1 - create challenge. @@ -28,7 +29,7 @@ def get_challenge_and_proof(username, xverifier, xsalt, xaa) { challenge: { B: @B, salt: xsalt }, - proof: { A: xaa, B: @B, b: SIRP.num_to_hex(@b), I: username, s: xsalt, v: xverifier } + proof: { A: xaa, B: @B, b: num_to_hex(@b), I: username, s: xsalt, v: xverifier } } end @@ -41,21 +42,21 @@ def verify_session(proof, client_M) @b = proof[:b].to_i(16) v = proof[:v].to_i(16) - u = SIRP.calc_u(@A, @B, @N, hash) + u = calc_u(@A, @B, @N, hash) # SRP-6a safety check return false if u == 0 # calculate session key - @S = SIRP.num_to_hex(SIRP.calc_server_S(@A.to_i(16), @b, v, u, @N)) - @K = SIRP.sha_hex(@S, hash) + @S = num_to_hex(calc_server_S(@A.to_i(16), @b, v, u, @N)) + @K = sha_hex(@S, hash) # calculate match - @M = SIRP.calc_M(@A, @B, @K, hash) + @M = calc_M(@A, @B, @K, hash) if @M == client_M # authentication succeeded - @H_AMK = SIRP.num_to_hex(SIRP.calc_H_AMK(@A, @M, @K, hash)) + @H_AMK = num_to_hex(calc_H_AMK(@A, @M, @K, hash)) else false end @@ -66,7 +67,7 @@ def verify_session(proof, client_M) def generate_B(xverifier) v = xverifier.to_i(16) @b ||= SecureRandom.hex(32).hex - @B = SIRP.num_to_hex(SIRP.calc_B(@b, k, v, @N, @g)) + @B = num_to_hex(calc_B(@b, k, v, @N, @g)) end end end diff --git a/spec/parameters_spec.rb b/spec/parameters_spec.rb index d8606d5..105e068 100644 --- a/spec/parameters_spec.rb +++ b/spec/parameters_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe SIRP do + include SIRP # Test predefined values for N and g. # Values are from vectors listed in RFC 5054 Appendix B. # @@ -32,9 +33,9 @@ end end - it 'should be correct when accessed through a SIRP.Ng' do + it 'should be correct when accessed through a Ng' do @params.each do |p| - nn, g, h = SIRP.Ng(p[:group]) + nn, g, h = Ng(p[:group]) expect(('%b' % nn).length).to eq(p[:group]) expect(Digest::SHA256.hexdigest(('%x' % nn))).to eq(p[:hash_nn]) expect(g).to eq(p[:generator]) diff --git a/spec/sirp_spec.rb b/spec/sirp_spec.rb index 0c20486..86fba67 100644 --- a/spec/sirp_spec.rb +++ b/spec/sirp_spec.rb @@ -5,6 +5,7 @@ # Some values are from http://srp.stanford.edu/demo/demo.html using 256 bit values. # describe SIRP do + include SIRP before :all do @N = '115b8b692e0e045692cf280b436735c77a5a9e8a9e7ed56c965f87db5b2a2ece3'.to_i(16) @g = 2 @@ -17,7 +18,7 @@ context 'hex_to_bytes' do it 'should calculate expected results' do - expect(SIRP.hex_to_bytes('abcdef0123456789')) + expect(hex_to_bytes('abcdef0123456789')) .to eq [171, 205, 239, 1, 35, 69, 103, 137] end end @@ -25,7 +26,7 @@ context 'num_to_hex' do it 'should calculate expected results' do num = 999_999_999_999 - expect(SIRP.num_to_hex(num)) + expect(num_to_hex(num)) .to eq 'e8d4a50fff' expect('e8d4a50fff'.hex).to eq num end @@ -33,24 +34,24 @@ context 'sha_hex' do it 'should calculate expected results for SHA1' do - expect(SIRP.sha_hex('e8d4a50fff', Digest::SHA1)) + expect(sha_hex('e8d4a50fff', Digest::SHA1)) .to eq '62b225b459b48a52f68064f15fd464c200645a92' end it 'should calculate expected results for SHA256' do - expect(SIRP.sha_hex('e8d4a50fff', Digest::SHA256)) + expect(sha_hex('e8d4a50fff', Digest::SHA256)) .to eq '50cc877b5c7fe308b204f5b57bc1ac8e6fbd02fe2b50efc3b535af8490feb31a' end end context 'sha_str' do it 'should calculate expected results for SHA1' do - expect(SIRP.sha_str('foo', Digest::SHA1)) + expect(sha_str('foo', Digest::SHA1)) .to eq '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' end it 'should calculate expected results for SHA256' do - expect(SIRP.sha_str('foo', Digest::SHA256)) + expect(sha_str('foo', Digest::SHA256)) .to eq '2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae' end end @@ -60,7 +61,7 @@ a = 2988348162058574136915891421498819466320163312926952423791023078876139 b = 2351399303373464486466122544523690094744975233415544072992656881240319 m = 10**40 - c = SIRP.mod_exp(a, b, m) + c = mod_exp(a, b, m) expect(c).to eq 1527229998585248450016808958343740453059 end end @@ -69,19 +70,19 @@ it 'should calculate expected results' do a = 2988348162058574136915891421498819466320163312926952423791023078876139 b = 2351399303373464486466122544523690094744975233415544072992656881240319 - c = SIRP.H(Digest::SHA1, a, b) + c = H(Digest::SHA1, a, b) expect(c).to eq 870206349645559849154987479939336526106829135959 end it 'should raise an error when given invalid args' do - expect { SIRP.H(Digest::SHA1, 1, '123456789abcdef') } + expect { H(Digest::SHA1, 1, '123456789abcdef') } .to raise_error(RuntimeError, 'Bit width does not match - client uses different prime') end end context 'calc_k' do it 'should calculate expected results' do - k = SIRP.calc_k(@N, @g, Digest::SHA1) + k = calc_k(@N, @g, Digest::SHA1) expect(('%x' % k)).to eq 'dbe5dfe0704fee4c85ff106ecd38117d33bcfe50' expect(('%b' % k).length).to eq 160 end @@ -89,7 +90,7 @@ context 'calc_x' do it 'should calculate expected results' do - x = SIRP.calc_x(@username, @password, @salt, Digest::SHA1) + x = calc_x(@username, @password, @salt, Digest::SHA1) expect(('%x' % x)).to eq 'bdd0a4e1c9df4082684d8d358b8016301b025375' expect(('%b' % x).length).to eq 160 end @@ -99,7 +100,7 @@ it 'should calculate expected results' do aa = 'b1c4827b0ce416953789db123051ed990023f43b396236b86e12a2c69638fb8e' bb = 'fbc56086bb51e26ee1a8287c0a7f3fd4e067e55beb8530b869b10b961957ff68' - u = SIRP.calc_u(aa, bb, @N, Digest::SHA1) + u = calc_u(aa, bb, @N, Digest::SHA1) expect(('%x' % u)).to eq 'c60b17ddf568dd5743d0e3ba5621646b742432c5' expect(('%b' % u).length).to eq 160 end @@ -108,7 +109,7 @@ context 'calc_v' do it 'should calculate expected results' do x = 'bdd0a4e1c9df4082684d8d358b8016301b025375'.to_i(16) - v = SIRP.calc_v(x, @N, @g) + v = calc_v(x, @N, @g) expect(('%x' % v)).to eq 'ce36e101ed8c37ed98ba4e441274dabd1062f3440763eb98bd6058e5400b6309' expect(('%b' % v).length).to eq 256 end @@ -116,7 +117,7 @@ context 'calc_A' do it 'should calculate expected results' do - aa = SIRP.calc_A(@a, @N, @g) + aa = calc_A(@a, @N, @g) expect(('%x' % aa)).to eq 'b1c4827b0ce416953789db123051ed990023f43b396236b86e12a2c69638fb8e' expect(('%b' % aa).length).to eq 256 end @@ -126,7 +127,7 @@ it 'should calculate expected results' do k = 'dbe5dfe0704fee4c85ff106ecd38117d33bcfe50'.to_i(16) v = 'ce36e101ed8c37ed98ba4e441274dabd1062f3440763eb98bd6058e5400b6309'.to_i(16) - bb = SIRP.calc_B(@b, k, v, @N, @g) + bb = calc_B(@b, k, v, @N, @g) expect(('%x' % bb)).to eq 'fbc56086bb51e26ee1a8287c0a7f3fd4e067e55beb8530b869b10b961957ff68' expect(('%b' % bb).length).to eq 256 end @@ -138,7 +139,7 @@ k = 'dbe5dfe0704fee4c85ff106ecd38117d33bcfe50'.to_i(16) x = 'bdd0a4e1c9df4082684d8d358b8016301b025375'.to_i(16) u = 'c60b17ddf568dd5743d0e3ba5621646b742432c5'.to_i(16) - ss = SIRP.calc_client_S(bb, @a, k, x, u, @N, @g) + ss = calc_client_S(bb, @a, k, x, u, @N, @g) expect(('%x' % ss)).to eq 'a606c182e364d2c15f9cdbeeeb63bb00c831d1da65eedc1414f21157d0312a5a' expect(('%b' % ss).length).to eq 256 end @@ -149,7 +150,7 @@ aa = 'b1c4827b0ce416953789db123051ed990023f43b396236b86e12a2c69638fb8e'.to_i(16) v = 'ce36e101ed8c37ed98ba4e441274dabd1062f3440763eb98bd6058e5400b6309'.to_i(16) u = 'c60b17ddf568dd5743d0e3ba5621646b742432c5'.to_i(16) - ss = SIRP.calc_server_S(aa, @b, v, u, @N) + ss = calc_server_S(aa, @b, v, u, @N) expect(('%x' % ss)).to eq 'a606c182e364d2c15f9cdbeeeb63bb00c831d1da65eedc1414f21157d0312a5a' expect(('%b' % ss).length).to eq 256 end @@ -160,9 +161,9 @@ xaa = 'b1c4827b0ce416953789db123051ed990023f43b396236b86e12a2c69638fb8e' xbb = 'fbc56086bb51e26ee1a8287c0a7f3fd4e067e55beb8530b869b10b961957ff68' xss = 'a606c182e364d2c15f9cdbeeeb63bb00c831d1da65eedc1414f21157d0312a5a' - xkk = SIRP.sha_hex(xss, Digest::SHA1) + xkk = sha_hex(xss, Digest::SHA1) expect(xkk).to eq '5844898ea6e5f5d9b737bc0ba2fb9d5edd3f8e67' - mm = SIRP.calc_M(xaa, xbb, xkk, Digest::SHA1) + mm = calc_M(xaa, xbb, xkk, Digest::SHA1) expect(mm).to eq '0c6de5c7892a71bf971d733a511c44940e227941' end end @@ -172,7 +173,7 @@ xaa = 'b1c4827b0ce416953789db123051ed990023f43b396236b86e12a2c69638fb8e' xmm = 'd597503056af882d5b27b419302ac7b2ea9d7468' xkk = '5844898ea6e5f5d9b737bc0ba2fb9d5edd3f8e67' - h_amk = SIRP.calc_H_AMK(xaa, xmm, xkk, Digest::SHA1) + h_amk = calc_H_AMK(xaa, xmm, xkk, Digest::SHA1) expect(('%x' % h_amk)).to eq '530fccc1c4aa82ae5c5cdfa8bdec987c6032451d' end end