
Secure Remote Password
Package srp is a Go implementation of
Secure Remote Password
protocol as defined by RFC 2945 and
RFC 5054.
SRP is an authentication method that allows the use
of user names and passwords over unencrypted channels without
revealing the password to an eavesdropper. SRP also supplies a
shared secret at the end of the authentication sequence that can be
used to generate encryption keys.
It's based on the work of 1Password,
with a few key changes to restore compatibility with the RFC, and to make
the codebase more idiomatic.
SRP is used by leading privacy-conscious companies such as
Apple,
1Password,
ProtonMail,
and yours truly.
Protocol
Conceptually, SRP is not different from how most of us think about
authentication; the client signs up by storing a secret on the server, and to
login, it must prove to that server that it knows it.
With SRP, the client first registers by storing a cryptographic value (verifier)
derived from its password on the server. To login, they both exchange a
series of opaque values but never the user's password or the verifier. Trust
can be established at the end of the process because for the server,
only the client who knows the verifier could have sent those values,
and vice versa.
SRP comes with four major benefits:
- For the end-user, the familiar experience of using a username and a password
remains fundamentally the same;
- Server cannot leak a password it never saw;
- After registration, both client and server can formally verify each
other's identities without needing a third-party (e.g. CA);
- Sessions can be secured with an extra layer of encryption on top of TLS.
Params selection
SRP requires the client and the server to agree on a given set of parameters,
namely a Diffie-Hellman (DH) group, a hash function, and a key derivation
function.
All the DH groups defined in RFC 5054
are available. You can use any hash function you would like
(e.g. SHA256, Blake2b), and
the same goes for key derivation
(e.g. Argon2,
Scrypt or
PBKDF2).
The example below shows the DH group 16 used in conjunction with SHA256 and
Argon2:
import (
"runtime"
"github.com/posterity/srp"
"golang.org/x/crypto/argon2"
_ "crypto/sha256"
)
func KDFArgon2(username, password string, salt []byte) ([]byte, error) {
p := []byte(username + ":" + password)
key := argon2.IDKey(p, salt, 3, 256 * 1048576, runtime.NumCPU(), 32)
return key, nil
}
var params = &srp.Params{
Name: "DH16–SHA256–Argon2",
Group: srp.RFC5054Group4096,
Hash: crypto.SHA256,
KDF: KDFArgon2,
}
User Registration
During user registration, the client must send the server a verifier; a
value safely derived from the user's password with a unique random salt.
tp, err := srp.ComputeVerifier(params, username, password, srp.NewSalt())
if err != nil {
log.Fatalf("error computing verifier: %v", err)
}
Send(tp)
The Triplet returned by ComputeVerifier encapsulates three variables into a
single byte array that the server can store:
It's important for the server to treat the triplet with care, as it contains
a secret value (verifier) which should never be shared with anyone.
The salt value it contains however should be made available publicly to
anyone who asks via a public URL.
Login
When it's time to authenticate a user, client and server follow a three-step
process:
client and server exchange ephemeral public keys A and B,
respectively;
client computes a proof and sends it to the server;
server checks the client's proof and sends the client a proof of their own.
Client-side
On the client side, the first step is to initialize a Client.
var (
username = "alice@example.com"
password = "p@$$w0rd"
salt []byte
)
client, err := srp.NewClient(params, username, password, salt)
if err != nil {
log.Fatal(err)
}
All the values must match those used to create the verifier that was stored
on the server. The salt should be retrievable from the server without
requiring prior authentication.
The next step is to send the ephemeral public key A to the server:
A := client.A()
The server will do the same, sending their ephemeral public key B instead.
Configure it on the client as following:
var B []byte
client.SetB(B)
Next, compute the client proof and send it to the server.
M1, err := client.ComputeM1()
if err != nil {
log.Fatalf("error computing proof: %v", err)
}
If the server accepts the client's proof, they will send their own server proof.
var M2 []byte
ok, err := client.CheckM2(M2)
if err != nil {
log.Fatalf("error checking M2: %v", err)
}
if !ok {
log.Fatalf("server is not authentic")
}
At this stage, the client and the server can trust each other, and can
(optionally) use a shared encryption key to secure their session from this
point on.
sharedKey, err := client.SessionKey()
if err != nil {
log.Fatalf("error computing key: %v", err)
}
Server-side
The process on the server-side is very similar to the above, with one key
difference: the server must first receive and verify the client's proof (M1)
before it computes and shares its own (M2).
var (
triplet srp.Triplet
)
server, err := srp.NewServer(params, username, password, salt)
if err != nil {
log.Fatal(err)
}
The next step is to wait for the user to send their ephemeral public key A
to configure it on the server.
var A []byte
if err := server.setA(A); err != nil {
log.Fatal("error configuring A: %v", err)
}
If no error is caught, the next step is to send to server's ephemeral public
key B to the client.
B := server.B()
Now the server must wait for the client to submit their proof M1.
var M1 []byte
ok, err := server.CheckM1(M1)
if err != nil {
log.Fatalf("error verifying M1: %v", err)
}
if !ok {
log.Fatalf("client is not authentic")
}
If this verification fails, the process must stop at this point, and no further
information should be shared with the client over this session. A new Server
instance will need to be created and the negotiation restarted.
If successful, the server can consider the client as authentic, but it
still needs to send its own proof M2.
M2, err := server.ComputeM2()
if err != nil {
log.Fatalf("error computing M2: %v", err)
}
If the client accepts the proof, they can both consider each other as
authentic and compute their shared session key to encrypt their exchanges
and protect themselves from eavesdropping.
sharedKey, err := server.SessionKey()
if err != nil {
log.Fatalf("error computing key: %v", err)
}
Implementation
SRP is protocol-agnostic and can be implemented on top of any existing
client/server architecture.
At Posterity, we use a custom websocket protocol, but a simple HTTP API would
be equally suitable. In any case, the process can usually be completed in
two round-trips, excluding the request needed to retrieve the salt value
of the user:
(Client) 👧🏼 ---------→ A
B ←--------- 👨🏽 (Server)
(Client) 👧🏼 ---------→ M1
M2 ←--------- 👨🏽 (Server)
If you're using a stateless architecture (e.g. REST), the state of a Server
can be saved and restored using Server.Save and RestoreServer respectively.
Bear in mind that a Server's internal state contains the user's verifier,
and should therefore be handled appropriately.
A secure connection between the client and the server is a necessity,
especially when the client first needs to send their verifier to the server.
Session Encryption
SRP defines a way for the client and the server to independently compute a
strong but ephemeral encryption key which they can use to secure their
communications during a session.
At Posterity, we use
Encrypted-Content-Encoding for HTTP to set
that in motion, using the shared key to encrypt all client-server exchanges
with AES-256-GCM after login.
Contributions
Contributions are welcome via Pull Requests.
About us
What if you're hit by a bus tomorrow? Posterity helps
you make a plan in the event something happens to you.