chore: rename project coven → hack-house ⛧

Rebrand the Rust client crate (coven/ → hh/, package+binary "hack-house"),
README, CLI strings, and branch (coven → hack-house). Gitea repo renamed
cmd-chat → hack-house to match. Crypto/server logic unchanged; selftest +
golden-vector test still green, binary is now `hack-house`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-05-30 13:29:14 -07:00
parent 82a04f3e12
commit bb1d662ee1
2730 changed files with 712933 additions and 46 deletions
@@ -0,0 +1,31 @@
_mod = None
try:
import srp._ctsrp
_mod = srp._ctsrp
except (ImportError, OSError):
pass
if not _mod:
import srp._pysrp
_mod = srp._pysrp
User = _mod.User
Verifier = _mod.Verifier
create_salted_verification_key = _mod.create_salted_verification_key
SHA1 = _mod.SHA1
SHA224 = _mod.SHA224
SHA256 = _mod.SHA256
SHA384 = _mod.SHA384
SHA512 = _mod.SHA512
NG_1024 = _mod.NG_1024
NG_2048 = _mod.NG_2048
NG_4096 = _mod.NG_4096
NG_8192 = _mod.NG_8192
NG_CUSTOM = _mod.NG_CUSTOM
rfc5054_enable = _mod.rfc5054_enable
no_username_in_x = _mod.no_username_in_x
@@ -0,0 +1,654 @@
# N A large safe prime (N = 2q+1, where q is prime)
# All arithmetic is done modulo N.
# g A generator modulo N
# k Multiplier parameter (k = H(N, g) in SRP-6a, k = 3 for legacy SRP-6)
# s User's salt
# I Username
# p Cleartext Password
# H() One-way hash function
# ^ (Modular) Exponentiation
# u Random scrambling parameter
# a,b Secret ephemeral values
# A,B Public ephemeral values
# x Private key (derived from p and s)
# v Password verifier
import os
import sys
import hashlib
import random
import ctypes
import time
_rfc5054_compat = False
_no_username_in_x = False
def rfc5054_enable(enable=True):
global _rfc5054_compat
_rfc5054_compat = enable
def no_username_in_x(enable=True):
global _no_username_in_x
_no_username_in_x = enable
SHA1 = 0
SHA224 = 1
SHA256 = 2
SHA384 = 3
SHA512 = 4
NG_1024 = 0
NG_2048 = 1
NG_4096 = 2
NG_8192 = 3
NG_CUSTOM = 4
_hash_map = { SHA1 : hashlib.sha1,
SHA224 : hashlib.sha224,
SHA256 : hashlib.sha256,
SHA384 : hashlib.sha384,
SHA512 : hashlib.sha512 }
_ng_const = (
# 1024-bit
(('''\
EEAF0AB9ADB38DD69C33F80AFA8FC5E86072618775FF3C0B9EA2314C9C256576D674DF7496\
EA81D3383B4813D692C6E0E0D5D8E250B98BE48E495C1D6089DAD15DC7D7B46154D6B6CE8E\
F4AD69B15D4982559B297BCF1885C529F566660E57EC68EDBC3C05726CC02FD4CBF4976EAA\
9AFD5138FE8376435B9FC61D2FC0EB06E3''').encode('ascii'),
b"2"),
# 2048
(('''\
AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4\
A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF60\
95179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF\
747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B907\
8717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB37861\
60279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DB\
FBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73''').encode('ascii'),
b"2"),
# 4096
(('''\
FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E08\
8A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B\
302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9\
A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE6\
49286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8\
FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D\
670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C\
180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718\
3995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D\
04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7D\
B3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D226\
1AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200C\
BBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFC\
E0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B26\
99C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB\
04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2\
233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127\
D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199\
FFFFFFFFFFFFFFFF''').encode('ascii'),
b"5"),
# 8192
(('''\
FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E08\
8A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B\
302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9\
A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE6\
49286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8\
FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D\
670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C\
180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718\
3995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D\
04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7D\
B3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D226\
1AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200C\
BBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFC\
E0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B26\
99C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB\
04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2\
233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127\
D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934028492\
36C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406\
AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918\
DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B33205151\
2BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03\
F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97F\
BEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AA\
CC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58B\
B7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632\
387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E\
6DBE115974A3926F12FEE5E438777CB6A932DF8CD8BEC4D073B931BA\
3BC832B68D9DD300741FA7BF8AFC47ED2576F6936BA424663AAB639C\
5AE4F5683423B4742BF1C978238F16CBE39D652DE3FDB8BEFC848AD9\
22222E04A4037C0713EB57A81A23F0C73473FC646CEA306B4BCBC886\
2F8385DDFA9D4B7FA2C087E879683303ED5BDD3A062B3CF5B3A278A6\
6D2A13F83F44F82DDF310EE074AB6A364597E899A0255DC164F31CC5\
0846851DF9AB48195DED7EA1B1D510BD7EE74D73FAF36BC31ECFA268\
359046F4EB879F924009438B481C6CD7889A002ED5EE382BC9190DA6\
FC026E479558E4475677E9AA9E3050E2765694DFC81F56E880B96E71\
60C980DD98EDD3DFFFFFFFFFFFFFFFFF''').encode('ascii'),
b'13')
)
#N_HEX = "AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF6095179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B9078717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB3786160279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DBFBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73"
#G_HEX = "2"
#HNxorg = None
dlls = list()
platform = sys.platform
if platform == 'darwin':
dlls.append( ctypes.cdll.LoadLibrary('libssl.32.dylib') )
elif 'win' in platform:
for d in ('libeay32.dll', 'libssl32.dll', 'ssleay32.dll'):
try:
dlls.append( ctypes.cdll.LoadLibrary(d) )
except:
pass
else:
try:
dlls.append( ctypes.cdll.LoadLibrary('libssl.so.1.1.0') )
except OSError:
dlls.append( ctypes.cdll.LoadLibrary('libssl.so') )
class BIGNUM_Struct (ctypes.Structure):
_fields_ = [ ("d", ctypes.c_void_p),
("top", ctypes.c_int),
("dmax", ctypes.c_int),
("neg", ctypes.c_int),
("flags", ctypes.c_int) ]
class BN_CTX_Struct (ctypes.Structure):
_fields_ = [ ("_", ctypes.c_byte) ]
BIGNUM = ctypes.POINTER( BIGNUM_Struct )
BN_CTX = ctypes.POINTER( BN_CTX_Struct )
def load_func( name, args, returns = ctypes.c_int):
d = sys.modules[ __name__ ].__dict__
f = None
for dll in dlls:
try:
f = getattr(dll, name)
f.argtypes = args
f.restype = returns
d[ name ] = f
return
except:
pass
raise ImportError('Unable to load required functions from SSL dlls')
load_func( 'BN_new', [], BIGNUM )
load_func( 'BN_free', [ BIGNUM ], None )
load_func( 'BN_clear', [ BIGNUM ], None )
load_func( 'BN_CTX_new', [] , BN_CTX )
load_func( 'BN_CTX_free', [ BN_CTX ], None )
load_func( 'BN_set_flags', [ BIGNUM, ctypes.c_int ], None )
BN_FLG_CONSTTIME = 0x04
load_func( 'BN_cmp', [ BIGNUM, BIGNUM ], ctypes.c_int )
load_func( 'BN_num_bits', [ BIGNUM ], ctypes.c_int )
load_func( 'BN_add', [ BIGNUM, BIGNUM, BIGNUM ] )
load_func( 'BN_sub', [ BIGNUM, BIGNUM, BIGNUM ] )
load_func( 'BN_mul', [ BIGNUM, BIGNUM, BIGNUM, BN_CTX ] )
load_func( 'BN_div', [ BIGNUM, BIGNUM, BIGNUM, BIGNUM, BN_CTX ] )
load_func( 'BN_mod_exp', [ BIGNUM, BIGNUM, BIGNUM, BIGNUM, BN_CTX ] )
load_func( 'BN_rand', [ BIGNUM, ctypes.c_int, ctypes.c_int, ctypes.c_int ] )
load_func( 'BN_bn2bin', [ BIGNUM, ctypes.c_char_p ] )
load_func( 'BN_bin2bn', [ ctypes.c_char_p, ctypes.c_int, BIGNUM ], BIGNUM )
load_func( 'BN_hex2bn', [ ctypes.POINTER(BIGNUM), ctypes.c_char_p ] )
load_func( 'BN_bn2hex', [ BIGNUM ], ctypes.c_char_p )
load_func( 'CRYPTO_free', [ ctypes.c_char_p ] )
load_func( 'RAND_seed', [ ctypes.c_char_p, ctypes.c_int ] )
def BN_num_bytes(a):
return ((BN_num_bits(a)+7)//8)
def BN_mod(rem,m,d,ctx):
return BN_div(None, rem, m, d, ctx)
def BN_is_zero( n ):
return n[0].top == 0
def bn_to_bytes( n ):
b = ctypes.create_string_buffer( BN_num_bytes(n) )
BN_bn2bin(n, b)
return b.raw
def bytes_to_bn( dest_bn, bytes ):
BN_bin2bn(bytes, len(bytes), dest_bn)
def H_str( hash_class, dest_bn, s ):
d = hash_class(s).digest()
buff = ctypes.create_string_buffer( s )
BN_bin2bn(d, len(d), dest)
def H_bn( hash_class, dest, n ):
bin = ctypes.create_string_buffer( BN_num_bytes(n) )
BN_bn2bin(n, bin)
d = hash_class( bin.raw ).digest()
BN_bin2bn(d, len(d), dest)
def H_bn_bn( hash_class, dest, n1, n2, width ):
h = hash_class()
bin1 = ctypes.create_string_buffer( BN_num_bytes(n1) )
bin2 = ctypes.create_string_buffer( BN_num_bytes(n2) )
BN_bn2bin(n1, bin1)
BN_bn2bin(n2, bin2)
if _rfc5054_compat:
h.update(bytes(width - len(bin1.raw)))
h.update( bin1.raw )
if _rfc5054_compat:
h.update(bytes(width - len(bin2.raw)))
h.update( bin2.raw )
d = h.digest()
BN_bin2bn(d, len(d), dest)
def H_bn_str( hash_class, dest, n, s ):
h = hash_class()
bin = ctypes.create_string_buffer( BN_num_bytes(n) )
BN_bn2bin(n, bin)
h.update( bin.raw )
h.update( s )
d = h.digest()
BN_bin2bn(d, len(d), dest)
def calculate_x( hash_class, dest, salt, username, password ):
username = username.encode() if hasattr(username, 'encode') else username
password = password.encode() if hasattr(password, 'encode') else password
if _no_username_in_x:
username = b''
up = hash_class(username + b':' + password).digest()
H_bn_str( hash_class, dest, salt, up )
BN_set_flags(dest, BN_FLG_CONSTTIME)
def update_hash( ctx, n ):
buff = ctypes.create_string_buffer( BN_num_bytes(n) )
BN_bn2bin(n, buff)
ctx.update( buff.raw )
def calculate_M( hash_class, N, g, I, s, A, B, K ):
I = I.encode() if hasattr(I, 'encode') else I
h = hash_class()
h.update( HNxorg( hash_class, N, g ) )
h.update( hash_class(I).digest() )
update_hash( h, s )
update_hash( h, A )
update_hash( h, B )
h.update( K )
return h.digest()
def calculate_H_AMK( hash_class, A, M, K ):
h = hash_class()
update_hash( h, A )
h.update( M )
h.update( K )
return h.digest()
def HNxorg( hash_class, N, g ):
bN = ctypes.create_string_buffer( BN_num_bytes(N) )
bg = ctypes.create_string_buffer( BN_num_bytes(g) )
BN_bn2bin(N, bN)
BN_bn2bin(g, bg)
padding = len(bN) - len(bg) if _rfc5054_compat else 0
hN = hash_class( bN.raw ).digest()
hg = hash_class( b''.join([ b'\0'*padding, bg.raw ]) ).digest()
return ( ''.join( chr( hN[i] ^ hg[i] ) for i in range(0,len(hN)) ) ).encode('latin1')
def get_ngk( hash_class, ng_type, n_hex, g_hex, ctx ):
if ng_type < NG_CUSTOM:
n_hex, g_hex = _ng_const[ ng_type ]
N = BN_new()
g = BN_new()
k = BN_new()
BN_hex2bn( N, n_hex )
BN_hex2bn( g, g_hex )
H_bn_bn(hash_class, k, N, g, width=BN_num_bytes(N))
if _rfc5054_compat:
BN_mod(k, k, N, ctx)
return N, g, k
def create_salted_verification_key( username, password, hash_alg=SHA1, ng_type=NG_2048, n_hex=None, g_hex=None, salt_len=4, k_hex=None ):
if ng_type == NG_CUSTOM and (n_hex is None or g_hex is None):
raise ValueError("Both n_hex and g_hex are required when ng_type = NG_CUSTOM")
s = BN_new()
v = BN_new()
x = BN_new()
ctx = BN_CTX_new()
hash_class = _hash_map[ hash_alg ]
N,g,k = get_ngk( hash_class, ng_type, n_hex, g_hex, ctx )
BN_rand(s, salt_len * 8, -1, 0);
calculate_x( hash_class, x, s, username, password )
BN_mod_exp(v, g, x, N, ctx)
salt = bn_to_bytes( s )
verifier = bn_to_bytes( v )
BN_free(s)
BN_free(v)
BN_free(x)
BN_free(N)
BN_free(g)
BN_free(k)
BN_CTX_free(ctx)
return salt, verifier
class Verifier:
def __init__(self, username, bytes_s, bytes_v, bytes_A=None, hash_alg=SHA1, ng_type=NG_2048, n_hex=None, g_hex=None, bytes_b=None, k_hex=None):
if ng_type == NG_CUSTOM and (n_hex is None or g_hex is None):
raise ValueError("Both n_hex and g_hex are required when ng_type = NG_CUSTOM")
if bytes_b and len(bytes_b) != 32:
raise ValueError("32 bytes required for bytes_b")
self.B = BN_new()
self.K = None
self.S = BN_new()
self.u = BN_new()
self.b = BN_new()
self.s = BN_new()
self.v = BN_new()
self.tmp1 = BN_new()
self.tmp2 = BN_new()
self.ctx = BN_CTX_new()
self.I = username
self.M = None
self.H_AMK = None
self._authenticated = False
self.safety_failed = False
hash_class = _hash_map[ hash_alg ]
N,g,k = get_ngk( hash_class, ng_type, n_hex, g_hex, self.ctx )
if k_hex is not None:
BN_hex2bn(k, k_hex)
self.hash_class = hash_class
self.N = N
self.g = g
self.k = k
bytes_to_bn( self.s, bytes_s )
bytes_to_bn( self.v, bytes_v )
if bytes_A:
self._set_A(bytes_A)
if not self.safety_failed:
if bytes_b:
bytes_to_bn( self.b, bytes_b )
else:
BN_rand(self.b, 256, 0, 0)
BN_set_flags(self.b, BN_FLG_CONSTTIME)
# B = kv + g^b
BN_mul(self.tmp1, k, self.v, self.ctx)
BN_mod_exp(self.tmp2, g, self.b, N, self.ctx)
BN_add(self.B, self.tmp1, self.tmp2)
BN_mod(self.B, self.B, N, self.ctx)
def __del__(self):
if not hasattr(self, 'A'):
return # __init__ threw exception. no clean up required
BN_free(self.A)
BN_free(self.B)
BN_free(self.S)
BN_free(self.u)
BN_free(self.b)
BN_free(self.s)
BN_free(self.v)
BN_free(self.N)
BN_free(self.g)
BN_free(self.k)
BN_free(self.tmp1)
BN_free(self.tmp2)
BN_CTX_free(self.ctx)
def authenticated(self):
return self._authenticated
def get_username(self):
return self.I
def get_ephemeral_secret(self):
return bn_to_bytes(self.b)
def get_session_key(self):
return self.K if self._authenticated else None
# returns (bytes_s, bytes_B) on success, (None,None) if SRP-6a safety check fails
def get_challenge(self):
if self.safety_failed:
return None, None
else:
return (bn_to_bytes(self.s), bn_to_bytes(self.B))
def verify_session(self, user_M, bytes_A=None):
if bytes_A:
self._set_A(bytes_A)
if not hasattr(self, 'A'):
raise ValueError("bytes_A must be provided through Verifier constructor or verify_session parameter.")
if not self.safety_failed:
self._derive_H_AMK()
if user_M == self.M:
self._authenticated = True
return self.H_AMK
def _set_A(self, bytes_A):
self.A = BN_new()
bytes_to_bn( self.A, bytes_A )
# SRP-6a safety check
BN_mod(self.tmp1, self.A, self.N, self.ctx)
if BN_is_zero(self.tmp1):
self.safety_failed = True
def _derive_H_AMK(self):
H_bn_bn(self.hash_class, self.u, self.A, self.B, width=BN_num_bytes(self.N))
# S = (A *(v^u)) ^ b
BN_mod_exp(self.tmp1, self.v, self.u, self.N, self.ctx)
BN_mul(self.tmp2, self.A, self.tmp1, self.ctx)
BN_mod_exp(self.S, self.tmp2, self.b, self.N, self.ctx)
self.K = self.hash_class( bn_to_bytes(self.S) ).digest()
self.M = calculate_M( self.hash_class, self.N, self.g, self.I, self.s, self.A, self.B, self.K )
self.H_AMK = calculate_H_AMK( self.hash_class, self.A, self.M, self.K )
class User:
def __init__(self, username, password, hash_alg=SHA1, ng_type=NG_2048, n_hex=None, g_hex=None, bytes_a=None, bytes_A=None, k_hex=None):
if ng_type == NG_CUSTOM and (n_hex is None or g_hex is None):
raise ValueError("Both n_hex and g_hex are required when ng_type = NG_CUSTOM")
if bytes_a and len(bytes_a) != 32:
raise ValueError("32 bytes required for bytes_a")
self.username = username
self.password = password
self.a = BN_new()
self.A = BN_new()
self.B = BN_new()
self.s = BN_new()
self.S = BN_new()
self.u = BN_new()
self.x = BN_new()
self.v = BN_new()
self.tmp1 = BN_new()
self.tmp2 = BN_new()
self.tmp3 = BN_new()
self.ctx = BN_CTX_new()
self.M = None
self.K = None
self.H_AMK = None
self._authenticated = False
hash_class = _hash_map[ hash_alg ]
N,g,k = get_ngk( hash_class, ng_type, n_hex, g_hex, self.ctx )
if k_hex is not None:
BN_hex2bn(k, k_hex)
self.hash_class = hash_class
self.N = N
self.g = g
self.k = k
if bytes_a:
bytes_to_bn( self.a, bytes_a )
else:
BN_rand(self.a, 256, 0, 0)
if bytes_A:
bytes_to_bn( self.A, bytes_A )
else:
BN_set_flags(self.a, BN_FLG_CONSTTIME)
BN_mod_exp(self.A, g, self.a, N, self.ctx)
def __del__(self):
if not hasattr(self, 'a'):
return # __init__ threw exception. no clean up required
BN_free(self.a)
BN_free(self.A)
BN_free(self.B)
BN_free(self.s)
BN_free(self.S)
BN_free(self.u)
BN_free(self.x)
BN_free(self.v)
BN_free(self.N)
BN_free(self.g)
BN_free(self.k)
BN_free(self.tmp1)
BN_free(self.tmp2)
BN_free(self.tmp3)
BN_CTX_free(self.ctx)
def authenticated(self):
return self._authenticated
def get_username(self):
return self.username
def get_ephemeral_secret(self):
return bn_to_bytes(self.a)
def get_session_key(self):
return self.K if self._authenticated else None
def start_authentication(self):
return (self.username, bn_to_bytes(self.A))
# Returns M or None if SRP-6a safety check is violated
def process_challenge(self, bytes_s, bytes_B):
hash_class = self.hash_class
N = self.N
g = self.g
k = self.k
bytes_to_bn( self.s, bytes_s )
bytes_to_bn( self.B, bytes_B )
# SRP-6a safety check
if BN_is_zero(self.B):
return None
H_bn_bn(hash_class, self.u, self.A, self.B, width=BN_num_bytes(N))
# SRP-6a safety check
if BN_is_zero(self.u):
return None
calculate_x( hash_class, self.x, self.s, self.username, self.password )
BN_mod_exp(self.v, g, self.x, N, self.ctx)
# S = (B - k*(g^x)) ^ (a + ux)
BN_mul(self.tmp1, self.u, self.x, self.ctx)
BN_add(self.tmp2, self.a, self.tmp1) # tmp2 = (a + ux)
BN_mod_exp(self.tmp1, g, self.x, N, self.ctx)
BN_mul(self.tmp3, k, self.tmp1, self.ctx) # tmp3 = k*(g^x)
BN_sub(self.tmp1, self.B, self.tmp3) # tmp1 = (B - K*(g^x))
BN_mod_exp(self.S, self.tmp1, self.tmp2, N, self.ctx)
self.K = hash_class( bn_to_bytes(self.S) ).digest()
self.M = calculate_M( hash_class, N, g, self.username, self.s, self.A, self.B, self.K )
self.H_AMK = calculate_H_AMK( hash_class, self.A, self.M, self.K )
return self.M
def verify_session(self, host_HAMK):
if self.H_AMK == host_HAMK:
self._authenticated = True
#---------------------------------------------------------
# Init
#
RAND_seed( os.urandom(32), 32 )
@@ -0,0 +1,441 @@
# N A large safe prime (N = 2q+1, where q is prime)
# All arithmetic is done modulo N.
# g A generator modulo N
# k Multiplier parameter (k = H(N, g) in SRP-6a, k = 3 for legacy SRP-6)
# s User's salt
# I Username
# p Cleartext Password
# H() One-way hash function
# ^ (Modular) Exponentiation
# u Random scrambling parameter
# a,b Secret ephemeral values
# A,B Public ephemeral values
# x Private key (derived from p and s)
# v Password verifier
import hashlib
import os
import binascii
import six
_rfc5054_compat = False
_no_username_in_x = False
def rfc5054_enable(enable=True):
global _rfc5054_compat
_rfc5054_compat = enable
def no_username_in_x(enable=True):
global _no_username_in_x
_no_username_in_x = enable
SHA1 = 0
SHA224 = 1
SHA256 = 2
SHA384 = 3
SHA512 = 4
NG_1024 = 0
NG_2048 = 1
NG_4096 = 2
NG_8192 = 3
NG_CUSTOM = 4
_hash_map = { SHA1 : hashlib.sha1,
SHA224 : hashlib.sha224,
SHA256 : hashlib.sha256,
SHA384 : hashlib.sha384,
SHA512 : hashlib.sha512 }
_ng_const = (
# 1024-bit
('''\
EEAF0AB9ADB38DD69C33F80AFA8FC5E86072618775FF3C0B9EA2314C9C256576D674DF7496\
EA81D3383B4813D692C6E0E0D5D8E250B98BE48E495C1D6089DAD15DC7D7B46154D6B6CE8E\
F4AD69B15D4982559B297BCF1885C529F566660E57EC68EDBC3C05726CC02FD4CBF4976EAA\
9AFD5138FE8376435B9FC61D2FC0EB06E3''',
"2"),
# 2048
('''\
AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4\
A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF60\
95179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF\
747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B907\
8717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB37861\
60279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DB\
FBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73''',
"2"),
# 4096
('''\
FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E08\
8A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B\
302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9\
A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE6\
49286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8\
FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D\
670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C\
180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718\
3995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D\
04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7D\
B3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D226\
1AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200C\
BBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFC\
E0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B26\
99C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB\
04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2\
233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127\
D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199\
FFFFFFFFFFFFFFFF''',
"5"),
# 8192
('''\
FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E08\
8A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B\
302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9\
A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE6\
49286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8\
FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D\
670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C\
180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718\
3995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D\
04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7D\
B3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D226\
1AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200C\
BBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFC\
E0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B26\
99C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB\
04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2\
233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127\
D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934028492\
36C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406\
AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918\
DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B33205151\
2BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03\
F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97F\
BEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AA\
CC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58B\
B7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632\
387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E\
6DBE115974A3926F12FEE5E438777CB6A932DF8CD8BEC4D073B931BA\
3BC832B68D9DD300741FA7BF8AFC47ED2576F6936BA424663AAB639C\
5AE4F5683423B4742BF1C978238F16CBE39D652DE3FDB8BEFC848AD9\
22222E04A4037C0713EB57A81A23F0C73473FC646CEA306B4BCBC886\
2F8385DDFA9D4B7FA2C087E879683303ED5BDD3A062B3CF5B3A278A6\
6D2A13F83F44F82DDF310EE074AB6A364597E899A0255DC164F31CC5\
0846851DF9AB48195DED7EA1B1D510BD7EE74D73FAF36BC31ECFA268\
359046F4EB879F924009438B481C6CD7889A002ED5EE382BC9190DA6\
FC026E479558E4475677E9AA9E3050E2765694DFC81F56E880B96E71\
60C980DD98EDD3DFFFFFFFFFFFFFFFFF''',
'0x13')
)
def get_ng( ng_type, n_hex, g_hex ):
if ng_type < NG_CUSTOM:
n_hex, g_hex = _ng_const[ ng_type ]
return int(n_hex,16), int(g_hex,16)
def bytes_to_long(s):
n = 0
for b in six.iterbytes(s):
n = (n << 8) | b
return n
def long_to_bytes(n):
l = list()
x = 0
off = 0
while x != n:
b = (n >> off) & 0xFF
l.append( chr(b) )
x = x | (b << off)
off += 8
l.reverse()
return six.b(''.join(l))
def get_random( nbytes ):
return bytes_to_long( os.urandom( nbytes ) )
def get_random_of_length( nbytes ):
offset = (nbytes*8) - 1
return get_random( nbytes ) | (1 << offset)
def old_H( hash_class, s1, s2 = '', s3=''):
if isinstance(s1, six.integer_types):
s1 = long_to_bytes(s1)
if s2 and isinstance(s2, six.integer_types):
s2 = long_to_bytes(s2)
if s3 and isinstance(s3, six.integer_types):
s3 = long_to_bytes(s3)
s = s1 + s2 + s3
return long(hash_class(s).hexdigest(), 16)
def H( hash_class, *args, **kwargs ):
width = kwargs.get('width', None)
h = hash_class()
for s in args:
if s is not None:
data = long_to_bytes(s) if isinstance(s, six.integer_types) else s
if width is not None and _rfc5054_compat:
h.update( bytes(width - len(data)))
h.update( data )
return h.digest()
#N = 0xAC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF6095179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B9078717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB3786160279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DBFBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73;
#g = 2;
#k = H(N,g)
def HNxorg( hash_class, N, g ):
bin_N = long_to_bytes(N)
bin_g = long_to_bytes(g)
padding = len(bin_N) - len(bin_g) if _rfc5054_compat else 0
hN = hash_class( bin_N ).digest()
hg = hash_class( b''.join( [b'\0'*padding, bin_g] ) ).digest()
return six.b( ''.join( chr( six.indexbytes(hN, i) ^ six.indexbytes(hg, i) ) for i in range(0,len(hN)) ) )
def gen_x( hash_class, salt, username, password ):
username = username.encode() if hasattr(username, 'encode') else username
password = password.encode() if hasattr(password, 'encode') else password
if _no_username_in_x:
username = six.b('')
return bytes_to_long( H(hash_class, salt, H( hash_class, username + six.b(':') + password ) ))
def create_salted_verification_key( username, password, hash_alg=SHA1, ng_type=NG_2048, n_hex=None, g_hex=None, salt_len=4, k_hex=None ):
if ng_type == NG_CUSTOM and (n_hex is None or g_hex is None):
raise ValueError("Both n_hex and g_hex are required when ng_type = NG_CUSTOM")
hash_class = _hash_map[ hash_alg ]
N,g = get_ng( ng_type, n_hex, g_hex )
_s = long_to_bytes( get_random( salt_len ) )
_v = long_to_bytes( pow(g, gen_x( hash_class, _s, username, password ), N) )
return _s, _v
def calculate_M( hash_class, N, g, I, s, A, B, K ):
I = I.encode() if hasattr(I, 'encode') else I
h = hash_class()
h.update( HNxorg( hash_class, N, g ) )
h.update( hash_class(I).digest() )
if isinstance(s, six.integer_types):
s = long_to_bytes(s)
h.update( s )
h.update( long_to_bytes(A) )
h.update( long_to_bytes(B) )
h.update( K )
return h.digest()
def calculate_H_AMK( hash_class, A, M, K ):
h = hash_class()
h.update( long_to_bytes(A) )
h.update( M )
h.update( K )
return h.digest()
class Verifier:
def __init__(self, username, bytes_s, bytes_v, bytes_A=None, hash_alg=SHA1, ng_type=NG_2048, n_hex=None, g_hex=None, bytes_b=None, k_hex=None):
if ng_type == NG_CUSTOM and (n_hex is None or g_hex is None):
raise ValueError("Both n_hex and g_hex are required when ng_type = NG_CUSTOM")
if bytes_b and len(bytes_b) != 256:
raise ValueError("256 bytes required for bytes_b")
self.s = bytes_s
self.v = bytes_to_long(bytes_v)
self.I = username
self.K = None
self._authenticated = False
self.safety_failed = False
N,g = get_ng( ng_type, n_hex, g_hex )
hash_class = _hash_map[ hash_alg ]
if k_hex is None:
k = bytes_to_long( H( hash_class, N, g, width=len(long_to_bytes(N)) ))
else:
k = int(k_hex, 16)
self.hash_class = hash_class
self.N = N
self.g = g
self.k = k
if bytes_A:
self._set_A(bytes_A)
if not self.safety_failed:
if bytes_b:
self.b = bytes_to_long(bytes_b)
else:
self.b = get_random_of_length( 256 )
self.B = (k*self.v + pow(g, self.b, N)) % N
def authenticated(self):
return self._authenticated
def get_username(self):
return self.I
def get_ephemeral_secret(self):
return long_to_bytes(self.b)
def get_session_key(self):
return self.K if self._authenticated else None
# returns (bytes_s, bytes_B) on success, (None,None) if SRP-6a safety check fails
def get_challenge(self):
if self.safety_failed:
return None,None
else:
return (self.s, long_to_bytes(self.B))
# returns H_AMK on success, None on failure
def verify_session(self, user_M, bytes_A=None):
if bytes_A:
self._set_A(bytes_A)
if not hasattr(self, 'A'):
raise ValueError("bytes_A must be provided through Verifier constructor or verify_session parameter.")
if not self.safety_failed:
self._derive_H_AMK()
if user_M == self.M:
self._authenticated = True
return self.H_AMK
def _set_A(self, bytes_A):
self.A = bytes_to_long(bytes_A)
# SRP-6a safety check
self.safety_failed = self.A % self.N == 0
def _derive_H_AMK(self):
self.u = bytes_to_long(H(self.hash_class, self.A, self.B, width=len(long_to_bytes(self.N))))
self.S = pow(self.A*pow(self.v, self.u, self.N ), self.b, self.N)
self.K = self.hash_class( long_to_bytes(self.S) ).digest()
self.M = calculate_M( self.hash_class, self.N, self.g, self.I, self.s, self.A, self.B, self.K )
self.H_AMK = calculate_H_AMK( self.hash_class, self.A, self.M, self.K )
class User:
def __init__(self, username, password, hash_alg=SHA1, ng_type=NG_2048, n_hex=None, g_hex=None, bytes_a=None, bytes_A=None, k_hex=None):
if ng_type == NG_CUSTOM and (n_hex is None or g_hex is None):
raise ValueError("Both n_hex and g_hex are required when ng_type = NG_CUSTOM")
if bytes_a and len(bytes_a) != 256:
raise ValueError("256 bytes required for bytes_a")
N,g = get_ng( ng_type, n_hex, g_hex )
hash_class = _hash_map[ hash_alg ]
if k_hex is None:
k = bytes_to_long(H( hash_class, N, g, width=len(long_to_bytes(N)) ))
else:
k = int(k_hex, 16)
self.I = username
self.p = password
if bytes_a:
self.a = bytes_to_long(bytes_a)
else:
self.a = get_random_of_length( 256 )
if bytes_A:
self.A = bytes_to_long(bytes_A)
else:
self.A = pow(g, self.a, N)
self.v = None
self.M = None
self.K = None
self.H_AMK = None
self._authenticated = False
self.hash_class = hash_class
self.N = N
self.g = g
self.k = k
def authenticated(self):
return self._authenticated
def get_username(self):
return self.I
def get_ephemeral_secret(self):
return long_to_bytes(self.a)
def get_session_key(self):
return self.K if self._authenticated else None
def start_authentication(self):
return (self.I, long_to_bytes(self.A))
# Returns M or None if SRP-6a safety check is violated
def process_challenge(self, bytes_s, bytes_B):
self.s = bytes_s
self.B = bytes_to_long( bytes_B )
N = self.N
g = self.g
k = self.k
hash_class = self.hash_class
# SRP-6a safety check
if (self.B % N) == 0:
return None
self.u = bytes_to_long(H( hash_class, self.A, self.B, width=len(long_to_bytes(N)) ))
# SRP-6a safety check
if self.u == 0:
return None
self.x = gen_x( hash_class, self.s, self.I, self.p )
self.v = pow(g, self.x, N)
self.S = pow((self.B - k*self.v), (self.a + self.u*self.x), N)
self.K = hash_class( long_to_bytes(self.S) ).digest()
self.M = calculate_M( hash_class, N, g, self.I, self.s, self.A, self.B, self.K )
self.H_AMK = calculate_H_AMK(hash_class, self.A, self.M, self.K)
return self.M
def verify_session(self, host_HAMK):
if self.H_AMK == host_HAMK:
self._authenticated = True
@@ -0,0 +1,216 @@
# -*- coding: utf-8 -*-
#
# Secure Remote Password documentation build configuration file, created by
# sphinx-quickstart on Fri Mar 25 10:20:52 2011.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = []
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'Secure Remote Password'
copyright = u'2011, Tom Cocagne'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '1.0'
# The full version, including alpha/beta/rc tags.
release = '1.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'SecureRemotePassworddoc'
# -- Options for LaTeX output --------------------------------------------------
# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt').
#latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'SecureRemotePassword.tex', u'Secure Remote Password Documentation',
u'Tom Cocagne', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Additional stuff for the LaTeX preamble.
#latex_preamble = ''
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'secureremotepassword', u'Secure Remote Password Documentation',
[u'Tom Cocagne'], 1)
]
@@ -0,0 +1,22 @@
.. Secure Remote Password documentation master file, created by
sphinx-quickstart on Fri Mar 25 10:20:52 2011.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to Secure Remote Password's documentation!
==================================================
Contents:
.. toctree::
:maxdepth: 2
srp.rst
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
@@ -0,0 +1,399 @@
:mod:`srp` --- Secure Remote Password
=====================================
.. module:: srp
:synopsis: Secure Remote Password
.. moduleauthor:: Tom Cocagne <tom.cocagne@gmail.com>
.. sectionauthor:: Tom Cocagne <tom.cocagne@gmail.com>
The Secure Remote Password protocol (SRP) is a cryptographically
strong authentication protocol for password-based, mutual
authentication over an insecure network connection. Successful SRP
authentication requires both sides of the connection to have knowledge
of the user's password. In addition to password verification, the SRP
protocol also performs a secure key exchange during the authentication
process. This key may be used to protect network traffic via symmetric
key encryption.
SRP offers security and deployment advantages over other
challenge-response protocols, such as Kerberos and SSL, in that it
does not require trusted key servers or certificate infrastructures.
Instead, small verification keys derived from each user's password are
stored and used by each SRP server application. SRP provides a
near-ideal solution for many applications requiring simple and secure
password authentication that does not rely on an external
infrastructure.
Another favorable aspect of the SRP protocol is that compromized
verification keys are of little value to an attacker. Possesion of a
verification key does not allow a user to be impersonated
and it cannot be used to obtain the users password except by way of a
computationally infeasible dictionary attack. A compromized key would,
however, allow an attacker to impersonate the server side of an SRP
authenticated connection. Consequently, care should be taken to
prevent unauthorized access to verification keys for applications in
which the client side relies on the server being genuine.
Usage
-----
SRP usage begins with *create_salted_verification_key()*. This function
creates a salted verification key from the user's password. The resulting salt
and key are stored by the server application and will be used during the
authentication process.
The authentication process occurs as an exchange of messages between the clent
and the server. The :ref:`example` below provides a simple demonstration of the
protocol. A comprehensive description of the SRP protocol is contained in the
:ref:`protocol-description` section.
The *User* & *Verifier* constructors, as well as the
*create_salted_verification_key()* function, accept optional arguments
to specify which hashing algorithm and prime number arguments should
be used during the authentication process. These options may be used
to tune the security/performance tradeoff for an application.
Generally speaking, specifying arguments with a higher number of bits
will result in a greater level of security. However, it will come at
the cost of increased computation time. The default values of SHA1
hashes and 2048 bit prime numbers strike a good balance between
performance and security. These values should be sufficient for most
applications. Regardless of which values are used, the parameters
passed to the *User* and *Verifier* constructors must exactly match
those passed to *create_salted_verification_key()*
.. _constants:
Constants
---------
.. table:: Hashing Algorithm Constants
============== ==============
Hash Algorithm Number of Bits
============== ==============
SHA1 160
SHA224 224
SHA256 256
SHA384 384
SHA512 512
============== ==============
.. note::
Larger hashing algorithms will result in larger session keys.
.. table:: Prime Number Constants
================= ==============
Prime Number Size Number of Bits
================= ==============
NG_1024 1024
NG_2048 2048
NG_4096 4096
NG_8192 8192
NG_CUSTOM User Supplied
================= ==============
.. note::
If NG_CUSTOM is used, the 'n_hex' and 'g_hex' parameters are required.
These parameters must be ASCII text containing hexidecimal notation of the
prime number 'n_hex' and the corresponding generator number 'g_hex'. Appendix
A of RFC 5054 contains several large prime number, generator pairs that may
be used with NG_CUSTOM.
Functions
---------
.. function:: create_salted_verification_key ( username, password[, hash_alg=SHA1, ng_type=NG_2048, n_hex=None, g_hex=None] )
*username* Name of the user
*password* Plaintext user password
*hash_alg*, *ng_type*, *n_hex*, *g_hex* Refer to the :ref:`constants` section.
Generate a salted verification key for the given username and password and return the tuple:
(salt_bytes, verification_key_bytes)
.. function:: rfc5054_enable( enable=True )
*enable* True if compatibility with RFC5054 is required, False otherwise.
For backward compatibility, pysrp by default does not conform to RFC5054. If you need compatibility
with RFC5054, just call this function before using pysrp.
:class:`Verifier` Objects
-------------------------
A :class:`Verifier` object is used to verify the identity of a remote
user.
.. note::
The standard SRP 6 protocol allows only one password attempt per
connection.
.. class:: Verifier( username, bytes_s, bytes_v[, bytes_A=None, hash_alg=SHA1, ng_type=NG_2048, n_hex=None, g_hex=None] )
*username* Name of the remote user being authenticated.
*bytes_s* Salt generated by :func:`create_salted_verification_key`.
*bytes_v* Verification Key generated by :func:`create_salted_verification_key`.
*bytes_A* Challenge from the remote user. Generated by
:meth:`User.start_authentication`. Useful when user generates A
before verifier generates B. If following RFC5054 section 2.2 where
B is generated prior to A, then A can instead later be provided to
Verifier via :func:`Verifier.verify_session`.
*hash_alg*, *ng_type*, *n_hex*, *g_hex* Refer to the :ref:`constants` section.
.. method:: Verifier.authenticated()
Return True if the authentication succeeded. False
otherwise.
.. method:: Verifier.get_username()
Return the name of the user this :class:`Verifier` object is for.
.. method:: Verifier.get_session_key()
Return the session key for an authenticated user or None if the
authentication failed or has not yet completed.
.. method:: Verifier.get_challenge()
Return (bytes_s, bytes_B) on success or (None, None) if
authentication has failed.
.. method:: Verifier.verify_session( user_M[, user_A=None] )
Complete the :class:`Verifier` side of the authentication process.
If A was generated after B as in RFC5054 Section 2.2, then bytes_A
can be provided here. If the authentication succeeded, bytes_H_AMK
should be returned to the remote user. On failure, this method
returns None.
:class:`User` Objects
-------------------------
A :class:`User` object is used to prove a user's identity to a remote :class:`Verifier` and
verifiy that the remote :class:`Verifier` knows the verification key associated with
the user's password.
.. class:: User( username, password[, hash_alg=SHA1, ng_type=NG_2048, n_hex=None, g_hex=None] )
*username* Name of the user being authenticated.
*password* Password for the user.
*hash_alg*, *ng_type*, *n_hex*, *g_hex* Refer to the :ref:`constants` section.
.. method:: User.authenticated()
Return True if authentication succeeded. False
otherwise.
.. method:: User.get_username()
Return the username passed to the constructor.
.. method:: User.get_session_key()
Return the session key if authentication succeeded or None if the
authentication failed or has not yet completed.
.. method:: User.start_authentication()
Return (username, bytes_A). These should be passed to the
constructor of the remote :class:`Verifer`
.. method:: User.process_challenge( bytes_s, bytes_B )
Processe the challenge returned
by :meth:`Verifier.get_challenge` on success this method
returns bytes_M that should be sent
to :meth:`Verifier.verify_session` if authentication failed,
it returns None.
.. method:: User.verify_session( bytes_H_AMK )
Complete the :class:`User` side of the authentication process. By
verifying the *bytes_H_AMK* value returned by
:meth:`Verifier.verify_session`. If the authentication succeded
:meth:`authenticated` will return True
.. _example:
Example
-------
Simple Usage Example::
import srp
# The salt and verifier returned from srp.create_salted_verification_key() should be
# stored on the server.
salt, vkey = srp.create_salted_verification_key( 'testuser', 'testpassword' )
class AuthenticationFailed (Exception):
pass
# ~~~ Begin Authentication ~~~
usr = srp.User( 'testuser', 'testpassword' )
uname, A = usr.start_authentication()
# The authentication process can fail at each step from this
# point on. To comply with the SRP protocol, the authentication
# process should be aborted on the first failure.
# Client => Server: username, A
svr = srp.Verifier( uname, salt, vkey, A )
s,B = svr.get_challenge()
if s is None or B is None:
raise AuthenticationFailed()
# Server => Client: s, B
M = usr.process_challenge( s, B )
if M is None:
raise AuthenticationFailed()
# Client => Server: M
HAMK = svr.verify_session( M )
if HAMK is None:
raise AuthenticationFailed()
# Server => Client: HAMK
usr.verify_session( HAMK )
# At this point the authentication process is complete.
assert usr.authenticated()
assert svr.authenticated()
Implementation Notes
--------------------
This implementation of SRP consists of both a pure-python module and a C-based
implementation that is approximately 10x faster. By default, the
C-implementation will be used if it is available. An additional benefit of the C
implementation is that it can take advantage of of multiple CPUs. For cases in
which the number of connections per second is an issue, using a small pool of
threads to perform the authentication steps on multi-core systems will yield a
substantial performance increase.
.. _protocol-description:
SRP 6a Protocol Description
---------------------------
The original SRP protocol, known as SRP-3, is defined in
RFC 2945. This implementation, however, uses SRP-6a which is a slight
improvement over SRP-3. The authoritative definition for the SRP-6a
protocol is available at http://srp.stanford.edu. An additional
resource is RFC 5054 which covers the integration of SRP into
TLS. This RFC is the source of hashing strategy and the predefined N
and g constants used in this implementation.
The following is a complete description of the SRP-6a protocol as implemented by
this library. Note that the ^ symbol indicates exponentiaion and the | symbol
indicates concatenation.
.. rubric:: Primary Variables used in SRP 6a
========= =================================================================
Variables Description
========= =================================================================
N A large, safe prime (N = 2q+1, where q is a Sophie Germain prime)
All arithmetic is performed in the field of integers modulo N
g A generator modulo N
s Small salt for the verification key
I Username
p Cleartext password
H() One-way hash function
a,b Secret, random values
K Session key
========= =================================================================
.. rubric:: Derived Values used in SRP 6a
====================================== ====================================
Derived Values Description
====================================== ====================================
k = H(N,g) Multiplier Parameter
A = g^a Public ephemeral value
B = kv + g^b Public ephemeral value
x = H(s, H( I | ':' | p )) Private key (as defined by RFC 5054)
v = g^x Password verifier
u = H(A,B) Random scrambling parameter
M = H(H(N) xor H(g), H(I), s, A, B, K) Session key verifier
====================================== ====================================
.. rubric:: Protocol Description
The server stores the password verifier *v*. Authentication begins with a
message from the client::
client -> server: I, [A = g^a]
where public ephemeral key *A* may be provided at this point or later
as part of verification. The server replies with the verifier salt and
challenge::
server -> client: s, B = kv + g^b, [N, g]
where *N* and *g* may be provided by the server or hardcoded in the
user client. At this point, both the client and server calculate the
shared session key::
client & server: u = H(A,B)
::
server: K = H( (Av^u) ^ b )
::
client: x = H( s, H( I + ':' + p ) )
client: K = H( (B - kg^x) ^ (a + ux) )
Now both parties have a shared, strong session key *K*. To complete
authentication they need to prove to each other that their keys match::
client -> server: M = H(H(N) xor H(g), H(I), s, A, B, K), [A = g^a]
server -> client: H(A, M, K)
Note that *A* is only provided here if it was not provided during the initial authentication request.
SRP 6a requires the two parties to use the following safeguards (all of which this library automatically checks):
1. The client will abort if it recieves B == 0 (mod N) or u == 0
2. The server will abort if it detects A == 0 (mod N)
3. The client must show its proof of K first. If the server detects that this
proof is incorrect it must abort without showing its own proof of K
Additionally, if the server provides N and g to the user, the user
should verify the safe prime bit length is as expected, that its
highest bit is 1 to ensure it is large, and that it is indeed a safe
prime with the expected generator. These N and g checks are not
currently provided by this library.
@@ -0,0 +1,316 @@
#!/usr/bin/env python
import unittest
import os.path
import os
import sys
import time
import _thread
this_dir = os.path.dirname( os.path.abspath(__file__) )
build_dir = os.path.join( os.path.dirname(this_dir), 'build' )
if not os.path.exists( build_dir ):
print('Please run "python setup.py build" prior to running tests')
sys.exit(1)
plat_dirs = [ d for d in os.listdir('build') if d.startswith('lib') ]
if not len(plat_dirs) == 1:
print('Unexpected build result... aborting')
plat_dir = os.path.join( build_dir, plat_dirs[0] )
sys.path.insert(0, os.path.join('build', plat_dir) )
import srp
import srp._pysrp as _pysrp
import srp._ctsrp as _ctsrp
test_g_hex = b"2"
test_n_hex = ('''\
AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4\
A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF60\
95179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF\
747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B907\
8717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB37861\
60279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DB\
FBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73''').encode('ascii')
class SRPTests( unittest.TestCase ):
def doit(self, u_mod, v_mod, g_mod, hash_alg=srp.SHA1, ng_type=srp.NG_2048, n_hex='', g_hex='', k_hex=None):
User = u_mod.User
Verifier = v_mod.Verifier
create_salted_verification_key = g_mod.create_salted_verification_key
username = b'testuser'
password = b'testpassword'
_s, _v = create_salted_verification_key( username, password, hash_alg, ng_type, n_hex, g_hex )
usr = User( username, password, hash_alg, ng_type, n_hex, g_hex, None, None, k_hex )
bytes_a = usr.get_ephemeral_secret()
uname, A = usr.start_authentication()
# Make sure a recreated User does all the same appropriate things
usr2 = User( username, password, hash_alg, ng_type, n_hex, g_hex, bytes_a, None, k_hex )
self.assertEqual(bytes_a, usr2.get_ephemeral_secret())
uname2, A2 = usr2.start_authentication()
self.assertEqual(uname, uname2)
self.assertEqual(A, A2)
# username, A => server
svr = Verifier( uname, _s, _v, A, hash_alg, ng_type, n_hex, g_hex, None, k_hex )
bytes_b = svr.get_ephemeral_secret()
s,B = svr.get_challenge()
# s,B => client
M = usr.process_challenge( s, B )
M2 = usr2.process_challenge( s, B )
self.assertEqual(M, M2)
# M => server
HAMK = svr.verify_session( M )
# Make sure that a recreated Verifier will authenticate appropriately
svr2 = Verifier( uname, _s, _v, A, hash_alg, ng_type, n_hex, g_hex, bytes_b, k_hex )
self.assertEqual(bytes_b, svr2.get_ephemeral_secret())
HAMK2 = svr2.verify_session( M )
self.assertEqual(HAMK, HAMK2)
# Make sure Verifier generating B before receiving A will not change authentication.
svr3 = Verifier ( uname, _s, _v, None, hash_alg, ng_type, n_hex, g_hex, bytes_b, k_hex )
self.assertEqual(bytes_b, svr3.get_ephemeral_secret())
HAMK3 = svr3.verify_session( M, A )
self.assertEqual(HAMK, HAMK3)
# HAMK => client
usr.verify_session( HAMK )
usr2.verify_session( HAMK )
self.assertTrue( svr.authenticated() )
self.assertTrue( svr2.authenticated() )
self.assertTrue( svr3.authenticated() )
self.assertTrue( usr.authenticated() )
self.assertTrue( svr2.authenticated() )
def test_pure_python_defaults(self):
self.doit( _pysrp, _pysrp, _pysrp )
def test_ctypes_defaults(self):
self.doit( _ctsrp, _ctsrp, _ctsrp )
def test_mix1(self):
self.doit( _pysrp, _ctsrp, _ctsrp )
def test_mix2(self):
self.doit( _pysrp, _pysrp, _ctsrp )
def test_mix3(self):
self.doit( _ctsrp, _pysrp, _pysrp )
def test_mix4(self):
self.doit( _ctsrp, _ctsrp, _pysrp )
def test_hash_SHA512(self):
self.doit( _ctsrp, _ctsrp, _ctsrp, hash_alg=srp.SHA512 )
def test_NG_8192(self):
self.doit( _ctsrp, _ctsrp, _ctsrp, ng_type=srp.NG_8192 )
def test_NG_CUSTOM(self):
self.doit( _ctsrp, _ctsrp, _ctsrp, ng_type=srp.NG_CUSTOM, n_hex=test_n_hex, g_hex=test_g_hex )
def test_all1(self):
self.doit( _ctsrp, _pysrp, _ctsrp, hash_alg=srp.SHA256, ng_type=srp.NG_CUSTOM, n_hex=test_n_hex, g_hex=test_g_hex )
def test_all2(self):
self.doit( _ctsrp, _pysrp, _ctsrp, hash_alg=srp.SHA224, ng_type=srp.NG_4096 )
def test_random_of_length(self):
"""
Verify that the Python implementation guarantees byte length by
setting most significant bit to 1
"""
for x in range(10):
val = _pysrp.get_random_of_length(32)
self.assertTrue(val >> 255 == 1)
def test_ephemeral_length(self):
"""
Verify that all implementations require 32 bytes for ephemeral values
"""
random31 = _pysrp.long_to_bytes(_pysrp.get_random_of_length(31))
random33 = _pysrp.long_to_bytes(_pysrp.get_random_of_length(33))
def verf_len(mod, val):
with self.assertRaises(ValueError) as ctx:
mod.User('uname', 'pwd', bytes_a=val)
self.assertIn('bytes_a', str(ctx.exception) )
with self.assertRaises(ValueError) as ctx:
mod.Verifier('uname', random31, random31, random31, bytes_b=val)
self.assertIn('bytes_b', str(ctx.exception) )
for mod in [_ctsrp, _pysrp]:
for val in [random31, random33]:
verf_len(mod, val)
def test_authenticated_on_init(self):
usr = _pysrp.User('test', 'test')
self.assertTrue(not usr.authenticated())
usr = _ctsrp.User('test', 'test')
self.assertTrue(not usr.authenticated())
def test_custom_k(self):
self.doit( _ctsrp, _pysrp, _ctsrp, k_hex=b'5')
def test_verifier_requires_A(self):
randbytes = _pysrp.long_to_bytes(_pysrp.get_random_of_length(32))
for mod in [_ctsrp, _pysrp]:
svr = mod.Verifier('uname', randbytes, randbytes)
with self.assertRaises(ValueError) as ctx:
svr.verify_session(randbytes)
self.assertIn('bytes_A', str(ctx.exception))
#-----------------------------------------------------------------------------------
# Performance Testing
#
hash_map = { 0 : 'SHA1 ', 1 : 'SHA224', 2 : 'SHA256', 3 : 'SHA384', 4 : 'SHA512' }
prime_map = { 0 : 1024, 1 : 2048, 2 : 4096, 3 : 8192 }
username = b'testuser'
password = b'testpassword'
NLEFT = 0
def do_auth( mod, hash_alg, ng_type, _s, _v ):
usr = mod.User( username, password, hash_alg, ng_type)
uname, A = usr.start_authentication()
# username, A => server
svr = mod.Verifier( uname, _s, _v, A, hash_alg, ng_type)
s,B = svr.get_challenge()
# s,B => client
M = usr.process_challenge( s, B )
# M => server
HAMK = svr.verify_session( M )
# HAMK => client
usr.verify_session( HAMK )
if not svr.authenticated() or not usr.authenticated():
raise Exception('Authentication failed!')
def performance_test( mod, hash_alg, ng_type, niter=10, nthreads=1 ):
global NLEFT
_s, _v = srp.create_salted_verification_key( username, password, hash_alg, ng_type )
NLEFT = niter
def test_thread():
global NLEFT
while NLEFT > 0:
do_auth( mod, hash_alg, ng_type, _s, _v )
NLEFT -= 1
start = time.time()
while nthreads > 1:
_thread.start_new_thread( test_thread, () )
nthreads -= 1
test_thread()
duration = time.time() - start
return duration
def get_param_str( mod, hash_alg, ng_type ):
m = { 'srp._pysrp' : 'Python',
'srp._ctsrp' : 'ctypes' }
cfg = '%s, %s, %d:' % (m[mod.__name__], hash_map[hash_alg], prime_map[ng_type])
return cfg
def param_test( mod, hash_alg, ng_type, niter=10 ):
duration = performance_test( mod, hash_alg, ng_type, niter )
cfg = get_param_str( mod, hash_alg, ng_type )
print(' ', cfg.ljust(20), '%.6f' % (duration/niter))
return duration/niter
def print_default_timings():
print('*'*60)
print('Default Parameter Timings:')
py_time = param_test( _pysrp, srp.SHA1, srp.NG_2048 )
ct_time = param_test( _ctsrp, srp.SHA1, srp.NG_2048 )
print('')
print('Performance increases: ')
print(' ctypes-module : ', py_time/ct_time)
def print_performance_table():
ng_types = [ srp.NG_1024, srp.NG_2048, srp.NG_4096, srp.NG_8192 ]
hash_types = [ srp.SHA1, srp.SHA224, srp.SHA256, srp.SHA384, srp.SHA512 ]
print('*'*60)
print('Hash Algorithm vs Prime Number performance table')
print('')
print(' |', end=' ')
for ng in ng_types:
print(('NG_%d' % prime_map[ng]).rjust(12), end=' ')
print('')
print('-'*60)
for hash_alg in hash_types:
print('%s |' % hash_map[hash_alg], end=' ')
for ng in ng_types:
print('{0:>12f}'.format(performance_test(_ctsrp, hash_alg, ng) / 10), end=' ')
print('')
def print_thread_performance():
print('*'*60)
print('Thread Performance Test:')
niter = 100
for nthreads in range(1,11):
print(' Thread Count {0:>2}: {1:8f}'.format(nthreads, performance_test(_ctsrp, srp.SHA1, srp.NG_2048, niter, nthreads)/niter))
print('*'*60)
print('*')
print('* Testing Implementation')
print('*')
suite = unittest.TestLoader().loadTestsFromTestCase(SRPTests)
unittest.TextTestRunner(verbosity=1).run(suite)
print('*'*60)
print('*')
print('* Performance Testing')
print('*')
print_thread_performance()
print_performance_table()
print_default_timings()
#---------------------------------------------------------------
# Pause briefly to ensure no background threads are still executing
time.sleep(0.1)