Sessions are stored as hashes, so a database dump contains nothing that can be pasted into a browser. A login that has passed the password but not the second factor is modelled as a pending session: it expires, is revocable, and reaches exactly one endpoint. Signup is invite-only and takes the email address from the invite rather than the form, so a link cannot be redirected to somebody else. The first admin is created by an endpoint that refuses once any account exists, checked and inserted in one transaction because it is necessarily unauthenticated. Login failures and forgotten-password requests answer identically whether or not the address has an account — otherwise the form is an account-existence oracle. Password rules are a length floor and nothing else: composition rules push people towards worse passwords. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
108 lines
3.2 KiB
Go
108 lines
3.2 KiB
Go
package store
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// NewID returns a UUIDv7: 48 bits of Unix milliseconds followed by randomness.
|
|
//
|
|
// Time-ordered ids matter more here than they look. SQLite stores rows in a B-tree
|
|
// keyed by the primary key, so random ids scatter inserts across the whole file and
|
|
// every new task dirties a different page. Sequential ids append. It also means
|
|
// `ORDER BY id` is creation order, which removes a column and an index from every
|
|
// table that would otherwise need one.
|
|
//
|
|
// Implemented directly rather than through a dependency: it is twenty lines, and the
|
|
// monotonic counter below is the only part with any subtlety.
|
|
func NewID() string {
|
|
var b [16]byte
|
|
|
|
ms, seq := nextTimestamp()
|
|
// 48-bit big-endian millisecond timestamp.
|
|
b[0] = byte(ms >> 40)
|
|
b[1] = byte(ms >> 32)
|
|
b[2] = byte(ms >> 24)
|
|
b[3] = byte(ms >> 16)
|
|
b[4] = byte(ms >> 8)
|
|
b[5] = byte(ms)
|
|
|
|
// Version 7 in the high nibble of byte 6, then 12 bits of sequence. The
|
|
// sequence is what keeps ids ordered *within* a millisecond — without it, two
|
|
// tasks created in the same tick sort by their random bits, and a list created
|
|
// by one fast paste comes back shuffled.
|
|
b[6] = 0x70 | byte(seq>>8)
|
|
b[7] = byte(seq)
|
|
|
|
if _, err := rand.Read(b[8:]); err != nil {
|
|
// crypto/rand does not fail on any supported platform; if it somehow
|
|
// does, continuing would silently produce guessable ids.
|
|
panic(fmt.Sprintf("store: read random for id: %v", err))
|
|
}
|
|
// Variant bits: 10xx in the top of byte 8.
|
|
b[8] = (b[8] & 0x3f) | 0x80
|
|
|
|
return hex.EncodeToString(b[:4]) + "-" +
|
|
hex.EncodeToString(b[4:6]) + "-" +
|
|
hex.EncodeToString(b[6:8]) + "-" +
|
|
hex.EncodeToString(b[8:10]) + "-" +
|
|
hex.EncodeToString(b[10:])
|
|
}
|
|
|
|
var idClock struct {
|
|
sync.Mutex
|
|
lastMS uint64
|
|
seq uint16
|
|
}
|
|
|
|
// nextTimestamp returns the current millisecond and a counter that increments for
|
|
// repeated calls inside the same one, so ids issued in a burst stay ordered.
|
|
func nextTimestamp() (uint64, uint16) {
|
|
idClock.Lock()
|
|
defer idClock.Unlock()
|
|
|
|
ms := uint64(time.Now().UnixMilli())
|
|
switch {
|
|
case ms > idClock.lastMS:
|
|
idClock.lastMS, idClock.seq = ms, 0
|
|
case idClock.seq < 0xfff:
|
|
idClock.seq++
|
|
default:
|
|
// More than 4096 ids in one millisecond: borrow from the next. The clock
|
|
// value stays ahead of real time until it catches up, which keeps ids
|
|
// unique and ordered rather than wrapping the counter.
|
|
idClock.lastMS++
|
|
idClock.seq = 0
|
|
ms = idClock.lastMS
|
|
}
|
|
return ms, idClock.seq
|
|
}
|
|
|
|
// IDTime recovers the moment an id was generated. Useful in diagnostics, and the
|
|
// reason a created_at column can be trusted against its own row's id.
|
|
func IDTime(id string) (time.Time, bool) {
|
|
clean := make([]byte, 0, 32)
|
|
for i := 0; i < len(id); i++ {
|
|
if id[i] != '-' {
|
|
clean = append(clean, id[i])
|
|
}
|
|
}
|
|
if len(clean) != 32 {
|
|
return time.Time{}, false
|
|
}
|
|
decoded := make([]byte, 16)
|
|
if _, err := hex.Decode(decoded, clean); err != nil {
|
|
return time.Time{}, false
|
|
}
|
|
if decoded[6]>>4 != 7 {
|
|
return time.Time{}, false
|
|
}
|
|
ms := uint64(binary.BigEndian.Uint16(decoded[0:2]))<<32 |
|
|
uint64(binary.BigEndian.Uint32(decoded[2:6]))
|
|
return time.UnixMilli(int64(ms)), true
|
|
}
|