Skip to content
This repository has been archived by the owner on Aug 26, 2022. It is now read-only.

Commit

Permalink
Fixes #1, Swap usage of rubype for contracts gem to enforce types. Ba…
Browse files Browse the repository at this point in the history
…sic usage, could be further integrated.
  • Loading branch information
grempe committed Dec 8, 2016
1 parent 06f31f5 commit 23186ed
Show file tree
Hide file tree
Showing 7 changed files with 45 additions and 31 deletions.
2 changes: 1 addition & 1 deletion lib/sirp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
require 'rbnacl'
require 'sysrandom/securerandom'
require 'hashie'
require 'rubype'
require 'contracts'
require 'sirp/util'
require 'sirp/sirp'
require 'sirp/parameters'
Expand Down
10 changes: 6 additions & 4 deletions lib/sirp/client.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module SIRP
class Client
include Contracts::Core
include Contracts::Builtin
include SIRP

attr_reader :N # Bignum
Expand All @@ -16,25 +18,25 @@ class Client
# Select modulus (N), generator (g), and one-way hash function (SHA1 or SHA256)
#
# @param group [Fixnum] the group size in bits
Contract Nat => Nat
def initialize(group = 2048)
raise ArgumentError, 'must be a known group size' unless [1024, 1536, 2048, 3072, 4096, 6144, 8192].include?(group)

@N, @g, @hash = Ng(group)
@k = calc_k(@N, @g, hash)
end
typesig :initialize, [Fixnum] => Integer

# Phase 1 : Step 1 : Start the authentication process by generating the
# client 'a' and 'A' values. Public 'A' should later be sent along with
# the username, to the server verifier to continue the auth process. The
# internal secret 'a' value should remain private.
#
# @return [String] the value of 'A' in hex
Contract None => String
def start_authentication
@a ||= RbNaCl::Util.bin2hex(RbNaCl::Random.random_bytes(32)).hex
@A = num_to_hex(calc_A(@a, @N, @g))
end
typesig :start_authentication, [] => String

#
# Phase 1 : Step 2 : See Verifier#get_challenge_and_proof(username, xverifier, xsalt, xaa)
Expand All @@ -47,6 +49,7 @@ def start_authentication
# @param xsalt [String] the server provided salt for the username in hex
# @param xbb [String] the server verifier 'B' value in hex
# @return [String] the client 'M' value in hex
Contract String, String, String, String => String
def process_challenge(username, password, xsalt, xbb)
raise ArgumentError, 'username must not be an empty string' if username.empty?
raise ArgumentError, 'password must not be an empty string' if password.empty?
Expand Down Expand Up @@ -76,7 +79,6 @@ def process_challenge(username, password, xsalt, xbb)
# Return the 'M' matcher to be sent to the server
@M
end
typesig :process_challenge, [String, String, String, String] => String

#
# Phase 2 : Step 2 : See Verifier#verify_session(proof, client_M)
Expand All @@ -91,11 +93,11 @@ def process_challenge(username, password, xsalt, xbb)
#
# @param server_HAMK [String] the server provided H_AMK in hex
# @return [true,false] returns true if the server and client agree on the H_AMK value, false if not
Contract String => Bool
def verify(server_HAMK)
return false unless @H_AMK
return false unless server_HAMK =~ /^[a-fA-F0-9]+$/
secure_compare(@H_AMK, server_HAMK)
end
typesig :verify, [String] => Boolean
end
end
5 changes: 4 additions & 1 deletion lib/sirp/parameters.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
module SIRP
include Contracts::Core
include Contracts::Builtin

Contract Num => Array[Nat, Nat, RespondTo[:hexdigest]]
def Ng(group)
case group
when 1024
Expand Down Expand Up @@ -168,5 +172,4 @@ def Ng(group)

[@N, @g, @hash]
end
typesig :Ng, [Fixnum] => Array
end
37 changes: 20 additions & 17 deletions lib/sirp/sirp.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
module SIRP
include Contracts::Core
include Contracts::Builtin

# Modular Exponentiation
# https://en.m.wikipedia.org/wiki/Modular_exponentiation
# http://rosettacode.org/wiki/Modular_exponentiation#Ruby
Expand All @@ -9,18 +12,19 @@ module SIRP
# @param b [Bignum] the exponent value as a Bignum
# @param m [Bignum] the modulus value as a Bignum
# @return [Bignum] the solution as a Bignum
Contract Or[Fixnum, Bignum], Nat, Nat => Bignum
def mod_pow(a, b, m)
# Convert type and use OpenSSL::BN#mod_exp to do the calculation
# Convert back to a Bignum so OpenSSL::BN doesn't leak everywhere
a.to_bn.mod_exp(b, m).to_i
end
typesig :mod_pow, [Integer, Integer, Integer] => Bignum

