Add KeepOrg sync server: accounts, nested groups, versioned KDBX sync

Why: collaborative KeePass use needs one arbiter of "who wrote last" that
never sees a master password, plus access control that follows an org chart.

What: stdlib-only Go server. Metadata is a single JSON document with one
writer and atomic commit; KDBX payloads are versioned blobs on disk. Access
comes from a group tree where a grant on a group reaches every member below
it. Login is password+TOTP, invite links, or OIDC through a browser-approved
device flow. Pushes carry If-Match and are arbitrated by a per-database merge
strategy (server-wins by default). Updates are pushed to clients over SSE.

Proved: go test ./internal/... covers RFC 6238 vectors, ACL inheritance and
isolation, store rollback, blob pruning, every push verdict, and an
end-to-end HTTP pass over login, conflict, force, read-only grants, invite
redemption, TOTP enrolment, device login and the event stream.
This commit is contained in:
2026-10-29 15:54:10 +01:00
commit 8bbb3179a1
26 changed files with 3897 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
// Package totp implements RFC 6238 time-based one-time passwords, the
// built-in second factor for password logins.
package totp
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha1"
"encoding/base32"
"encoding/binary"
"fmt"
"net/url"
"strings"
"time"
)
const (
digits = 6
step = 30 * time.Second
// skew is how many steps either side of now are accepted, covering
// clock drift and a user typing slowly.
skew = 1
)
// NewSecret returns a fresh base32 secret with no padding, as authenticator
// apps expect.
func NewSecret() string {
b := make([]byte, 20)
if _, err := rand.Read(b); err != nil {
panic("totp: " + err.Error())
}
return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(b)
}
// URI is the otpauth:// URL an authenticator app can scan.
func URI(issuer, account, secret string) string {
label := url.PathEscape(issuer + ":" + account)
q := url.Values{"secret": {secret}, "issuer": {issuer}, "algorithm": {"SHA1"},
"digits": {fmt.Sprint(digits)}, "period": {"30"}}
return "otpauth://totp/" + label + "?" + q.Encode()
}
// Code is the password for secret at time t.
func Code(secret string, t time.Time) (string, error) {
key, err := decode(secret)
if err != nil {
return "", err
}
return code(key, uint64(t.Unix())/uint64(step.Seconds())), nil
}
// Check reports whether given is a valid password for secret at time t,
// allowing one step of drift either way.
func Check(secret, given string, t time.Time) bool {
key, err := decode(secret)
if err != nil {
return false
}
given = strings.TrimSpace(given)
if len(given) != digits {
return false
}
ctr := uint64(t.Unix()) / uint64(step.Seconds())
for d := -skew; d <= skew; d++ {
if hmac.Equal([]byte(code(key, ctr+uint64(d))), []byte(given)) {
return true
}
}
return false
}
func decode(secret string) ([]byte, error) {
s := strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(secret), " ", ""))
return base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(s)
}
func code(key []byte, counter uint64) string {
var buf [8]byte
binary.BigEndian.PutUint64(buf[:], counter)
mac := hmac.New(sha1.New, key)
mac.Write(buf[:])
sum := mac.Sum(nil)
off := sum[len(sum)-1] & 0x0f
trunc := binary.BigEndian.Uint32(sum[off:off+4]) & 0x7fffffff
mod := uint32(1)
for i := 0; i < digits; i++ {
mod *= 10
}
return fmt.Sprintf("%0*d", digits, trunc%mod)
}