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

New API PoC #4

Open
wants to merge 48 commits into
base: calc_x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
8f949b5
Strengthen calc_x, and the derived verifier, using SHA256, HMAC_SHA25…
grempe May 20, 2016
27db641
Don't SHA256 the salt any longer in calc_x. Just use a 32 Byte salt. …
grempe May 22, 2016
18bbb0b
Typo
grempe May 22, 2016
1275295
Split common utility functions out to their own file
grempe May 26, 2016
e965c9a
Return nil instead of false on certain failures. Make sure docs refle…
grempe May 26, 2016
33d6aaa
calc_H_AMK should return a hex value, not a Bignum that we then have …
grempe May 26, 2016
c21a9f3
Document calc_M
grempe May 27, 2016
bc5976d
Document calc_server_S
grempe May 27, 2016
5990a1c
calc_client_S had an extra % N in the formula which is not present in…
grempe May 27, 2016
9fbe273
Update calc_B docs and order the formula as specified for readability
grempe May 27, 2016
35caf24
Update calc_A docs
grempe May 27, 2016
368a9e9
Update docs for calc_v
grempe May 27, 2016
d9f5dfa
Update docs for calc_u
grempe May 27, 2016
bccc5ea
Update docs for calc_x
grempe May 27, 2016
8cc4009
Update docs for calc_k
grempe May 27, 2016
6986575
Update docs for mod_exp
grempe May 27, 2016
f02ba8c
Rename mod_exp to mod_pow
grempe May 27, 2016
079e3f3
Use rubype to check types of arguments and return values from all fun…
grempe May 28, 2016
00251cc
Teach secure_compare to always hash the inputs on its own. Caller sho…
grempe May 28, 2016
8f95d04
Reverse the leakage of OpenSSL::BN type all over the app and enforce …
grempe May 28, 2016
dd2d918
Refactor away the sha_str method which was only used once.
grempe May 28, 2016
5a20206
Refactor away sha_hex method. Directly use .pack('H*') for now.
grempe May 28, 2016
9b423a1
Breaking change. Refactor H() one-way hash function to simplify.
grempe May 28, 2016
9879da5
Raise an exception on SRP-6a safety check failures instead of returni…
grempe May 28, 2016
b90cd60
Remove some comments and tighten up a Type check
grempe May 28, 2016
98960d4
Return an empty string instead of nil if auth fails. Also allows stri…
grempe May 28, 2016
073a5f7
Document client attribute types
grempe May 28, 2016
7f32adb
Document verifier attribute types
grempe May 28, 2016
21955f1
Whitespace
grempe May 28, 2016
db7c5b8
Define variables on their own lines.
grempe Jun 22, 2016
5de4be7
Fixes #1, Swap usage of rubype for contracts gem to enforce types. Ba…
grempe Jun 22, 2016
a520977
Allow proper handling of safety check exceptions
Ptico Jun 23, 2016
96355fe
Merge pull request #2 from Ptico/calc_x
grempe Jun 26, 2016
71250e5
Compatible server API PoC
Ptico Dec 7, 2016
03ab984
Add Server::Start tests
Ptico Feb 17, 2017
656bb6e
Start Server::Finish tests, move predefined values to shared context
Ptico Feb 17, 2017
0020d30
Done Server::Finish tests
Ptico Feb 17, 2017
a455941
Ooops! add sirp.rb
Ptico Feb 17, 2017
01e9aec
Implement SIRP::Register, dependency tuning, add frozen string litera…
Ptico Mar 1, 2017
7e0067c
Fix empty string Server::Start spec
Ptico Mar 2, 2017
f33dd4b
Default group and hash for constructors
Ptico Mar 2, 2017
a43ab56
Truncate whitespace chars in arguments validations
Ptico Mar 2, 2017
7af0739
Move b generation to dedicated method for better debugging
Ptico Mar 2, 2017
7e0d62f
Subclass backend for better extendability
Ptico Mar 2, 2017
5505f71
Add Backend::Digest for 2.0 compatibility
Ptico Mar 2, 2017
fffc0f8
Move helper functions to Utils object
Ptico Mar 2, 2017
64989e8
Symbolize hashes from user input
Ptico Mar 3, 2017
d2a4bb6
Add client
Ptico Sep 27, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
--color
--require spec_helper
60 changes: 58 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ which is a 'zero-knowledge' mutual authentication system.

