srp.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. """Secure Remote Passwords. This is slightly different from the standard
  2. implementation (with regard to the definition of 'u', the authentication
  3. hash, and the fact that the database is a pickle). Also the default random
  4. number generator is not cryptographically strong. It may be good enough to
  5. password protect your MUD, if not your bank account. Note that the passwd
  6. database should not be world readable, or it will be vulnerable to a
  7. dictionary attack (like the standard Unix password file). See the SRP
  8. distribution at http://srp.stanford.edu/srp/ for more information."""
  9. import sha
  10. from hmac import hmac
  11. import random
  12. import getpass
  13. import pickle
  14. import string
  15. import base64
  16. # Some constants defining the sizes of various entities.
  17. saltlen = 16 # bytes
  18. tlen = 128 # bits
  19. ablen = 128 # bits
  20. # The prime field to work in, and the base to use. Note that this must be
  21. # common to both client and host. (Alternatively, the host can send these
  22. # values to the client, who should then verify that they are safe.)
  23. # The first number is a prime p of the form 2q + 1, where q is also prime.
  24. # The second number is a generator in the field GF(p).
  25. pflist = [(137656596376486790043182744734961384933899167257744121335064027192370741112305920493080254690601316526576747330553110881621319493425219214435734356437905637147670206858858966652975541347966997276817657605917471296442404150473520316654025988200256062845025470327802138620845134916799507318209468806715548156999L,
  26. 8623462398472349872L)]
  27. # New exceptions we raise.
  28. class NoSuchUser(Exception): pass
  29. class ImproperKeyValue(Exception): pass
  30. class AuthFailure(Exception): pass
  31. # Some utility functions:
  32. def random_long(bits):
  33. """Generate a random long integer with the given number of bits."""
  34. r = 0L
  35. chunk = 24
  36. bchunk = (1 << chunk) - 1
  37. while bits > 0:
  38. if bits < chunk:
  39. bchunk = (1 << bits) - 1
  40. i = random.randint(0, bchunk)
  41. r = (r << chunk) + i
  42. bits = bits - chunk
  43. return r
  44. def random_string(bytes):
  45. """Generate a random string with the given number of bytes."""
  46. r = ''
  47. for i in range(0, bytes):
  48. r = r + chr(random.randint(0, 255))
  49. return r
  50. def string_to_long(s):
  51. """Convert a string of bytes into a long integer."""
  52. r = 0L
  53. for c in s:
  54. r = (r << 8) + ord(c)
  55. return r
  56. def long_to_string(i):
  57. """Convert a long integer into a string of bytes."""
  58. s = ''
  59. while i > 0:
  60. s = chr(i & 255) + s
  61. i = i >> 8
  62. return s
  63. def hash(s):
  64. """Hash a value with some hashing algorithm."""
  65. if type(s) != type(''):
  66. s = long_to_string(s)
  67. return sha.new(s).digest()
  68. def private_key(u, s, p):
  69. """Given the username, salt, and cleartext password, return the private
  70. key, which is the long integer form of the hashed arguments."""
  71. h = hash(s + hash(u + p))
  72. x = string_to_long(h)
  73. return x
  74. # This creates a new entry for the host password database. In other words,
  75. # this is called when the user changes his password.
  76. # Note that when this is done over the network, the channel should be
  77. # encrypted. The password should obviously never be sent in the clear, and
  78. # neither should the salt, verifier pair, as they are vulnerable to a
  79. # dictionary attack. For the same reason, the passwd database should not be
  80. # world readable.
  81. def create_new_verifier(u, p, pf):
  82. """Given a username, cleartext password, and a prime field, pick a
  83. random salt and calculate the verifier. The salt, verifier tuple is
  84. returned."""
  85. s = random_string(saltlen)
  86. n, g = pf
  87. v = pow(g, private_key(u, s, p), n)
  88. return (s, v)
  89. def new_passwd(user, password=None):
  90. if password is None:
  91. password = getpass.getpass('Enter new password for %s: ' % user)
  92. pfid = 0
  93. pf = pflist[pfid]
  94. salt, verifier = create_new_verifier(user, password, pf)
  95. passwd[user] = (salt, verifier, pfid)
  96. # This is the authentication protocol. There are two parts, the client and
  97. # the host. These functions are called from the client side.
  98. def client_begin(user):
  99. # Here we could optionally query the host for the pfid and salt, or
  100. # indeed the pf itself plus salt. We'd have to verify that n and g
  101. # are valid in the latter case, and we need a local copy anyway in the
  102. # former.
  103. pfid = 0
  104. n, g = pflist[pfid]
  105. # Pick a random number and send it to the host, who responds with
  106. # the user's salt and more random numbers. Note that in the standard
  107. # SRP implementation, u is derived from B.
  108. a = random_long(ablen)
  109. A = pow(g, a, n)
  110. return (A, a, g, n)
  111. def client_key(user, passphrase, s, B, u, keys):
  112. A, a, g, n = keys
  113. # We don't trust the host. Perhaps the host is being spoofed.
  114. if B <= 0 or n <= B:
  115. raise ImproperKeyValue
  116. # Calculate the shared, secret session key.
  117. x = private_key(user, s, passphrase)
  118. v = pow(g, x, n)
  119. t = B
  120. if t < v:
  121. t = t + n
  122. S = pow(t - v, a + u * x, n)
  123. K = hash(S)
  124. # Compute the authentication proof.
  125. # This verifies that we do indeed know the same session key,
  126. # implying that we knew the correct password (even though the host
  127. # doesn't know the password!)
  128. m = client_authenticator(K, n, g, user, s, A, B, u)
  129. return (K, m)
  130. # The next function is called from the host side.
  131. def lookup(user, A, passwdfile):
  132. """Look the user up in the passwd database, calculate our version of
  133. the session key, and return it along with a keyed hash of the values
  134. used in the calculation as proof. The client must match this proof."""
  135. read_passwd(passwdfile)
  136. if not passwd.has_key(user):
  137. raise NoSuchUser, user
  138. s, v, pfid = passwd[user]
  139. pf = pflist[pfid]
  140. n, g = pf
  141. # We don't trust the client, who might be trying to send bogus data in
  142. # order to break the protocol.
  143. if A <= 0 or n <= A:
  144. raise ImproperKeyValue
  145. # Pick our random public keys.
  146. while 1:
  147. b = random_long(ablen)
  148. B = (v + pow(g, b, n)) % n
  149. if B != 0: break
  150. u = pow(g, random_long(tlen), n)
  151. # Calculate the (private, shared secret) session key.
  152. t = (A * pow(v, u, n)) % n
  153. if t <= 1 or t + 1 == n:
  154. raise ImproperKeyValue # WeakKeyValue -- could be our fault so retry
  155. S = pow(t, b, n)
  156. K = hash(S)
  157. # Create the proof using a keyed hash.
  158. m = client_authenticator(K, n, g, user, s, A, B, u)
  159. return (s, B, u, K, m)
  160. # These two functions calculate the "proofs": keyed hashes of values used
  161. # in the computation of the key.
  162. def client_authenticator(K, n, g, user, s, A, B, u):
  163. return hmac(K, hash(n) + hash(g) + hash(user) + s + `A` + `B` + `u`)
  164. def host_authenticator(K, A, m):
  165. return hmac(K, `A` + m)
  166. # Simple password file management.
  167. def read_passwd(filename = 'passwd'):
  168. global passwd
  169. try:
  170. f = open(filename)
  171. passwd = pickle.load(f)
  172. f.close()
  173. except:
  174. passwd = {}
  175. def write_passwd(filename):
  176. f = open(filename, 'w')
  177. pickle.dump(passwd, f)
  178. f.close()
  179. # Utility functions to read/write long ints and strings from/to a file (or
  180. # socket). Values are stored in base64 delimited by blank lines.
  181. def decode_long(val):
  182. val = val.replace(' ', '\n')
  183. return string_to_long(base64.decodestring(val))
  184. def encode_long(val):
  185. s = base64.encodestring(long_to_string(val))
  186. s = s.replace('\n' , ' ')
  187. return s
  188. def read_long(file):
  189. ll = []
  190. while 1:
  191. line = file.readline()
  192. if not line:
  193. raise EOFError
  194. l = string.strip(line)
  195. if not l:
  196. break
  197. ll.append(l)
  198. val = decode_long(string.join(ll, ''))
  199. return val
  200. def decode_string(val):
  201. val = val.replace(' ', '\n')
  202. return base64.decodestring(val)
  203. def encode_string(val):
  204. s = base64.encodestring(val)
  205. s = s.replace('\n', ' ')
  206. return s
  207. def read_string(file):
  208. ll = []
  209. while 1:
  210. line = file.readline()
  211. if not line:
  212. raise EOFError
  213. l = string.strip(line)
  214. if not l:
  215. break
  216. ll.append(l)
  217. val = decode_string(string.join(ll, ''))
  218. return val
  219. def usage():
  220. print "srp.py <passwdfile> [command [ <arg1> <arg2> ... ]"
  221. print " e.g. srp.py /etc/supervisor/passwd add ausername apassword"
  222. print
  223. print "Command summary:"
  224. print
  225. print " list -- list all known usernames"
  226. print " del -- delete a user"
  227. print " add <username> [password] -- add a user"
  228. print
  229. print "When run without options, an interactive interpreter is started."
  230. if __name__ == '__main__':
  231. import sys
  232. if len(sys.argv) < 2:
  233. usage()
  234. sys.exit(1)
  235. passwdfile = sys.argv[1]
  236. from cmd import Cmd
  237. class srp(Cmd):
  238. def __init__(self):
  239. Cmd.__init__(self)
  240. self.saved = 1
  241. def emptyline(self):
  242. pass
  243. def do_EOF(self, arg):
  244. print
  245. if not self.saved:
  246. print 'passwd file not saved; "quit" to abort or "save" first.'
  247. return
  248. return 1
  249. def do_quit(self, arg):
  250. return 1
  251. def do_list(self, arg):
  252. print passwd.keys()
  253. def do_add(self, arg):
  254. try:
  255. user, password = arg.split()
  256. except:
  257. user = arg
  258. password = None
  259. if not user:
  260. print "add username <password>"
  261. return
  262. new_passwd(user, password)
  263. self.saved = 0
  264. def do_del(self, user):
  265. if passwd.has_key(user):
  266. del(passwd[user])
  267. self.saved = 0
  268. def do_save(self, arg):
  269. write_passwd(passwdfile)
  270. self.saved = 1
  271. interp = srp()
  272. interp.prompt = "SRP> "
  273. read_passwd(passwdfile)
  274. if len(sys.argv) > 2:
  275. interp.onecmd(" ".join(sys.argv[2:]))
  276. if not interp.saved:
  277. interp.do_save(passwdfile)
  278. else:
  279. interp.cmdloop()