Skip to content

crypto.scram #

Description

crypto.scram implements the Salted Challenge Response Authentication Mechanism (SCRAM) family of SASL mechanisms: SCRAM-SHA-1 (RFC 5802), SCRAM-SHA-256 (RFC 7677) and SCRAM-SHA-512, with and without channel binding.

SCRAM is a password authentication protocol that never puts the password on the wire. The client proves it knows the password, the server proves it too, and neither side can replay a recorded exchange:

  • the password is stretched with PBKDF2 under a per-user salt, so the server stores two derived keys instead of anything reversible;
  • what the client sends is a proof computed over both nonces and both messages, so it is worthless outside the exchange that produced it;
  • the server signs the same transcript back, so a client that reaches the end of the exchange knows it is talking to a server that holds the real credentials, not to something that merely accepted its proof.

It is what PostgreSQL 10+, MongoDB 3.0+, Kafka, LDAP, XMPP and the SASL profiles of IMAP and SMTP authenticate with. If you are writing a driver for any of them in V, this is the piece that used to be missing.

The module has no dependencies outside vlib and no C dependency: it is built on crypto.hmac, crypto.sha1, crypto.sha256, crypto.sha512, crypto.rand, crypto.subtle and encoding.base64.

Usage

The exchange is four messages. The client produces the first and third, the server the second and fourth. That maps to three calls on Client and two on Server, each one taking the message the peer just sent and returning the message to send back.

Authenticating against a server

This is the case you want most of the time. Everything the transport has to do is carry four opaque SCRAM payloads. They can contain UTF-8 user or authorization identities, so preserve their bytes without re-encoding them.

import crypto.scram

mut client := scram.new_client(username: 'user', password: 'pencil')!

// 1. announce the mechanism and send the first message
send(client.mechanism_name(), client.first()!)
// 2. answer the server challenge
final := client.final(receive())!
send_payload(final)
// 3. check that the server proved itself too — never skip this step
client.verify(receive())!

Do not treat the connection as authenticated before verify returns. A server that answers the first three messages and fails the fourth does not hold your credentials.

Storing credentials

A server never stores a password. new_credentials derives the record it does store, with a fresh random salt:

import crypto.scram

fn main() {
    credentials := scram.new_credentials(.sha256, 'pencil')!
    // Persist all four fields; none of them lets you recover the password.
    println(credentials.mechanism.name())
    println(credentials.salt.len)
    println(credentials.iterations)
    println(credentials.stored_key.len)
}

Use derive_credentials instead when the salt and iteration count are imposed, for instance to reproduce a record another implementation wrote.

encode and parse_credentials turn a record into this one-line format and back:

import crypto.scram

fn main() {
    credentials := scram.derive_credentials(.sha256, 'pencil', 'saltsaltsaltsalt'.bytes(), 4096)!
    line := credentials.encode()
    // SCRAM-SHA-256$4096:c2FsdHNhbHRzYWx0c2FsdA==$Y7KMtn...:c1MMj1...
    restored := scram.parse_credentials(line)!
    assert restored.stored_key == credentials.stored_key
}

For SCRAM-SHA-1, this is the RFC 5803 authPassword scheme laid out in the syntax of RFC 3112 and can interoperate with LDAP implementations of that RFC. For SCRAM-SHA-256, it is the layout PostgreSQL stores in pg_authid.rolpassword. PostgreSQL does not accept SCRAM-SHA-1 or SCRAM-SHA-512 records, while RFC 5803 does not define SCRAM-SHA-256 or SCRAM-SHA-512 records for LDAP. Treat the line as a secret: the server key in it is enough to impersonate the server to that user.

Printing is safe by default. Client, Server and Credentials define their own str(), so a println(client) while debugging shows the state and the user name but never the password or the keys — V would otherwise format every field, including the secrets.

Authenticating a client

Server reads credentials through a callback, so it does not care where they are stored:

import crypto.scram

