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:
@@ -0,0 +1,77 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type credentialStore struct {
|
||||
mu sync.RWMutex
|
||||
path string
|
||||
creds map[string]localCred
|
||||
}
|
||||
|
||||
func newCredentialStore(path string) (*credentialStore, error) {
|
||||
cs := &credentialStore{
|
||||
path: path,
|
||||
creds: make(map[string]localCred),
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return cs, nil
|
||||
}
|
||||
return nil, fmt.Errorf("read %s: %w", path, err)
|
||||
}
|
||||
if len(data) > 0 {
|
||||
if err := json.Unmarshal(data, &cs.creds); err != nil {
|
||||
return nil, fmt.Errorf("parse %s: %w", path, err)
|
||||
}
|
||||
}
|
||||
return cs, nil
|
||||
}
|
||||
|
||||
func (cs *credentialStore) persist() error {
|
||||
data, err := json.MarshalIndent(cs.creds, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(cs.path, data, 0600); err != nil {
|
||||
return fmt.Errorf("write %s: %w", cs.path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cs *credentialStore) Save(id string, publicKey []byte) error {
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
if _, exists := cs.creds[id]; exists {
|
||||
return fmt.Errorf("credential already exists")
|
||||
}
|
||||
cs.creds[id] = localCred{PublicKey: publicKey}
|
||||
return cs.persist()
|
||||
}
|
||||
|
||||
func (cs *credentialStore) Lookup(id string) (publicKey []byte, signCount int64, err error) {
|
||||
cs.mu.RLock()
|
||||
defer cs.mu.RUnlock()
|
||||
cred, ok := cs.creds[id]
|
||||
if !ok {
|
||||
return nil, 0, fmt.Errorf("credential not found")
|
||||
}
|
||||
return cred.PublicKey, cred.SignCount, nil
|
||||
}
|
||||
|
||||
func (cs *credentialStore) UpdateSignCount(id string, count int64) error {
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
cred, ok := cs.creds[id]
|
||||
if !ok {
|
||||
return fmt.Errorf("credential not found")
|
||||
}
|
||||
cred.SignCount = count
|
||||
cs.creds[id] = cred
|
||||
return cs.persist()
|
||||
}
|
||||
Reference in New Issue
Block a user