Skip to content
quarry Artifact [7e9a240773]
Login

Artifact [7e9a240773]

Artifact 7e9a2407738874290e5afb756c87540b86d6a42ffdbc2208a05f0481a58033e6:


// synctoken.go is the push credential a person can actually hold.
//
// The sync protocol needs the server to recompute
// SHA1(project-code/user/password) for whatever the client types into
// its remote (RFC §6.3). For a while this forge derived that from the
// account password, which was wrong twice over: it planted a fast-hash
// derivative of everybody's password in a database whose whole point
// is that argon2id makes a stolen copy worthless, and it silently
// evicted any purpose-made credential the moment its owner signed in.
//
// So the value behind a push is now a random token, minted on request,
// shown once, revocable on its own, and belonging to no other system.
// It is kept SEALED rather than derived-and-forgotten (secret.go),
// which is the one deliberate trade here: a reversible secret at rest,
// scoped to push, in exchange for deriving per-repository secrets on
// demand. That is what ends the staleness — a repository created after
// the token was minted, or a grant made this morning, needs no
// re-issue, no second sign-in and no admin. A credential that quietly
// goes out of date is the exact shape that locked this forge's
// twenty-three repositories away from their own owners.
package oltp

import (
	"crypto/rand"
	"database/sql"
	"encoding/base32"
	"errors"
	"strings"
	"time"

	"craftdesign.group/quarry/internal/xfer"
)

// syncSecret is RFC §6.3's derivation, named once here so the store and
// the protocol cannot drift apart.
func syncSecret(projectCode, user, token string) string {
	return xfer.SharedSecret(projectCode, user, token)
}

const syncTokenSchema = `
CREATE TABLE IF NOT EXISTS sync_token (
	user    TEXT PRIMARY KEY,
	sealed  TEXT NOT NULL, -- the token itself, sealed (secret.go)
	created TEXT NOT NULL
);`

// syncTokenAlphabet is Crockford-ish base32 without padding: no case
// question, and none of the pairs a person mistypes when copying a
// credential out of a page and into a terminal.
var syncTokenAlphabet = base32.NewEncoding("0123456789ABCDEFGHJKMNPQRSTVWXYZ").WithPadding(base32.NoPadding)

// newSyncToken returns 160 bits in groups of five, which is enough that
// the derived SHA1 is not worth attacking and short enough to paste.
func newSyncToken() (string, error) {
	raw := make([]byte, 20)
	if _, err := rand.Read(raw); err != nil {
		return "", err
	}
	s := syncTokenAlphabet.EncodeToString(raw)
	var b strings.Builder
	for i, r := range s {
		if i > 0 && i%4 == 0 {
			b.WriteByte('-')
		}
		b.WriteRune(r)
	}
	return b.String(), nil
}

// MintSyncToken issues a push token, replacing any the account holds,
// and returns it the ONLY time it is ever readable.
func (d *DB) MintSyncToken(user string) (string, error) {
	if user == "" {
		return "", errors.New("oltp: empty account name")
	}
	token, err := newSyncToken()
	if err != nil {
		return "", err
	}
	sealed, err := d.seal.seal(token)
	if err != nil {
		return "", err
	}
	if _, err := d.db.Exec(`INSERT OR REPLACE INTO sync_token (user, sealed, created)
		VALUES (?, ?, ?)`, user, sealed, time.Now().UTC().Format(time.RFC3339)); err != nil {
		return "", err
	}
	return token, nil
}

// RevokeSyncToken drops the account's push token. Pushes stop at once,
// because nothing is cached: every push unseals afresh.
func (d *DB) RevokeSyncToken(user string) error {
	_, err := d.db.Exec(`DELETE FROM sync_token WHERE user = ?`, user)
	return err
}

// SyncTokenIssued reports when the account's token was minted, without
// revealing it. This is what a page shows: that a credential exists and
// how old it is, never its value.
func (d *DB) SyncTokenIssued(user string) (string, bool) {
	var created string
	err := d.db.QueryRow(`SELECT created FROM sync_token WHERE user = ?`, user).Scan(&created)
	return created, err == nil
}

// syncToken unseals the account's push token, or reports that it holds
// none.
func (d *DB) syncToken(user string) (string, bool) {
	var sealed string
	err := d.db.QueryRow(`SELECT sealed FROM sync_token WHERE user = ?`, user).Scan(&sealed)
	if errors.Is(err, sql.ErrNoRows) || err != nil {
		return "", false
	}
	token, err := d.seal.open(sealed)
	if err != nil {
		return "", false
	}
	return token, true
}