fn main() {
    credentials := scram.new_credentials(.sha256, 'pencil')!

    mut server := scram.new_server(
        lookup: fn [credentials] (username string) !scram.Credentials {
            // Look the user up in your database here.
            return credentials
        }
    )!
    mut client := scram.new_client(username: 'user', password: 'pencil')!

    server_first := server.first(client.first()!)!
    server_final := server.final(client.final(server_first)!)!
    client.verify(server_final)!

    println('${server.username()} authenticated: ${client.done() && server.done()}')
}

When final returns an AuthenticationFailed, answer the client with scram.server_error_message('invalid-proof') rather than closing the connection, so it can tell a refusal from a network failure.

Errors

Four typed errors let a caller react without parsing strings:

Error Meaning
MalformedMessage the peer did not send a valid SCRAM message
AuthenticationFailed the peer failed to prove what it claimed
UnsupportedMechanism mechanism_from_name did not recognise a name
ServerError the server refused with an e= code

Calling the steps out of order, or configuring a client without a user name, returns a plain error() instead: those are bugs in the calling code, not protocol outcomes.

Channel binding

Channel binding ties the exchange to the TLS connection carrying it, so that a proxy which terminates TLS cannot relay a valid exchange. It is what the -PLUS mechanism names mean.

This module does not reach into the TLS layer: pass the binding data in and it will use it.

import crypto.scram

fn main() {
    binding := scram.ChannelBinding{
        mode: .required
        name: 'tls-server-end-point' // RFC 5929; or 'tls-exporter', RFC 9266
        data: certificate_hash()
    }
    mut client := scram.new_client(
        username:        'user'
        password:        'pencil'
        channel_binding: binding
    )!
    println(client.mechanism_name()) // SCRAM-SHA-256-PLUS
}

fn certificate_hash() []u8 {
    return []u8{len: 32}
}

data is mandatory when mode is .required: a binding with a name but no data is refused at construction, because it would otherwise announce a -PLUS mechanism and complete an exchange that binds nothing.

If the server advertised no -PLUS mechanism but your client supports channel binding, set mode: .unsupported_by_server. That sends the GS2 y flag, which lets a server that does offer -PLUS detect that its advertised list was stripped in transit. Leaving the default .not_supported in that situation silently gives up the protection.

The other half of that check is on the server, and it needs a fact the mode cannot carry. ServerConfig.channel_binding.mode describes the exchange in front of it; whether the server advertises -PLUS is a separate question, because a server usually lists both names and lets the client choose. Set advertises_plus: true whenever the -PLUS name is in the list, including on exchanges where the client picked the base mechanism — that is exactly where a y flag means the list was tampered with. Without it a stripped advertisement goes undetected.

Security notes

Normalisation. SCRAM-SHA-1 passes passwords through SASLprep (RFC 4013), while SCRAM-SHA-256 uses the PRECIS OpaqueString profile (RFC 8265). V implements neither profile, so this module hashes the password bytes it is given. Prepare and validate every password, including ASCII input, with the profile for the selected mechanism before calling. Preparation rejects prohibited characters and ensures equivalent spellings do not disagree. User names use SASLprep (RFC 4013). On the server, supply ServerConfig.prepare_username to apply it before credential lookup. Without that callback the server accepts only printable ASCII user names, whose SASLprep form is unchanged, and rejects non-ASCII or control characters. Prepare a client's authorization identity with the application protocol's profile before sending it. On the server, ServerConfig.prepare_authzid applies that profile to the untrusted peer value before Server.authzid() exposes it; without the callback, only printable ASCII authorization identities are accepted.

Iteration count. A client refuses a server asking for fewer than default_min_iterations (4096, the floor in RFC 7677 §4), because a low count makes an offline attack on a recorded exchange cheap. Lower it with ClientConfig.min_iterations only for a legacy server that leaves no choice. new_credentials writes default_iterations (32768).

A client also refuses a count above default_max_iterations (2^20). The count is chosen by the server and consumed before the server has been authenticated, so without a ceiling a hostile endpoint turns a short message into minutes of client CPU. Raise it with ClientConfig.max_iterations if you really talk to a server that asks for more.

