webauthn: persist credentials to JSON file instead of in-memory map
- New credentialStore type in credentials.go loads/saves map[id]localCred to a JSON file. Thread-safe with sync.RWMutex, writes to disk on every mutation. - PasskeyHandle replaces sync.RWMutex+map with *credentialStore. Inline save/lookip/update closures delegate to store methods. - newPasskeyHandle now takes a storePath parameter and returns an error; callers updated. - New -passkey-file flag (default ./passkeys.json) configures the credential store path in main.go. - Tests use os.CreateTemp in t.TempDir() so each test gets an isolated, auto-cleaned store file.
This commit is contained in:
+11
-29
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/kami/maven/internal/ipc"
|
||||
@@ -29,8 +28,7 @@ type assertIPC interface {
|
||||
type PasskeyHandle struct {
|
||||
rp *webauthn.RP
|
||||
assertFn assertIPC // *ipc.Client when connected; nil ⇒ no step-up IPC
|
||||
mu sync.RWMutex
|
||||
creds map[string]localCred // credential ID → stored credential
|
||||
store *credentialStore
|
||||
}
|
||||
|
||||
type localCred struct {
|
||||
@@ -38,16 +36,20 @@ type localCred struct {
|
||||
SignCount int64
|
||||
}
|
||||
|
||||
func newPasskeyHandle(cfg webauthn.Config, core ipc.CoreAPI) *PasskeyHandle {
|
||||
func newPasskeyHandle(cfg webauthn.Config, core ipc.CoreAPI, storePath string) (*PasskeyHandle, error) {
|
||||
var af assertIPC
|
||||
if c, ok := core.(assertIPC); ok {
|
||||
af = c
|
||||
}
|
||||
store, err := newCredentialStore(storePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("credential store: %w", err)
|
||||
}
|
||||
return &PasskeyHandle{
|
||||
rp: webauthn.NewRP(cfg),
|
||||
assertFn: af,
|
||||
creds: make(map[string]localCred),
|
||||
}
|
||||
store: store,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Page serves the passkey enrollment + step-up UI. It's the only surface that
|
||||
@@ -124,13 +126,7 @@ func (h *PasskeyHandle) RegisterFinish(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
save := func(id string, publicKey []byte, _ []byte, _ string) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if _, exists := h.creds[id]; exists {
|
||||
return fmt.Errorf("credential already exists")
|
||||
}
|
||||
h.creds[id] = localCred{PublicKey: publicKey}
|
||||
return nil
|
||||
return h.store.Save(id, publicKey)
|
||||
}
|
||||
credID, err := h.rp.FinishRegistration(save, body.Challenge, body.Credential)
|
||||
if err != nil {
|
||||
@@ -168,24 +164,10 @@ func (h *PasskeyHandle) AssertFinish(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
lookup := func(id string) ([]byte, int64, error) {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
cred, ok := h.creds[id]
|
||||
if !ok {
|
||||
return nil, 0, fmt.Errorf("credential not found")
|
||||
}
|
||||
return cred.PublicKey, cred.SignCount, nil
|
||||
return h.store.Lookup(id)
|
||||
}
|
||||
update := func(id string, count int64) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
cred, ok := h.creds[id]
|
||||
if !ok {
|
||||
return fmt.Errorf("credential not found")
|
||||
}
|
||||
cred.SignCount = count
|
||||
h.creds[id] = cred
|
||||
return nil
|
||||
return h.store.UpdateSignCount(id, count)
|
||||
}
|
||||
|
||||
credID, err := h.rp.FinishAssertion(lookup, update, body.Challenge, body.Credential)
|
||||
|
||||
Reference in New Issue
Block a user