srp

package module
v2.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 29, 2023 License: Apache-2.0 Imports: 15 Imported by: 0

README

GoDoc reference

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.

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:

  1. For the end-user, the familiar experience of using a username and a password remains fundamentally the same;
  2. Server cannot leak a password it never saw;
  3. After registration, both client and server can formally verify each other's identities without needing a third-party (e.g. CA);
  4. 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"
)

// KDFArgon2 uses Argon2.
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
}

// Params instance using DH group 16, SHA256 for hashing and Argon2 as a KDF.
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)
}

// The verifier can be accessed as tp.Verifier().

// On the server, it's recommended to store the verifier along with
// the username and the salt used to compute it, so sending the whole
// triplet tp ([]byte) is more appropriate.
Send(tp)

The Triplet returned by ComputeVerifier encapsulates three variables into a single byte array that the server can store:

  • Username
  • Verifier
  • Salt

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:

  1. client and server exchange ephemeral public keys A and B, respectively;
  2. client computes a proof and sends it to the server;
  3. 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 // Retrieved from the server
)
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()

// Send A to the server

The server will do the same, sending their ephemeral public key B instead. Configure it on the client as following:

var B []byte // Received from the server

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)
}

// send M1 to the server

If the server accepts the client's proof, they will send their own server proof.

var M2 []byte // Received from the server

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)
}

// sharedKey is a 256 bit key which was computed
// locally.
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                             // Retrieved from the server
)
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 // received from the client

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()

// send B to the client

Now the server must wait for the client to submit their proof M1.

var M1 []byte   // Received from the client

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)
}

// send M2 to the client

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)
}

// sharedKey is a 256 bit key which was computed
// locally.

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.

Documentation

Overview

Package srp is an implementation of the Secure Remote Password protocol as defined in RFC5054 and RFC2945.

Index

Examples

Constants

View Source
const SaltLength = 12

SaltLength represents the default length for a salt created with NewSalt.

Variables

View Source
var (
	RFC5054Group2048 = &Group{
		ID:           "14",
		Generator:    big.NewInt(2),
		N:            mustParseHex(hex2048),
		ExponentSize: 27,
	}

	RFC5054Group3072 = &Group{
		ID:           "15",
		Generator:    big.NewInt(5),
		N:            mustParseHex(hex3072),
		ExponentSize: 32,
	}

	RFC5054Group4096 = &Group{
		ID:           "16",
		Generator:    big.NewInt(5),
		N:            mustParseHex(hex4096),
		ExponentSize: 38,
	}

	RFC5054Group6144 = &Group{
		ID:           "17",
		Generator:    big.NewInt(5),
		N:            mustParseHex(hex6144),
		ExponentSize: 43,
	}

	RFC5054Group8192 = &Group{
		ID:           "18",
		Generator:    big.NewInt(19),
		N:            mustParseHex(hex8192),
		ExponentSize: 48,
	}
)

Diffie-Hellman group 14, 15, 16, 17 and 18 defined in RFC5054.

View Source
var ErrClientNotReady = errors.New("server's public ephemeral key (B) must be set first")

ErrClientNotReady is returned when the client is not ready for the invoked action.

View Source
var ErrServerNoReady = errors.New("client's public ephemeral key (A) must be set first")

ErrServerNoReady is returned when the server is not ready for the invoked action.

View Source
var RFC5054Group1024 = &Group{
	ID:           "2",
	Generator:    big.NewInt(2),
	N:            mustParseHex(hex1024),
	ExponentSize: 32,
}

Diffie-Hellman group 2.

Deprecated: This group is not recommended for production-use.

View Source
var RFC5054Group1536 = &Group{
	ID:           "5",
	Generator:    big.NewInt(2),
	N:            mustParseHex(hex1536),
	ExponentSize: 23,
}

Diffie-Hellman group 5.

Deprecated: This group is not recommended for production-use.

Functions

func NFKD

func NFKD(str string) string

NFKD returns str as a NFKD-normalized unicode string, stripped of all leading and trailing spaces.

func NewSalt

func NewSalt() []byte

NewSalt returns a new random salt using rand.Reader.

func RFC5054KDF deprecated

func RFC5054KDF(username, password string, salt []byte) ([]byte, error)

RFC5054KDF is the KDF defined in RFC5054.

x = SHA(s | SHA(U | ":" | p))

Deprecated: This KDF function is only provided for compatibility with RFC5054 and for testing purposes. It is not recommended for production use. Instead, use a key derivation function KDF designed for password hashing such as Argon2, Scrypt or PBKDF2.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client represents the client-side perspective of an SRP session.

