npm package: @mzattahri/srp
Description: RFC-compliant SRP client/server implementation.
Author: MZA
Latest version: 3.0.1

[![npm version](https://img.shields.io/npm/v/@mzattahri/srp.svg)](https://www.npmjs.com/package/@mzattahri/srp)

```bash
npm install @mzattahri/srp
```

# Secure Remote Password

Package `@mzattahri/srp` is a TypeScript implementation of
[Secure Remote Password](https://en.wikipedia.org/wiki/Secure_Remote_Password_protocol)
protocol as defined by [RFC 2945](https://tools.ietf.org/html/rfc2945) and
[RFC 5054](https://tools.ietf.org/html/rfc5054).

> 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](https://support.apple.com/guide/security/escrow-security-for-icloud-keychain-sec3e341e75d/web),
[1Password](https://blog.1password.com/developers-how-we-use-srp-and-you-can-too/), and
[ProtonMail](https://protonmail.com/blog/cryptographic-architecture-response/).

## 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](https://tools.ietf.org/html/rfc5054)
are available. You can use any hash function you would like
(e.g. `SHA-256`, `SHA-512`), 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 `SHA-256` and
a custom KDF:

```typescript
import {
  RFC5054Group4096,
  concatUint8Array,
  type Params,
} from "@mzattahri/srp"

const params: Params = {
  name: "DH16-SHA256-CustomKDF",
  group: RFC5054Group4096,
  hash: async (...inputs: Uint8Array[]) => {
    const data = concatUint8Array(...inputs)
    return new Uint8Array(await crypto.subtle.digest("SHA-256", data))
  },
  kdf: async (username: string, password: string, salt: Uint8Array) => {
    const enc = new TextEncoder()
    const inner = await crypto.subtle.digest(
      "SHA-256",
      concatUint8Array(enc.encode(username), enc.encode(":"), enc.encode(password)),
    )
    return new Uint8Array(
      await crypto.subtle.digest("SHA-256", concatUint8Array(salt, new Uint8Array(inner))),
    )
  },
}
```

### 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.

```typescript
import { Triplet, generateSalt } from "@mzattahri/srp"

const salt = generateSalt()
const triplet = await Triplet.create(params, username, password, salt)

// The verifier can be accessed as triplet.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 is more appropriate.
await send(triplet.toUint8Array())
```

The `Triplet` returned by `Triplet.create` 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`.

```typescript
import { Client } from "@mzattahri/srp"

const username = "alice@example.com"
const password = "p@$$w0rd"
const salt: Uint8Array = // Retrieved from the server

const client = await Client.initialize(params, username, password, salt)
```

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:

```typescript
const 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:

```typescript
const B: Uint8Array = // Received from the server

await client.setB(B)
```

Next, get the client proof and send it to the server.

```typescript
const M1 = client.M1
if (!M1) {
  throw new Error("M1 not available")
}

// send M1 to the server
```

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

```typescript
const M2: Uint8Array = // Received from the server

if (!client.checkM2(M2)) {
  throw new Error("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.

```typescript
const sharedKey = await client.exportSessionKey()

// sharedKey is a CryptoKey ready for use with AES-GCM
```

#### Server-side

On the server side, the process is very similar, with one key
difference: the server must first receive and verify the client's proof (`M1`)
before it computes and shares its own (`M2`).

```typescript
import { Server, Triplet } from "@mzattahri/srp"

// Retrieve the triplet from your database
const tripletBytes: Uint8Array = // from database
const triplet = new Triplet(tripletBytes)

const server = await Server.fromTriplet(params, triplet)
```

The next step is to wait for the user to send their ephemeral public key `A`
and configure it on the server.

```typescript
const A: Uint8Array = // received from the client

await server.setA(A)
```

If no error is thrown, send the server's ephemeral public key `B` to the client.

```typescript
const B = server.B

// send B to the client
```

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

```typescript
const M1: Uint8Array = // Received from the client

if (!server.checkM1(M1)) {
  throw new Error("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.

If successful, the server can consider the client as _authentic_, but it
still needs to send its own proof `M2`.

```typescript
const M2 = server.M2
if (!M2) {
  throw new Error("M2 not available")
}

// 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.

```typescript
const sharedKey = await server.exportSessionKey()

// sharedKey is a CryptoKey ready for use with AES-GCM
```

#### State Serialization (Stateless Architectures)

If you're using a stateless architecture (e.g., REST), the state of a `Server`
can be saved and restored using `toJSON()` and `Server.fromJSON()` respectively.

```typescript
// Save server state between requests
const state = server.toJSON()
// Store state securely (encrypted!) between requests

// Later, restore the server
const server = await Server.fromJSON(params, state)
```

**WARNING**: The server state contains sensitive cryptographic material including
the user's verifier. It MUST be encrypted before storage or transmission.

## Implementation

SRP is protocol-agnostic and can be implemented on top of any existing
client/server architecture.

The process can usually be completed in two round-trips, excluding the
request needed to retrieve the `salt` value of the user:

```plain
(Client) 👧🏼  ---------→ A
                        B   ←--------- 👨🏽 (Server)

(Client) 👧🏼  ---------→ M1
                        M2  ←--------- 👨🏽 (Server)

```

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.

The `exportSessionKey()` method returns a `CryptoKey` ready for use with
AES-256-GCM to encrypt all client-server exchanges after login.

## Contributions

Contributions are welcome via Pull Requests.