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

Commit

Permalink
Strengthen calc_x, and the derived verifier, using SHA256, HMAC_SHA25…
Browse files Browse the repository at this point in the history
…6, and Scrypt
  • Loading branch information
grempe committed May 20, 2016
1 parent a865e3b commit 8f949b5
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 13 deletions.
58 changes: 57 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
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
2 changes: 1 addition & 1 deletion lib/sirp/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def process_challenge(username, password, xsalt, xbb)
# SRP-6a safety check
return false if (bb % @N).zero?

x = calc_x(username, password, xsalt, hash)
x = calc_x(username, password, xsalt)
u = calc_u(@A, xbb, @N, hash)

# SRP-6a safety check
Expand Down
49 changes: 45 additions & 4 deletions lib/sirp/sirp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,51 @@ def calc_k(n, g, hash_klass)
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
#
# The spec calls for calculating 'x' using:
#
# x = H(salt || H(username || ':' || password))
#
# However, this can be greatly strengthened against attacks
# on the verififier. The specified scheme requires only brute
# forcing 2x SHA1 or SHA256 hashes and a modular exponentiation.
#
# The implementation that follows is based on extensive discussion with
# Dmitry Chestnykh (@dchest). This approach is also informed by
# the security audit done on the Spider Oak crypton.io project which
# can be viewed here and talks about the weaknesses in the original
# SRP spec when considering brute force attacks on the verifier.
#
# See page 12:
# https://web.archive.org/web/20150403175113/http://www.leviathansecurity.com/wp-content/uploads/SpiderOak-Crypton_pentest-Final_report_u.pdf
#
# This strengthened version uses SHA256 and HMAC_SHA256 in concert
# with the scrypt memory and CPU hard key stretching algorithm to
# derive a much stronger 'x' value. Since the verifier is directly
# derived from 'x' using Modular Exponentiation this makes brute force
# attack much less likely. The new algorithm is:
#
# prehash_pw = HMAC_SHA256('srp-x-1', password)
# int_key = scrypt(prehash_pw, SHA256(salt), ...)
# HMAC_SHA256('srp-x-2', int_key + username)
#
# The scrypt values equate to the 'interactive' use constants in libsodium.
# The values given to the RbNaCl::PasswordHash.scrypt can be converted for use
# with https://github.com/dchest/scrypt-async-js using the following conversions:
#
#
# CPU/memory cost parameters
# Conversion from RbNaCl / libsodium and scrypt-async-js
# SCRYPT_OPSLIMIT_INTERACTIVE == 2**19 == (2**24 / 32) == 524288 == logN 14
# SCRYPT_OPSLIMIT_SENSITIVE == 2**25 == (2**30 / 32) == 33554432 == logN 20
#
# The value returned should be the final HMAC_SHA256 hex converted to an Integer
#
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, Digest::SHA256.digest(salt), 2**19, 2**24, 32)
x_hex = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), 'srp-x-2', int_key + username)
x_hex.hex
end

# Random scrambling parameter
Expand Down
2 changes: 1 addition & 1 deletion lib/sirp/verifier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def generate_userauth(username, password)
raise ArgumentError, 'password must be a string' unless password.is_a?(String) && !password.empty?

@salt ||= SecureRandom.hex(10)
x = calc_x(username, password, @salt, hash)
x = calc_x(username, password, @salt)
v = calc_v(x, @N, @g)
{ username: username, verifier: num_to_hex(v), salt: @salt }
end
Expand Down
4 changes: 2 additions & 2 deletions spec/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@
mm = client.process_challenge(@username, @password, @salt, bb)

# Client keys
expect(client.S).to eq '7f44592cc616e0d761b2d3309d513b69b386c35f3ed9b11e6d43f15799b673d6dcfa4117b4456af978458d62ad61e1a37be625f46d2a5bd9a50aae359e4541275f0f4bd4b4caed9d2da224b491231f905d47abd9953179aa608854b84a0e0c6195e73715932b41ab8d0d4a2977e7642163be6802c5907fb9e233b8c96e457314'
expect(client.K).to eq '404bf923682abeeb3c8c9164d2cdb6b6ba21b64d'
expect(client.S).to eq '8f3ba58c95e2e3b6b99eaed3e46c29f1fc44dbc47411ca24cd4e72998df90ccc8e54ad2c39f45a68d4c3aae2066aaf8f158205d34bc64b4a66623087de9a15f71b3023d5e7bc468a78f0f3b89d6b3ff6259aea0aef2ec0677df83901312305809de71cdf67e260a432d8c195c9a8e24452da2208691723a20fff571e8a5c7e31'
expect(client.K).to eq 'fe9581d9df8120ab7af338e015a362049a822ad3'
end

it 'should verify true with matching server H_AMK' do
Expand Down
6 changes: 3 additions & 3 deletions spec/sirp_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,9 @@

context 'calc_x' do
it 'should calculate expected results' do
x = calc_x(@username, @password, @salt, Digest::SHA1)
expect(('%x' % x)).to eq 'bdd0a4e1c9df4082684d8d358b8016301b025375'
expect(('%b' % x).length).to eq 160
x = calc_x(@username, @password, @salt)
expect(('%x' % x)).to eq 'e34be086aeade02b32e2941077b262664ad120d5146e3a96ab8069254860b14c'
expect(('%b' % x).length).to eq 256
end
end

Expand Down
2 changes: 1 addition & 1 deletion spec/verifier_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
v = auth[:verifier]
salt = auth[:salt]
expect(salt).to eq @salt
expect(v).to eq '321307d87ca3462f5b0cb5df295bea04498563794e5401899b2f32dd5cab5b7de9da78e7d62ea235e6d7f43a4ea09fea7c0dafdee6e79a1d12e2e374048deeaf5ba7c68e2ad952a3f5dc084400a7f1599a31d6d9d50269a9208db88f84090e8aa3c7b019f39529dcc19baa985a8d7ffb2d7628071d2313c9eaabc504d3333688'
expect(v).to eq 'a8b75c5a887cd27506bad148d24b00ef93d8a51418dd663508c394f34b657f62edb514fa67f67951a260d2e36d938a583b6a072b8fe93ca9305ea840e778e943e57e0c9cf8f360e415e27217c4397844074e5f2073096d3fbe04d472fa9dafb33dd3b341904754167031cd4f35f27c8a5c0b77a397516c2db20a0a4a3d0eb7f0'
end

it 'should generate salt and calculate verifier' do
Expand Down

0 comments on commit 8f949b5

Please sign in to comment.