172 lines
3.4 KiB
Go
172 lines
3.4 KiB
Go
|
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"))
|
||
|
}
|