Skip to content
quarry Artifact [035e3730c8]
Login

Artifact [035e3730c8]

Artifact 035e3730c8c6bb28021665bc5ca939d686a178683424ffd5f9df0f83c897ce80:


// 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 (
	"encoding/base32"
	"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)
}

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

// MintSyncToken adds a push token named for the day and returns it the
// ONLY time it is ever readable. It adds rather than replaces (RFC
// 0021): replacing is what broke one session's pushes whenever another
// session on the same account minted.
func (d *DB) MintSyncToken(user string) (string, error) {
	value, _, err := d.MintToken(user, TokenSpec{
		Name:   "push " + time.Now().UTC().Format("2006-01-02"),
		Scopes: []string{ScopeSync},
	})
	return value, err
}

// RevokeSyncToken revokes every live sync-scoped token of the account.
// Pushes stop at once, because nothing is cached: every push unseals
// afresh.
//
// Every unrevoked sync-scoped row, expired ones included: "revoke all"
// leaves nothing for the list to call live, now or after a clock moves.
func (d *DB) RevokeSyncToken(user string) error {
	toks, err := d.Tokens(user)
	if err != nil {
		return err
	}
	for _, t := range toks {
		if t.Revoked == "" && t.Has(ScopeSync) {
			if err := d.RevokeToken(user, t.ID); err != nil {
				return err
			}
		}
	}
	return nil
}

// SyncTokenIssued reports when the account's newest live push token
// was minted, without revealing it: that a credential exists and how
// old it is, never its value.
func (d *DB) SyncTokenIssued(user string) (string, bool) {
	toks := d.liveSyncTokens(user, time.Now())
	if len(toks) == 0 {
		return "", false
	}
	newest := toks[0].Created
	for _, t := range toks[1:] {
		if t.Created > newest {
			newest = t.Created
		}
	}
	return newest, true
}

// VerifySyncToken reports whether token is one of user's live push
// tokens.
//
// This is the credential a sync client already holds; it signs the
// login card on every round after the first. A clone's FIRST round
// carries no card — the client cannot sign one before the server has
// told it the project code — so both fossil and zit fall back to HTTP
// Basic, and the only thing they have to put in Basic's password slot
// is this token. The read lane therefore has to recognise it there, or
// a private repository stays unclonable by the token its owner minted
// for exactly that (2bb5659e).
func (d *DB) VerifySyncToken(user, token string) bool {
	_, ok := d.ResolveToken(user, token, ScopeSync)
	return ok
}