User enumeration. Returning an error from the lookup callback tells the caller — and through it, an attacker — that a user name does not exist. RFC 5802 §7 suggests answering unknown users with credentials derived from a server-side secret, so they are indistinguishable from a wrong password. That policy belongs to the application, which is why the module leaves it in the callback.

Timing. Proofs and signatures are compared with crypto.subtle.constant_time_compare.

What SCRAM does not do. It authenticates, it does not authorize: Server.authzid() is a request from the client, not a granted right. And without channel binding, SCRAM over an unauthenticated channel is still vulnerable to a relay; use it over TLS.

Conformance

conformance_test.v drives both halves of the exchange against eight vectors and checks all four messages byte for byte, plus the salted password, stored key and server key behind them.

The first two vectors are the normative examples of RFC 5802 §5 and RFC 7677 §3, transcribed from the RFC text. The other six cover what the RFCs leave without an example: SHA-512, a user name needing saslname escaping, an authorization identity, channel binding and a non-ASCII password.

All eight were generated by an implementation written independently from RFC 5802 §3, then replayed through github.com/xdg-go/scram v1.2.0 — the library the MongoDB Go driver authenticates with — which agrees on every message. Hi() is additionally checked against crypto.pbkdf2, which is an unrelated implementation of the same primitive already in vlib.

References

  • RFC 5802 — SCRAM-SHA-1 and the SCRAM family
  • RFC 7677 — SCRAM-SHA-256
  • RFC 5801 — the GS2 header
  • RFC 5929 — tls-server-end-point channel binding
  • RFC 9266 — tls-exporter channel binding
  • RFC 4013 — SASLprep

Constants #

const default_min_iterations = 4096

default_min_iterations is the smallest PBKDF2 iteration count a client accepts from a server unless ClientConfig.min_iterations says otherwise. RFC 7677 §4 requires clients to reject anything below 4096, because a low count makes an offline attack on an intercepted exchange cheap.

const default_max_iterations = 1_1048576

default_max_iterations is the largest PBKDF2 iteration count a client accepts from a server unless ClientConfig.max_iterations says otherwise. The count is a number a server picks, and deriving the salted password happens before anything about that server has been authenticated, so without a ceiling a hostile endpoint turns a few bytes of server-first-message into unbounded work on the client: at the 999999999 the grammar allows, that is tens of minutes of CPU per connection. 2^20 is three orders of magnitude above what deployments use in practice.

const default_iterations = 32768

default_iterations is the iteration count used by new_credentials when the caller does not pick one. RFC 7677 §4 gives 4096 as the floor; this module defaults an order of magnitude above it. Derivation typically takes tens of milliseconds on contemporary desktop CPUs; benchmark the target deployment when sizing authentication capacity.

const default_salt_size = 16

default_salt_size is the number of random bytes new_credentials uses for a salt, matching the 16 octets recommended by RFC 5802 §5.1.

fn derive_credentials #

fn derive_credentials(mechanism Mechanism, password string, salt []u8, iterations int) !Credentials

derive_credentials computes the credentials a server stores for password, using a salt and iteration count the caller chooses. Use it when you need to reproduce an existing record; prefer new_credentials for a fresh user, as it picks a random salt for you.

Prepare and validate the password before calling: SASLprep for .sha1, PRECIS OpaqueString for .sha256, and the profile agreed with the peer for .sha512. This function hashes the supplied bytes unchanged.

fn mechanism_from_name #

fn mechanism_from_name(name string) !Mechanism

mechanism_from_name maps an IANA SASL mechanism name to a Mechanism. Both spellings are accepted, so SCRAM-SHA-256 and SCRAM-SHA-256-PLUS both return .sha256; whether channel binding is in use is carried by ChannelBinding, not by the mechanism. Use it to pick a mechanism from the list a server advertises.

fn new_client #

fn new_client(config ClientConfig) !&Client

new_client creates a client for one SCRAM exchange.

Example

mut client := scram.new_client(username: 'user', password: 'pencil')!
assert client.mechanism_name() == 'SCRAM-SHA-256'

fn new_credentials #

fn new_credentials(mechanism Mechanism, password string) !Credentials