# One-Way Hash Function
#
# @param hash_klass [Digest::SHA1, Digest::SHA256] The hash class that responds to hexdigest
# @param a [Array] the Array of values to be hashed together
# @return [Bignum] the hexdigest as a Bignum
Contract RespondTo[:hexdigest], ArrayOf[Or[String, Nat]] => Bignum
def H(hash_klass, a)
hasher = hash_klass.new

Expand All @@ -32,19 +36,18 @@ def H(hash_klass, a)
digest = hasher.hexdigest
digest.hex
end
typesig :H, [:hexdigest, Array] => Bignum

# Multiplier Parameter
# k = H(N, g) (in SRP-6a)
#
# @param nn [Bignum] the 'N' value as a Bignum
# @param g [Bignum] the 'g' value as a Bignum
# @param g [Fixnum] the 'g' value as a Fixnum
# @param hash_klass [Digest::SHA1, Digest::SHA256] The hash class that responds to hexdigest
# @return [Bignum] the 'k' value as a Bignum
Contract Bignum, Nat, RespondTo[:hexdigest] => Bignum
def calc_k(nn, g, hash_klass)
H(hash_klass, [nn, g])
end
typesig :calc_k, [Integer, Integer, :hexdigest] => Bignum

# Private Key (derived from username, password and salt)
#
Expand Down Expand Up @@ -91,13 +94,13 @@ def calc_k(nn, g, hash_klass)
# @param password [String] the 'password' (p) as a String
# @param salt [String] the 'salt' in hex
# @return [Bignum] the Scrypt+HMAC stretched 'x' value as a Bignum
Contract String, String, String => Bignum
def calc_x(username, password, salt)
prehash_pw = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), 'srp-x-1', password)
int_key = RbNaCl::PasswordHash.scrypt(prehash_pw, salt.force_encoding('BINARY'), 2**19, 2**24, 32)
x_hex = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), 'srp-x-2', int_key + username)
x_hex.hex
end
typesig :calc_x, [String, String, String] => Integer

# Random Scrambling Parameter
# u = H(A, B)
Expand All @@ -106,34 +109,34 @@ def calc_x(username, password, salt)
# @param xbb [String] the 'B' value in hex
# @param hash_klass [Digest::SHA1, Digest::SHA256] The hash class that responds to hexdigest
# @return [Bignum] the 'u' value as a Bignum
Contract String, String, RespondTo[:hexdigest] => Bignum
def calc_u(xaa, xbb, hash_klass)
H(hash_klass, [xaa, xbb])
end
typesig :calc_u, [String, String, :hexdigest] => Integer

# Password Verifier
# v = g^x (mod N)
#
# @param x [Bignum] the 'x' value as a Bignum
# @param nn [Bignum] the 'N' value as a Bignum
# @param g [Bignum] the 'g' value as a Bignum
# @param g [Fixnum] the 'g' value as a Fixnum
# @return [Bignum] the client 'v' value as a Bignum
Contract Bignum, Bignum, Fixnum => Bignum
def calc_v(x, nn, g)
mod_pow(g, x, nn)
end
typesig :calc_v, [Integer, Integer, Integer] => Bignum

# Client Ephemeral Value
# A = g^a (mod N)
#
# @param a [Bignum] the 'a' value as a Bignum
# @param nn [Bignum] the 'N' value as a Bignum
# @param g [Bignum] the 'g' value as a Bignum
# @param g [Fixnum] the 'g' value as a Fixnum
# @return [Bignum] the client ephemeral 'A' value as a Bignum
Contract Bignum, Bignum, Fixnum => Bignum
def calc_A(a, nn, g)
mod_pow(g, a, nn)
end
typesig :calc_A, [Integer, Integer, Integer] => Bignum

