iovxw

别在服务端储存用户密码了,来用 SRP 吧

只是介绍

新年第一篇文章就是从去年的草稿箱里发出来的,感觉真不怎么新……

当然这个协议也不新了,是 2000 年出的 RFC

不过现在都 2017 年了,还是有一大堆网站在用什么 md5 加盐(不,就算你用 SHA2XX 也不行),甚至连 bcrypt 和 Argon2 是啥都不知道,感觉真是悲伤(

所以,来看看 SRP 是个啥(如果你真的不知道 Argon2,还是先去看它吧):

The Secure Remote Password (SRP) protocol is widely-tested and widely-deployed password-authenticated key agreement (PAKE) protocol which mutually authenticates both the user and server while never sending any password-equivalent data over the network. Authentication attempts cannot be replayed, and the protocol remains secure even in the presence of active tampering by a third party.

以上摘自《ProtonMail Authentication Technical Implementation》中对 SRP 的介绍(ProtonMail 就是那个斯诺登事件后创建的以安全著称的邮件服务,而 SRP 就是人家用的方案)

看见重点了吗?

never sending any password-equivalent data over the network

是的,哪怕你用的是 Argon2,也得想办法先把密码发到服务器上,而 SRP

NEVER SENDING ANY PASSWORD-EQUIVALENT DATA OVER THE NETWORK!!

咳,激动了,总之就是这么一个神奇的东西


SRP 的安全性基于离散对数问题,就和 DH 还有 ECC 一样(当然问题是量子计算机出来就完蛋了,不过现在不用关心这个)

具体的原理不用管,相信看这篇文章的都不懂密码学(我也不懂,真的),要是真想研究的话请看 RFC

一般程序员只需要了解协议过程就行了,而维基百科上的 Python 例子就讲的很明白。这里就直接复制过来:

# An example SRP authentication
# WARNING: Do not use for real cryptographic purposes beyond testing.
# based on http://srp.stanford.edu/design.html
import hashlib
import random

def global_print(*names):
    x = lambda s: ["{}", "0x{:x}"] [hasattr(s, 'real')].format(s)
    print("".join("{} = {}\n".format(name, x(globals()[name])) for name in names))

# note: str converts as is, str( [1,2,3,4] ) will convert to "[1,2,3,4]"
def H(*args):  # a one-way hash function
    a = ':'.join(str(a) for a in args)
    return int(hashlib.sha256(a.encode('utf-8')).hexdigest(), 16)

def cryptrand(n=1024):
    return random.SystemRandom().getrandbits(n) % N

# A large safe prime (N = 2q+1, where q is prime)
# All arithmetic is done modulo N
# (generated using "openssl dhparam -text 1024")
N = '''00:c0:37:c3:75:88:b4:32:98:87:e6:1c:2d:a3:32:
       4b:1b:a4:b8:1a:63:f9:74:8f:ed:2d:8a:41:0c:2f:
       c2:1b:12:32:f0:d3:bf:a0:24:27:6c:fd:88:44:81:
       97:aa:e4:86:a6:3b:fc:a7:b8:bf:77:54:df:b3:27:
       c7:20:1f:6f:d1:7f:d7:fd:74:15:8b:d3:1c:e7:72:
       c9:f5:f8:ab:58:45:48:a9:9a:75:9b:5a:2c:05:32:
       16:2b:7b:62:18:e8:f1:42:bc:e2:c3:0d:77:84:68:
       9a:48:3e:09:5e:70:16:18:43:79:13:a8:c3:9c:3d:
       d0:d4:ca:3c:50:0b:88:5f:e3'''
N = int(''.join(N.split()).replace(':', ''), 16)
g = 2        # A generator modulo N

k = H(N, g)  # Multiplier parameter (k=3 in legacy SRP-6)

print("#. H, N, g, and k are known beforehand to both client and server:")
global_print("H", "N", "g", "k")

print("0. server stores (I, s, v) in its password database")

# the server must first generate the password verifier
I = "person"         # Username
p = "password1234"   # Password
s = cryptrand(64)    # Salt for the user
x = H(s, I, p)       # Private key
v = pow(g, x, N)     # Password verifier
global_print("I", "p", "s", "x", "v")

print("1. client sends username I and public ephemeral value A to the server")
a = cryptrand()
A = pow(g, a, N)
global_print("I", "A")  # client->server (I, A)

print("2. server sends user's salt s and public ephemeral value B to client")
b = cryptrand()
B = (k * v + pow(g, b, N)) % N
global_print("s", "B")  # server->client (s, B)

print("3. client and server calculate the random scrambling parameter")
u = H(A, B)  # Random scrambling parameter
global_print("u")

print("4. client computes session key")
x = H(s, I, p)
S_c = pow(B - k * pow(g, x, N), a + u * x, N)
K_c = H(S_c)
global_print("S_c", "K_c")

print("5. server computes session key")
S_s = pow(A * pow(v, u, N), b, N)
K_s = H(S_s)
global_print("S_s", "K_s")

print("6. client sends proof of session key to server")
M_c = H(H(N) ^ H(g), H(I), s, A, B, K_c)
global_print("M_c")
# client->server (M_c) ; server verifies M_c

print("7. server sends proof of session key to client")
M_s = H(A, M_c, K_s)
global_print("M_s")
# server->client (M_s) ;  client verifies M_s

然后 SRP 的各种实现可以看这里: http://srp.stanford.edu/links.html

或者可以去相应语言的包管理系统还有 Github 搜搜看,当然不要自己实现,除非你真的懂密码学 :-)