new_credentials derives the credentials a server stores for a new user, with a freshly generated random salt of default_salt_size bytes and default_iterations iterations.

Prepare and validate the password before calling: SASLprep for .sha1, PRECIS OpaqueString for .sha256, and the profile agreed with the peer for .sha512. This function hashes the supplied bytes unchanged.

Example

credentials := scram.new_credentials(.sha256, 'pencil')!
assert credentials.salt.len == scram.default_salt_size
assert credentials.iterations == scram.default_iterations

fn new_server #

fn new_server(config ServerConfig) !&Server

new_server creates a server for one SCRAM exchange.

Example

creds := scram.new_credentials(.sha256, 'pencil')!
mut server := scram.new_server(lookup: fn [creds] (username string) !scram.Credentials {
	return creds
})!
assert server.mechanism_name() == 'SCRAM-SHA-256'

fn parse_credentials #

fn parse_credentials(encoded string) !Credentials

parse_credentials reads back what Credentials.encode wrote, including RFC 5803 SCRAM-SHA-1 records and PostgreSQL SCRAM-SHA-256 records. Every field is validated, including the key lengths against the mechanism, so a truncated or hand-edited record is rejected rather than silently producing an account nobody can log into.

fn server_error_message #

fn server_error_message(code string) string

server_error_message renders a server-final-message that refuses the exchange, as defined by RFC 5802 §7. Common codes are invalid-proof, unknown-user and invalid-encoding; a client surfaces the value as a ServerError.

fn ChannelBindingMode.from #

fn ChannelBindingMode.from[W](input W) !ChannelBindingMode

fn ClientState.from #

fn ClientState.from[W](input W) !ClientState

fn Mechanism.from #

fn Mechanism.from[W](input W) !Mechanism

fn ServerState.from #

fn ServerState.from[W](input W) !ServerState

enum ChannelBindingMode #

enum ChannelBindingMode {
	// not_supported sends `n`: this client cannot do channel binding at all.
	not_supported
	// unsupported_by_server sends `y`: this client supports channel binding,
	// but the server did not advertise a `-PLUS` mechanism. A server that did
	// advertise one must then abort, since only a downgrade attack explains it.
	unsupported_by_server
	// required sends `p=<name>`: the exchange is bound to the channel, and
	// both sides must agree on `name` and `data`. Use the `-PLUS` mechanism
	// name in that case.
	required
}

ChannelBindingMode says how an exchange relates to the TLS channel underneath it, and becomes the GS2 cbind-flag on the wire. The distinction between .not_supported and .unsupported_by_server is not cosmetic: it is what lets a server detect an attacker stripping the -PLUS mechanisms from its advertised list.

enum Mechanism #

enum Mechanism {
	sha1
	sha256
	sha512
}

Mechanism selects the hash function of a SCRAM exchange, which is the only thing that varies between the members of the SCRAM family.

fn (Mechanism) name #

fn (m Mechanism) name() string

name returns the IANA SASL mechanism name, such as SCRAM-SHA-256.

fn (Mechanism) name_plus #

fn (m Mechanism) name_plus() string

name_plus returns the channel binding spelling of the mechanism name, such as SCRAM-SHA-256-PLUS. Advertise this name when the exchange is bound to the underlying TLS channel, i.e. when ChannelBinding.mode is .required.

fn (Mechanism) size #

fn (m Mechanism) size() int

size returns the digest length of the mechanism in bytes, which is also the length of every key and proof it produces.

struct AuthenticationFailed #

struct AuthenticationFailed {
	Error
pub:
	// reason is a short diagnostic for logs. Do not relay it to a remote peer.
	reason string
}

AuthenticationFailed means the exchange was well formed but the peer did not prove what it claimed. On the server that means a wrong password; on the client it means the server could not sign the exchange, so it does not hold the credentials it should — treat the connection as hostile rather than retrying.

It is deliberately not split into finer variants: telling a caller why authentication failed is exactly the information an attacker wants.

fn (AuthenticationFailed) msg #

fn (e &AuthenticationFailed) msg() string

msg formats an AuthenticationFailed for IError.msg().

struct ChannelBinding #