# Server Ephemeral Value
# B = kv + g^b % N
Expand All @@ -142,12 +145,12 @@ def calc_A(a, nn, g)
# @param k [Bignum] the 'k' value as a Bignum
# @param v [Bignum] the 'v' value as a Bignum
# @param nn [Bignum] the 'N' value as a Bignum
# @param g [Bignum] the 'g' value as a Bignum
# @param g [Fixnum] the 'g' value as a Fixnum
# @return [Bignum] the verifier ephemeral 'B' value as a Bignum
Contract Bignum, Bignum, Bignum, Bignum, Fixnum => Bignum
def calc_B(b, k, v, nn, g)
(k * v + mod_pow(g, b, nn)) % nn
end
typesig :calc_B, [Integer, Integer, Integer, Integer, Integer] => Bignum

# Client Session Key
# S = (B - (k * g^x)) ^ (a + (u * x)) % N
Expand All @@ -158,12 +161,12 @@ def calc_B(b, k, v, nn, g)
# @param x [Bignum] the 'x' value as a Bignum
# @param u [Bignum] the 'u' value as a Bignum
# @param nn [Bignum] the 'N' value as a Bignum
# @param g [Bignum] the 'g' value as a Bignum
# @param g [Fixnum] the 'g' value as a Fixnum
# @return [Bignum] the client 'S' value as a Bignum
Contract Bignum, Bignum, Bignum, Bignum, Bignum, Bignum, Fixnum => Bignum
def calc_client_S(bb, a, k, x, u, nn, g)
mod_pow((bb - k * mod_pow(g, x, nn)), a + u * x, nn)
end
typesig :calc_client_S, [Integer, Integer, Integer, Integer, Integer, Integer, Integer] => Bignum

# Server Session Key
# S = (A * v^u) ^ b % N
Expand All @@ -174,10 +177,10 @@ def calc_client_S(bb, a, k, x, u, nn, g)
# @param u [Bignum] the 'u' value as a Bignum
# @param nn [Bignum] the 'N' value as a Bignum
# @return [Bignum] the verifier 'S' value as a Bignum
Contract Bignum, Bignum, Bignum, Bignum, Bignum => Bignum
def calc_server_S(aa, b, v, u, nn)
mod_pow(aa * mod_pow(v, u, nn), b, nn)
end
typesig :calc_server_S, [Integer, Integer, Integer, Integer, Integer] => Bignum

# M = H(A, B, K)
#
Expand All @@ -186,14 +189,14 @@ def calc_server_S(aa, b, v, u, nn)
# @param xkk [String] the 'K' value in hex
# @param hash_klass [Digest::SHA1, Digest::SHA256] The hash class that responds to hexdigest
# @return [String] the 'M' value in hex
Contract String, String, String, RespondTo[:hexdigest] => String
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
typesig :calc_M, [String, String, String, :hexdigest] => String

# H(A, M, K)
#
Expand All @@ -202,9 +205,9 @@ def calc_M(xaa, xbb, xkk, hash_klass)
# @param xkk [String] the 'K' value in hex
# @param hash_klass [Digest::SHA1, Digest::SHA256] The hash class that responds to hexdigest
# @return [String] the 'H_AMK' value in hex
Contract String, String, String, RespondTo[:hexdigest] => String
def calc_H_AMK(xaa, xmm, xkk, hash_klass)
byte_string = hex_to_bytes([xaa, xmm, xkk].join('')).pack('C*')
hash_klass.hexdigest(byte_string)
end
typesig :calc_H_AMK, [String, String, String, :hexdigest] => String
end
9 changes: 6 additions & 3 deletions lib/sirp/util.rb
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
module SIRP
include Contracts::Core
include Contracts::Builtin

# Convert a hex string to an a array of Integer bytes by first converting
# the String to hex, and then converting that hex to an Array of Integer bytes.
#
# @param str [String] a string to convert
# @return [Array<Integer>] an Array of Integer bytes
Contract String => ArrayOf[Nat]
def hex_to_bytes(str)
[str].pack('H*').unpack('C*')
end
typesig :hex_to_bytes, [String] => Array

# Convert a number to a downcased hex string, prepending '0' to the
# hex string if the hex conversion resulted in an odd length string.
#
# @param num [Integer] a number to convert to a hex string
# @return [String] a hex string
Contract Nat => String
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
typesig :num_to_hex, [Integer] => String