Example

Example of a client session.

var (
	username = "alice@example.com"
	password = "some-password"
)

// Request the user's salt from the server.
// The server should send it to whoever asks.
salt := Receive()

// Create a client, specifying the same params used on the server.
client, err := NewClient(params, username, password, salt)
if err != nil {
	log.Fatal(err)
}

// Send A to the server.
A := client.A()
Send(A)

// Receive B from the server,
// then configure it on the client.
B := Receive()
if err := client.SetB(B); err != nil {
	log.Fatalf("invalid B received from the server: %v", err)
}

// Compute the proof M1,
// then send it to the server.
M1, err := client.ComputeM1()
if err != nil {
	log.Fatalf("failed to compute M1: %v", err)
}
Send(M1)

// If the server accepts the client's proof (M1), it will
// send a proof of their own (M2).
M2 := Receive()
valid, err := client.CheckM2(M2)
if err != nil {
	log.Fatalf("failed to verify server proof M2: %v", err)
}
if valid == false {
	log.Fatalf("server is not authentic: %v", err)
}

// At this stage, the client and the server
// have proved to each other that they know
// the same secret.
//
// They can both consider each other as authentic
// and legitimate.

// They also share a common key they both derived independently
// from the process
K, err := client.SessionKey()
if err != nil {
	log.Fatalf("failed to access shared session key: %v", err)
}

// K can optionally be used to encrypt/decrypt all exchanges between
// them moving forward.
SendEncrypted(K, []byte("hello, world!"))
Output:

func NewClient

func NewClient(params *Params, username, password string, salt []byte) (*Client, error)

NewClient a new SRP client instance.

func (*Client) A

func (c *Client) A() []byte

A returns the public ephemeral key (A) of this client.

func (*Client) CheckM2

func (c *Client) CheckM2(M2 []byte) (bool, error)

CheckM2 returns true if the server proof M2 is verified.

func (*Client) ComputeM1

func (c *Client) ComputeM1() ([]byte, error)

ComputeM1 returns the proof (M1) which should be sent to the server.

func (*Client) SessionKey

func (c *Client) SessionKey() ([]byte, error)

SessionKey returns the session key that will be shared with the server.

func (*Client) SetB

func (c *Client) SetB(public []byte) error

SetB configures the server's public ephemeral key (B).

type Group

type Group struct {
	ID           string
	Generator    *big.Int
	N            *big.Int
	ExponentSize int
}

Group represents a Diffie-Hellman group.

type KDF

type KDF func(username, password string, salt []byte) ([]byte, error)

KDF is the signature of a key derivation function.

type Params

type Params struct {
	Name  string
	Group *Group
	Hash  crypto.Hash
	KDF   KDF
}

Params represents the DH group, the hash and key derivation function that a client and server jointly agreed to use.

  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,
	 }
Example

This example shows how to create a Params instance that ensures strict compatibility with RFC5054 (Not recommended in production).

params := &Params{
	Group: RFC5054Group1024,
	Hash:  crypto.SHA1,
	KDF:   RFC5054KDF,
}

_, err := NewClient(params, "username", "p@$$w0rd", NewSalt())
if err != nil {
	log.Fatal(err)
}
Output:

func (*Params) String

func (p *Params) String() string

String returns the name of p.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server represents the server-side perspective of an SRP session.

Example

Example of a server session.

// Load the user's Triplet from the persistent
// storage.
var user Triplet = Query("alice@example.com")

// Create a server, specifying the same params used on the client.
server, err := NewServer(params, user.Username(), user.Salt(), user.Verifier())
if err != nil {
	log.Fatal(err)
}

// Send B to the client.
B := server.B()
Send(B)

// Receive A from the client,
// then configure it on the server.
A := Receive()
if err := server.SetA(A); err != nil {
	log.Fatalf("invalid A received from the client: %v", err)
}

// The server needs to verify the client's proof first,
// so it must wait to receive M1.
M1 := Receive()

// Verify the client proof M1.
// The process must be interrupted if valid is false, or
// an error occurred.
valid, err := server.CheckM1(M1)
if err != nil {
	log.Fatalf("failed to verify client proof M1: %v", err)
}
if valid == false {
	log.Fatalf("client is not authentic: %v", err)
}