struct ChannelBinding {
pub:
	// mode selects the GS2 `cbind-flag`.
	mode ChannelBindingMode = .not_supported
	// name is the channel binding type, e.g. `tls-server-end-point`. Required
	// when `mode` is `.required`, ignored otherwise.
	name string
	// data is the binding data from the TLS layer. Required when `mode` is
	// `.required` — an empty value is refused rather than producing a `-PLUS`
	// exchange that binds nothing — and ignored otherwise.
	data []u8
}

ChannelBinding describes the channel binding of an exchange. The default value means no channel binding, which is what a client without access to its TLS layer should use.

This module never derives binding data itself: data comes from the TLS stack, and the caller passes it in. For tls-server-end-point (RFC 5929) it is the hash of the server certificate; for tls-exporter (RFC 9266) it is a TLS exporter output.

struct Client #

@[heap]
struct Client {
	mechanism       Mechanism
	username        string
	password        string
	authzid         string
	channel_binding ChannelBinding
	min_iterations  int
	max_iterations  int
	gs2_header      string
	client_nonce    string
mut:
	first_bare   string
	auth_message string
	server_key   []u8
	state        ClientState = .awaiting_first
}

Client drives the client side of one SCRAM exchange. Create it with new_client, then call first, final and verify in that order. It holds the password for the duration of the exchange and is not safe to share between threads.

fn (Client) str #

fn (c &Client) str() string

str renders a Client without its secrets. The password and the derived keys are deliberately left out: V formats structs automatically, so a println(client) while debugging would otherwise write the password to wherever the logs go.

fn (Client) mechanism_name #

fn (c &Client) mechanism_name() string

mechanism_name returns the SASL mechanism name to announce to the server, which is the -PLUS spelling when channel binding is in use.

fn (Client) done #

fn (c &Client) done() bool

done reports whether the exchange finished successfully, which is only true once verify has accepted the server-final-message.

fn (Client) first #

fn (mut c Client) first() !string

first returns the client-first-message to send to the server. It carries no secret, only the user name and the client nonce.

fn (Client) final #

fn (mut c Client) final(server_first string) !string

final consumes the server-first-message and returns the client-final-message, which carries the proof that this client knows the password. The server's salt and iteration count are validated here, so a server can neither weaken the exchange by asking for a trivial amount of work, nor stall this client by asking for an absurd amount of it.

fn (Client) verify #

fn (mut c Client) verify(server_final string) !

verify checks the server-final-message. It returns without a value when the server proved that it holds the credentials for this user, which is the point at which the connection may be trusted. A ServerError means the server refused the exchange; an AuthenticationFailed means it answered but could not sign the exchange, so it is not the server it claims to be.

struct ClientConfig #

@[params]
struct ClientConfig {
pub:
	// username is the authentication identity. Apply SASLprep (RFC 4013) before
	// passing every value, including ASCII input. This module validates UTF-8
	// and NUL but does not normalize the value. Commas and equals signs are
	// escaped on the wire.
	username string @[required]
	// password is the secret. Prepare and validate it before passing it:
	// SASLprep for `.sha1`, PRECIS OpaqueString for `.sha256`, and the profile
	// agreed with the peer for `.sha512`. The module hashes its bytes unchanged.
	// It is required so an exchange is never attempted with an empty password
	// by accident.
	password string @[required]
	// mechanism selects the hash. Prefer the default over `.sha1`, which
	// survives only for servers that offer nothing better.
	mechanism Mechanism = .sha256
	// authzid is the authorization identity, when it differs from `username`.
	// Prepare it with the application protocol's profile before passing it.
	// Leave it empty in the common case where a user authenticates as itself.
	authzid string
	// channel_binding binds the exchange to the TLS channel underneath it.
	channel_binding ChannelBinding
	// min_iterations is the smallest iteration count accepted from the server.
	// Lowering it below `default_min_iterations` weakens the protection an
	// intercepted exchange gets against an offline attack.
	min_iterations int = default_min_iterations
	// max_iterations is the largest iteration count accepted from the server.
	// Raising it lets a hostile server spend more of this client's CPU on a
	// single message; see `default_max_iterations`.
	max_iterations int = default_max_iterations
	// nonce overrides the generated client nonce. Leave it empty outside of
	// tests: a nonce that repeats across exchanges destroys replay protection.
	nonce string
}

ClientConfig configures one client-side exchange. Only username and password are mandatory; the defaults give an RFC 7677 compliant SCRAM-SHA-256 client without channel binding.

struct Credentials #

struct Credentials {
pub:
	// mechanism is the hash the keys below were derived with. Credentials are
	// not interchangeable between mechanisms.
	mechanism Mechanism
	// salt is the per-user random salt, sent to the client in cleartext.
	salt []u8
	// iterations is the PBKDF2 iteration count used to derive the keys.
	iterations int
	// stored_key is `H(HMAC(SaltedPassword, "Client Key"))`, used to check the
	// proof a client sends.
	stored_key []u8
	// server_key is `HMAC(SaltedPassword, "Server Key")`, used to sign the
	// server-final-message so the client can authenticate the server.
	server_key []u8
}

Credentials is what a SCRAM server stores for one user. It is derived from the password but does not allow recovering it, and stored_key alone is not enough to authenticate as the user — that is the whole point of the mechanism. server_key, on the other hand, does let its holder impersonate the server, so it deserves the same protection as any secret.

fn (Credentials) str #

fn (c Credentials) str() string

str renders Credentials without their key material. stored_key and server_key are secrets — server_key in particular lets its holder impersonate the server — and V formats structs automatically, so the default rendering would leak them into any log that prints a record.

fn (Credentials) encode #

fn (c Credentials) encode() string

encode renders the credentials in this one-line storage format:

"$" ":" "$" ":"

with the three binary fields in base64. For .sha1, this is the RFC 5803 authPassword scheme laid out in the syntax of RFC 3112 §2 and can interoperate with LDAP implementations of that RFC. For .sha256, it is the format PostgreSQL keeps in pg_authid.rolpassword. PostgreSQL does not accept the .sha1 or .sha512 records, and RFC 5803 does not define the .sha256 or .sha512 records for LDAP.

The result is a secret: server_key lets its holder impersonate the server to any client of this user.

Example

credentials := scram.derive_credentials(.sha256, 'pencil', 'saltsaltsaltsalt'.bytes(),
	4096)!
encoded := credentials.encode()
assert encoded.starts_with('SCRAM-SHA-256\$4096:')
assert scram.parse_credentials(encoded)!.stored_key == credentials.stored_key

struct MalformedMessage #

struct MalformedMessage {
	Error
pub:
	// reason names the check that failed.
	reason string
}

MalformedMessage means the bytes received do not form a valid SCRAM message. This is a permanent error: retrying will not help, and it usually points at a peer that speaks a different protocol or a corrupted transport.

fn (MalformedMessage) msg #

fn (e &MalformedMessage) msg() string

msg formats a MalformedMessage for IError.msg().

struct Server #

@[heap]
struct Server {
	mechanism        Mechanism
	channel_binding  ChannelBinding
	advertises_plus  bool
	server_nonce     string
	lookup           fn (username string) !Credentials = unsafe { nil }
	prepare_username fn (username string) !string      = unsafe { nil }
	prepare_authzid  fn (authzid string) !string       = unsafe { nil }
mut:
	username     string
	authzid      string
	gs2_header   string
	nonce        string
	credentials  Credentials
	auth_message string
	state        ServerState = .awaiting_client_first
}

Server drives the server side of one SCRAM exchange. Create it with new_server, then call first and final in that order. It is not safe to share between threads; use one value per connection.

fn (Server) str #

fn (s &Server) str() string

str renders a Server without its secrets, for the same reason as Client.str: the credentials it holds must not reach a log through an incidental println.

fn (Server) mechanism_name #

fn (s &Server) mechanism_name() string

mechanism_name returns the SASL mechanism name this server implements, which is the -PLUS spelling when it requires channel binding.

fn (Server) username #

fn (s &Server) username() string

username returns the authentication identity the client sent, unescaped and prepared. It is only meaningful once first has returned, and only trustworthy once final has succeeded.

fn (Server) authzid #

fn (s &Server) authzid() string

authzid returns the authorization identity the client asked for, unescaped and prepared by ServerConfig.prepare_authzid, or an empty string when it did not ask for one. Authorizing it is the application's job: SCRAM only proves who the client is, never what it may act as.

fn (Server) done #

fn (s &Server) done() bool

done reports whether the exchange finished successfully.

fn (Server) first #

fn (mut s Server) first(client_first string) !string

first consumes the client-first-message and returns the server-first-message, which carries the user's salt, the iteration count and the combined nonce.

fn (Server) final #

fn (mut s Server) final(client_final string) !string

final consumes the client-final-message, checks the client's proof and returns the server-final-message. An AuthenticationFailed here means a wrong password; answer the client with server_error_message('invalid-proof') rather than closing silently, so that it can tell a refusal from a broken connection.

struct ServerConfig #

@[params]
struct ServerConfig {
pub:
	// mechanism selects the hash. It must match the mechanism the credentials
	// returned by `lookup` were derived with.
	mechanism Mechanism = .sha256
	// channel_binding describes what *this exchange* uses. Set `mode` to
	// `.required` when the client picked the `-PLUS` mechanism, and leave it
	// at the default when it picked the base one.
	channel_binding ChannelBinding
	// advertises_plus says whether this server offers a `-PLUS` mechanism at
	// all, which is a different question from what this exchange uses: a
	// server commonly lists both `SCRAM-SHA-256` and `SCRAM-SHA-256-PLUS` and
	// lets the client choose. Set it to true whenever the `-PLUS` name is in
	// the advertised list, including on the exchanges where the client chose
	// the base mechanism — that is precisely where a stripped advertisement
	// has to be detected. `mode: .required` implies it.
	advertises_plus bool
	// nonce overrides the generated server nonce. Leave it empty outside of
	// tests.
	nonce string
	// prepare_username applies SASLprep (RFC 4013) to the authentication identity
	// before `lookup`. When omitted, the server accepts only printable ASCII,
	// whose SASLprep form is unchanged; non-ASCII identities require a callback.
	prepare_username fn (username string) !string = unsafe { nil }
	// prepare_authzid applies the application protocol's preparation profile to
	// a requested authorization identity before `authzid()` exposes it. When
	// omitted, only printable ASCII is accepted. The callback is not called when
	// the client sends no authorization identity.
	prepare_authzid fn (authzid string) !string = unsafe { nil }
	// lookup returns the credentials stored for `username`, which arrives
	// already unescaped and prepared.
	//
	// Returning an error aborts the exchange before a server challenge exists;
	// report that through the enclosing SASL protocol, not as a SCRAM
	// server-final-message. To avoid revealing which user names exist, RFC 5802
	// §7 suggests returning credentials derived from a server-side secret for
	// unknown users and continuing the exchange. That policy belongs to the
	// application, which is why it lives in this callback.
	lookup fn (username string) !Credentials @[required]
}

ServerConfig configures one server-side exchange. lookup is mandatory: it is how the server obtains the credentials it stored for a user.

struct ServerError #

struct ServerError {
	Error
pub:
	// code is the `server-error-value` sent by the server, verbatim.
	code string
}

ServerError carries the e= attribute of a server-final-message, which is how a server reports a refusal instead of signing the exchange. RFC 5802 §7 defines the values, invalid-proof and unknown-user being the common ones, but a server may send any valid extension value, so do not assume the set is closed.

fn (ServerError) msg #

fn (e &ServerError) msg() string

msg formats a ServerError for IError.msg().

struct UnsupportedMechanism #

struct UnsupportedMechanism {
	Error
pub:
	// name is the mechanism name that was not recognised.
	name string
}

UnsupportedMechanism means a mechanism name is not one this module implements. It is returned by mechanism_from_name, typically while picking a mechanism out of the list a server advertises.

fn (UnsupportedMechanism) msg #

fn (e &UnsupportedMechanism) msg() string

msg formats an UnsupportedMechanism for IError.msg().