# Constant time string comparison.
# Extracted from Rack::Utils
Expand All @@ -34,6 +37,7 @@ def num_to_hex(num)
# @param a [String] the private value
# @param b [String] the user provided value
# @return [true, false] whether the strings match or not
Contract String, String => Bool
def secure_compare(a, b)
# Do all comparisons on equal length hashes of the inputs
a = Digest::SHA256.hexdigest(a)
Expand All @@ -47,5 +51,4 @@ def secure_compare(a, b)
b.each_byte { |v| r |= v ^ l[i+=1] }
r == 0
end
typesig :secure_compare, [String, String] => Boolean
end
11 changes: 7 additions & 4 deletions lib/sirp/verifier.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module SIRP
class Verifier
include Contracts::Core
include Contracts::Builtin
include SIRP

attr_reader :N # Bignum
Expand All @@ -17,13 +19,13 @@ class Verifier
# Select modulus (N), generator (g), and one-way hash function (SHA1 or SHA256)
#
# @param group [Fixnum] the group size in bits
Contract Nat => Nat
def initialize(group = 2048)
raise ArgumentError, 'must be a known group size' unless [1024, 1536, 2048, 3072, 4096, 6144, 8192].include?(group)

@N, @g, @hash = Ng(group)
@k = calc_k(@N, @g, hash)
end
typesig :initialize, [Fixnum] => Integer

# Phase 0 ; Generate a verifier and salt client-side. This should only be
# used during the initial user registration process. All three values
Expand All @@ -36,6 +38,7 @@ def initialize(group = 2048)
# @param username [String] the authentication username
# @param password [String] the authentication password
# @return [Hash] a Hash of the username, verifier, and salt
Contract String, String => { username: String, verifier: String, salt: String }
def generate_userauth(username, password)
raise ArgumentError, 'username must not be an emoty string' if username.empty?
raise ArgumentError, 'password must not be an emoty string' if password.empty?
Expand All @@ -45,7 +48,6 @@ def generate_userauth(username, password)
v = calc_v(x, @N, @g)
{ username: username, verifier: num_to_hex(v), salt: @salt }
end
typesig :generate_userauth, [String, String] => Hash

# Phase 1 : Step 2 : Create a challenge for the client, and a proof to be stored
# on the server for later use when verifying the client response.
Expand All @@ -55,6 +57,8 @@ def generate_userauth(username, password)
# @param xsalt [String] the server stored salt for the username in hex
# @param xaa [String] the client provided 'A' value in hex
# @return [Hash] a Hash with the challenge for the client and a proof for the server
Contract String, String, String, String => { challenge: { B: String, salt: String },
proof: { A: String, B: String, b: String, I: String, s: String, v: String } }
def get_challenge_and_proof(username, xverifier, xsalt, xaa)
raise ArgumentError, 'username must not be an empty string' if username.empty?
raise ArgumentError, 'xverifier must be a hex string' unless xverifier =~ /^[a-fA-F0-9]+$/
Expand All @@ -73,7 +77,6 @@ def get_challenge_and_proof(username, xverifier, xsalt, xaa)
proof: { A: xaa, B: @B, b: num_to_hex(@b), I: username, s: xsalt, v: xverifier }
}
end
typesig :get_challenge_and_proof, [String, String, String, String] => Hash

#
# Phase 2 : Step 1 : See Client#start_authentication
Expand All @@ -93,6 +96,7 @@ def get_challenge_and_proof(username, xverifier, xsalt, xaa)
# @param proof [Hash] the server stored proof Hash with keys A, B, b, I, s, v
# @param client_M [String] the client provided 'M' value in hex
# @return [String] the H_AMK value in hex for the client, or empty string if verification failed
Contract ({A: String, B: String, b: String, I: String, s: String, v: String}), String => String
def verify_session(proof, client_M)
raise ArgumentError, 'proof must be a hash' unless proof.is_a?(Hash)
# gracefully handle string or symbol keys
Expand Down Expand Up @@ -124,6 +128,5 @@ def verify_session(proof, client_M)
''
end
end
typesig :verify_session, [Hash, String] => String
end
end
2 changes: 1 addition & 1 deletion sirp.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Gem::Specification.new do |spec|
spec.add_runtime_dependency 'rbnacl', '~> 3.4.0'
spec.add_runtime_dependency 'sysrandom', '~> 1.0'
spec.add_runtime_dependency 'hashie', '~> 3.4'
spec.add_runtime_dependency 'rubype', '~> 0.3'
spec.add_runtime_dependency 'contracts', '~> 0.14'

spec.add_development_dependency 'bundler'
spec.add_development_dependency 'rake'
Expand Down

0 comments on commit 23186ed

Please sign in to comment.