// The client proved they're authentic, so it's safe
// to compute the server proof (M2) and send it over.
M2, err := server.ComputeM2()
if err != nil {
	log.Fatalf("failed to compute M1: %v", err)
}
Send(M2)

// At this stage, the server should consider
// the client as authentic and requests from it
// should be fulfilled.

// They both share a common key they both derived
// independently from the process,
K, err := server.SessionKey()
if err != nil {
	log.Fatalf("failed to access session key: %v", err)
}

// K can optionally be used to encrypt/decrypt all exchanges between
// them moving forward.
SendEncrypted(K, []byte("hello, world!"))
Output:

func NewServer

func NewServer(params *Params, username string, salt, verifier []byte) (*Server, error)

NewServer returns a new SRP server instance.

func RestoreServer

func RestoreServer(params *Params, state []byte) (*Server, error)

RestoreServer restores a server from a previous state obtained with Server.Save.

func (*Server) B

func (s *Server) B() []byte

B returns the server's public ephemeral key B.

func (*Server) CheckM1

func (s *Server) CheckM1(M1 []byte) (bool, error)

CheckM1 returns true if the client proof M1 is verified.

func (*Server) ComputeM2

func (s *Server) ComputeM2() ([]byte, error)

ComputeM2 returns the proof (M2) which should be sent to the client.

An error is returned if the client's proof (M1) has not been checked by calling the s.CheckM1 method first.

func (*Server) MarshalJSON

func (s *Server) MarshalJSON() ([]byte, error)

MarshalJSON returns a JSON object representing the current state of s.

func (*Server) Reset

func (s *Server) Reset(params *Params, username string, salt, verifier []byte) error

Reset resets s to its initial state.

func (*Server) Save

func (s *Server) Save() ([]byte, error)

Save encodes the current state of s in a JSON object. Use RestoreServer to restore a previously saved state.

func (*Server) SessionKey

func (s *Server) SessionKey() ([]byte, error)

SessionKey returns the session key that will be shared with the client.

An error is returned if the client's proof (M1) has not been checked by calling the s.CheckM1 method first.

func (*Server) SetA

func (s *Server) SetA(public []byte) error

SetA configures the public ephemeral key (B) of this server.

func (*Server) UnmarshalJSON

func (s *Server) UnmarshalJSON(data []byte) error

UnmarshalJSON restores from an existing state object obtained with MarshalJSON.

type Triplet

type Triplet []byte

Triplet holds the parameters the server should store in a single byte array.

A triplet is structured as following:

+------------------------+
| usernameLen (1)        |
+------------------------+
| username (usernameLen) |
+------------------------+
| saltLen (1)            |
+------------------------+
| salt (saltLen)         |
+------------------------+
| verifier               |
+------------------------+

func ComputeVerifier

func ComputeVerifier(params *Params, username, password string, salt []byte) (Triplet, error)

ComputeVerifier computes a verifier value from the user's username, password and salt.

The function is called client-side to generate a triplet containing the information that should be sent to the server over a secure connection (TLS), and stored in a secure persistent-storage (e.g. database).

Example

The verifier is calculated on the client, and sent to the server for storage along with the username and salt used to compute it as a triplet.

const (
	username = "bob@example.com"
	password = "p@$$w0rd"
)
tp, err := ComputeVerifier(params, username, password, NewSalt())
if err != nil {
	log.Fatalf("failed to compute verifier: %v", err)
}

// The verifier can be accessed via the returned triplet tp
// as tp.Verifier().

// On the server, it's recommended to store the verifier along with
// the username and the salt used to compute it, so sending the whole
// triplet ([]byte) is more appropriate.
Send(tp)
Output:

func NewTriplet

func NewTriplet(username string, salt, verifier []byte) Triplet

NewTriplet returns a new Triplet instance from the given username, verifier and salt.

NewTriplet panics if the length of username or salt exceeds math.MaxUint8.

func (Triplet) MarshalJSON

func (t Triplet) MarshalJSON() ([]byte, error)

MarshalJSON returns a JSON representation of t that includes the username and the salt, but not the verifier.

{
   "username": "alice",
   "salt": "EzDH8afmICl6Xxsv",
}

func (Triplet) Salt

func (t Triplet) Salt() []byte

Salt returns the Salt in p, or an empty string if p is mis-formatted.

func (Triplet) Username

func (t Triplet) Username() string

Username returns the username string in p, or an empty string if p is mis-formatted.

func (Triplet) Verifier

func (t Triplet) Verifier() []byte

Verifier returns the verifier in p, or an empty string if p is mis-formatted.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL