package main import ( "crypto/rand" "encoding/base32" "encoding/json" "log" "net/http" "os" "path/filepath" "sync" "m.cluseau.fr/go/httperr" "novit.tech/direktil/local-server/secretstore" ) var secStore *secretstore.Store func secStorePath(name string) string { return filepath.Join(*dataDir, "secrets", name) } func secKeysStorePath() string { return secStorePath(".keys") } func openSecretStore() { var err error keysPath := secKeysStorePath() if err := os.MkdirAll(filepath.Dir(filepath.Dir(keysPath)), 0755); err != nil { log.Fatal("failed to create dirs: ", err) } if err := os.MkdirAll(filepath.Dir(keysPath), 0700); err != nil { log.Fatal("failed to secret store dir: ", err) } secStore, err = secretstore.Open(keysPath) switch { case err == nil: wPublicState.Change(func(v *PublicState) { v.Store.New = false v.Store.Open = false }) case os.IsNotExist(err): secStore = secretstore.New() wPublicState.Change(func(v *PublicState) { v.Store.New = true v.Store.Open = false }) default: log.Fatal("failed to open keys store: ", err) } } var ( unlockMutex = sync.Mutex{} ErrStoreAlreadyUnlocked = httperr.NewStd(http.StatusConflict, 1, "store already unlocked") ErrInvalidPassphrase = httperr.NewStd(http.StatusBadRequest, 2, "invalid passphrase") ) func unlockSecretStore(passphrase []byte) *httperr.Error { unlockMutex.Lock() defer unlockMutex.Unlock() if secStore.Unlocked() { return ErrStoreAlreadyUnlocked } if secStore.IsNew() { err := secStore.Init(passphrase) if err != nil { return httperr.New(http.StatusInternalServerError, err) } err = secStore.SaveTo(secKeysStorePath()) if err != nil { log.Print("secret store save error: ", err) secStore.Close() return httperr.New(http.StatusInternalServerError, err) } } else { if !secStore.Unlock([]byte(passphrase)) { return ErrInvalidPassphrase } } token := "" if err := readSecret("admin-token", &token); err != nil { if !os.IsNotExist(err) { log.Print("failed to read admin token: ", err) secStore.Close() return httperr.New(http.StatusInternalServerError, err) } randBytes := make([]byte, 32) _, err := rand.Read(randBytes) if err != nil { log.Print("rand read error: ", err) secStore.Close() return httperr.New(http.StatusInternalServerError, err) } token = base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(randBytes) err = writeSecret("admin-token", token) if err != nil { log.Print("write error: ", err) secStore.Close() return httperr.New(http.StatusInternalServerError, err) } log.Print("wrote new admin token") } *adminToken = token wPublicState.Change(func(v *PublicState) { v.Store.New = false v.Store.Open = true }) go updateState() return nil } func readSecret(name string, value any) (err error) { f, err := os.Open(secStorePath(name + ".data")) if err != nil { return } defer f.Close() in, err := secStore.NewReader(f) if err != nil { return } return json.NewDecoder(in).Decode(value) } func writeSecret(name string, value any) (err error) { f, err := os.Create(secStorePath(name + ".data.new")) if err != nil { return } err = func() (err error) { defer f.Close() out, err := secStore.NewWriter(f) if err != nil { return } return json.NewEncoder(out).Encode(value) }() if err != nil { return } return os.Rename(f.Name(), secStorePath(name+".data")) }