SRP is an protocol that allows for mutual authentication of a client and
server over an insecure network connection without revealing the password to the
server or an evesdropper. If the client lacks the user's password, or the server
server or an eavesdropper. If the client lacks the user's password, or the server
lacks the proper verification key, the authentication will fail. This approach
is much more secure than the vast majority of authentication systems in common use
since the password is never sent over the wire. The password is impossible to
Expand Down Expand Up @@ -137,7 +137,7 @@ this verification at [https://www.rempe.us/keys/](https://www.rempe.us/keys/).
This implementation has been tested for compatibility with the following SRP-6a
compliant third-party libraries:

[JSRP / JavaScript](https://github.com/alax/jsrp)
[grempe/jsrp (JavaScript)](https://github.com/grempe/jsrp)

## SRP-6a Protocol Design

Expand Down Expand Up @@ -203,6 +203,62 @@ The two parties also employ the following safeguards:
user's proof is incorrect, it must abort without showing its own proof of K.
```

### Implementation Decisions

The interoperability of different implementations of SRP is elusive. The spec
leaves a number of decisions up to the implementer. The choice of hashing
algorithm (H) is left open, the method of verifying shared keys (H_AMK) is
not clearly specified, and the generation of the Verifier (v) is not considered
very strong by modern standards.

It is also not specified how the client and server should exchange information
over the wire (binary, hex, protobuf, etc).

It is therefore no wonder that most implementations don't work together.

This library has also made its own choices. This implementation provides Ruby
code that makes a choice for strength where possible. This code is suitable for
use as both a Ruby client and Ruby SRP server. There is also a JavaScript
client based on the work of alax/jsrp which has been modified to be compatible.

It is unlikely that any other implementation will just work out of the box. No
support is provided for any other implementations not listed here.

#### Hashing Algorithm

The hashing algorithm used internally is either `SHA1` or `SHA256`. Only group sizes
`1024` and `1536` use `SHA1` for legacy support, and the rest will use `SHA256`.

This matches the choices made in the `jsrp` package.

#### Calculating `x`

The derivation of the private key `x` has been strengthened in this
implementation and makes use of SHA256, HMAC-SHA256, and Scrypt. Scrypt
is a modern memory and CPU hard key derivation function and is used
to protect against the possibility of a brute-force attack on the Verifier.
See the `calc_x` method in `lib/sirp/sirp.rb` for details.

#### Proof of `K`

According to the Wikipaedia page for Secure Remote Password implementations will
often choose different methods to prove that the client and server have both
negotiatied the same keys.

```
Carol → Steve: M1 = H[H(N) XOR H(g) | H(I) | s | A | B | KCarol]. Steve verifies M1.
Steve → Carol: M2 = H(A | M1 | KSteve). Carol verifies M2.
```

and

```
Carol → Steve: M1 = H(A | B | SCarol). Steve verifies M1.
Steve → Carol: M2 = H(A | M1 | SSteve). Carol verifies M2.
```

This implementation makes use of the second method. See `SIRP.calc_H_AMK`.

## Usage Example

In this example the client and server steps are interleaved for demonstration
Expand Down
14 changes: 8 additions & 6 deletions bin/console
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
#!/usr/bin/env ruby

require 'bundler/setup'
require 'sirp'
require 'sirp/all'

# You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like.

# (If you use this, don't forget to add pry to your Gemfile!)
require 'pry'
Pry.start

require 'irb'
IRB.start
begin
require 'pry'
Pry.start
rescue
require 'irb'
IRB.start
end
17 changes: 11 additions & 6 deletions lib/sirp.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
require 'openssl'
require 'digest'
require 'digest/sha2'
require 'rbnacl/libsodium'
require 'securer_randomer'
require 'sirp/sirp'
require 'rbnacl'
require 'openssl'

module SIRP
SafetyCheckError = Class.new(StandardError)
end

require 'sirp/utils'
require 'sirp/parameters'
require 'sirp/client'
require 'sirp/verifier'
require 'sirp/backend'

require 'sirp/version'
5 changes: 5 additions & 0 deletions lib/sirp/all.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
require 'sirp/server'
require 'sirp/register'
require 'sirp/client'

require 'sirp/backend/scrypt_hmac'
192 changes: 192 additions & 0 deletions lib/sirp/backend.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# frozen_string_literal: true

require 'contracts'

module SIRP
class Backend
include Contracts::Core
include Contracts::Builtin

attr_reader :prime

def initialize(group=Prime[2048], hash=Digest::SHA256)
@prime = group
@hash = hash
end

# Modular Exponentiation
# https://en.m.wikipedia.org/wiki/Modular_exponentiation
# http://rosettacode.org/wiki/Modular_exponentiation#Ruby
#
# a^b (mod m)
#
# @param a [Integer] the base value as an Integer, depending on size
# @param b [Integer] the exponent value as an Integer
# @param m [Integer] the modulus value as an Integer
# @return [Integer] the solution as an Integer
Contract Integer, Nat, Nat => Integer
def mod_pow(a, b, m)
# Convert type and use OpenSSL::BN#mod_exp to do the calculation
# Convert back to an Integer so OpenSSL::BN doesn't leak everywhere
a.to_bn.mod_exp(b, m).to_i
end

# One-Way Hash Function
#
# @param a [Array] the Array of values to be hashed together
# @return [Integer] the hexdigest as an Integer
Contract ArrayOf[Or[String, Nat]] => Integer
def H(a)
hasher = @hash.new

a.compact.map do |v|
xv = v.is_a?(String) ? v : Utils.num_to_hex(v)
hasher.update(xv.downcase)
end

digest = hasher.hexdigest
digest.hex
end

# Multiplier Parameter
# k = H(N, g) (in SRP-6a)
#
# @return [Integer] the 'k' value as an Integer
Contract None => Integer
def k
@k ||= H([prime.N, prime.g].map(&:to_s))
end

# Abstract
# Private Key (derived from username, password and salt)
#
# The spec calls for calculating 'x' using:
#
# x = H(salt || H(username || ':' || password))
#
# @param username [String] the 'username' (I) as a String
# @param password [String] the 'password' (p) as a String
# @param salt [String] the 'salt' in hex
# @return [Integer] the Scrypt+HMAC stretched 'x' value as an Integer
Contract String, String, String => Integer
def calc_x(username, password, salt)
fail NotImplementedError
end

# Random Scrambling Parameter
# u = H(A, B)
#
# @param xaa [String] the 'A' value in hex
# @param xbb [String] the 'B' value in hex
# @return [Integer] the 'u' value as an Integer
Contract String, String => Integer
def calc_u(xaa, xbb)
u = H([xaa, xbb])

u.zero? ? fail(SafetyCheckError, 'u cannot equal 0') : u
end

# Password Verifier
# v = g^x (mod N)
#
# @param x [Integer] the 'x' value as an Integer
# @return [Integer] the client 'v' value as an Integer
Contract Integer => Integer
def calc_v(x)
mod_pow(prime.g, x, prime.N)
end

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

# Server Ephemeral Value
# B = kv + g^b % N
#
# @param b [Integer] the 'b' value as an Integer
# @param v [Integer] the 'v' value as an Integer
# @return [Integer] the verifier ephemeral 'B' value as an Integer
Contract Integer, Integer => Integer
def calc_B(b, v)
(k * v + mod_pow(prime.g, b, prime.N)) % prime.N
end

# Client Session Key
# S = (B - (k * g^x)) ^ (a + (u * x)) % N
#
# @param bb [Integer] the 'B' value as an Integer
# @param a [Integer] the 'a' value as an Integer
# @param x [Integer] the 'x' value as an Integer
# @param u [Integer] the 'u' value as an Integer
# @return [Integer] the client 'S' value as an Integer
Contract Integer, Integer, Integer, Integer, Integer => Integer
def calc_client_S(bb, a, x, u)
mod_pow((bb - k * mod_pow(prime.g, x, prime.N)), a + u * x, prime.N)
end

# Server Session Key
# S = (A * v^u) ^ b % N
#
# @param aa [Integer] the 'A' value as a String
# @param b [Integer] the 'b' value as an Integer
# @param v [Integer] the 'v' value as an Integer
# @param u [Integer] the 'u' value as an Integer
# @return [Integer] the verifier 'S' value as an Integer
Contract String, Integer, Integer, Integer => Integer
def calc_server_S(aa, b, v, u)
mod_pow(aa.to_i(16) * mod_pow(v, u, prime.N), b, prime.N)
end

# M = H( H(N) XOR H(g), H(I), s, A, B, K)
# @param username [String] plain username
# @param xsalt [String] salt value in hex
# @param xaa [String] the 'A' value in hex
# @param xbb [String] the 'B' value in hex
# @param xkk [String] the 'K' value in hex
# @return [String] the 'M' value in hex
Contract String, String, String, String, String => String
def calc_M(username, xsalt, xaa, xbb, xkk)
hn = @hash.hexdigest(prime.N.to_s)
hg = @hash.hexdigest(prime.g.to_s)
hxor = hn.to_i(16) ^ hg.to_i(16)
hi = @hash.hexdigest(username)
Utils.num_to_hex(H([[hxor, hi.to_i(16), xsalt, xaa.to_i(16), xbb.to_i(16), xkk].map(&:to_s).join]))
end

# K = H(S)
#
# @param ss [Integer] the 'S' value as an Integer
# @return [String] the 'K' value in hex
Contract String => String
def calc_K(ss)
@hash.hexdigest(ss.to_i(16).to_s)
end

# H(A, M, K)
#
# @param xaa [String] the 'A' value in hex
# @param xmm [String] the 'M' value in hex
# @param xkk [String] the 'K' value in hex
# @return [String] the 'H_AMK' value in hex
Contract String, String, String => String
def calc_H_AMK(xaa, xmm, xkk)
@hash.hexdigest(xaa.to_i(16).to_s + xmm + xkk)
end

# Client Ephemeral Value
# A = g^a (mod N)
#
# @param a [Bignum] the 'a' value as a Bignum
# @return [Bignum] the client ephemeral 'A' value as a Bignum
Contract Bignum => Bignum
def calc_A(a)
mod_pow(prime.g, a, prime.N)
end
end
end
21 changes: 21 additions & 0 deletions lib/sirp/backend/digest.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module SIRP
class Backend
class Digest < self

# Private Key (derived from username, password and salt)
#
# x = H(salt || H(username || ':' || password))
#
# @param username [String] the 'username' (I) as a String
# @param password [String] the 'password' (p) as a String
# @param salt [String] the 'salt' in hex
# @return [Integer] the Scrypt+HMAC stretched 'x' value as an Integer
Contract String, String, String => Integer
def calc_x(username, password, salt)
spad = salt.length.odd? ? '0' : ''
h = spad + salt + @hash.hexdigest([username, password].join(':'))
@hash.hexdigest([h].pack('H*')).hex
end
end
end
end
Loading