333 lines
5.9 KiB
Go
333 lines
5.9 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
restful "github.com/emicklei/go-restful"
|
|
"m.cluseau.fr/go/httperr"
|
|
|
|
"novit.tech/direktil/local-server/secretstore"
|
|
)
|
|
|
|
var secStore *secretstore.Store
|
|
|
|
func secStoreRoot() string { return filepath.Join(*dataDir, "secrets") }
|
|
func secStorePath(name string) string { return filepath.Join(secStoreRoot(), 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) (err httperr.Error) {
|
|
unlockMutex.Lock()
|
|
defer unlockMutex.Unlock()
|
|
|
|
if secStore.Unlocked() {
|
|
return ErrStoreAlreadyUnlocked
|
|
}
|
|
|
|
if secStore.IsNew() {
|
|
err := secStore.Init(passphrase)
|
|
if err != nil {
|
|
return httperr.Internal(err)
|
|
}
|
|
|
|
err = secStore.SaveTo(secKeysStorePath())
|
|
if err != nil {
|
|
log.Print("secret store save error: ", err)
|
|
secStore.Close()
|
|
|
|
return httperr.Internal(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.Internal(err)
|
|
}
|
|
|
|
token, err = newToken(32)
|
|
if err != nil {
|
|
secStore.Close()
|
|
return httperr.Internal(err)
|
|
}
|
|
|
|
err = writeSecret("admin-token", token)
|
|
if err != nil {
|
|
log.Print("write error: ", err)
|
|
secStore.Close()
|
|
|
|
return httperr.Internal(err)
|
|
}
|
|
|
|
log.Print("wrote new admin token")
|
|
}
|
|
|
|
*adminToken = token
|
|
|
|
{
|
|
token, err := newToken(16)
|
|
if err != nil {
|
|
secStore.Close()
|
|
return httperr.Internal(err)
|
|
}
|
|
|
|
wState.Change(func(v *State) {
|
|
v.Store.DownloadToken = token
|
|
})
|
|
}
|
|
|
|
wPublicState.Change(func(v *PublicState) {
|
|
v.Store.New = false
|
|
v.Store.Open = true
|
|
})
|
|
|
|
go updateState()
|
|
go migrateSecrets()
|
|
|
|
return
|
|
}
|
|
|
|
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) {
|
|
path := secStorePath(name + ".data.new")
|
|
|
|
if err = os.MkdirAll(filepath.Dir(path), 0700); err != nil {
|
|
return
|
|
}
|
|
|
|
f, err := os.Create(path)
|
|
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
|
|
}
|
|
|
|
err = os.Rename(f.Name(), secStorePath(name+".data"))
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
go updateState()
|
|
return
|
|
}
|
|
|
|
var secL sync.Mutex
|
|
|
|
func updateSecret[T any](name string, update func(*T)) (err error) {
|
|
secL.Lock()
|
|
defer secL.Unlock()
|
|
|
|
v := new(T)
|
|
err = readSecret(name, v)
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return
|
|
}
|
|
err = nil
|
|
}
|
|
|
|
update(v)
|
|
|
|
return writeSecret(name, *v)
|
|
}
|
|
|
|
func updateSecretWithKey[T any](name, key string, update func(v *T)) (err error) {
|
|
secL.Lock()
|
|
defer secL.Unlock()
|
|
|
|
kvs := map[string]*T{}
|
|
|
|
err = readSecret(name, &kvs)
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return
|
|
}
|
|
err = nil
|
|
}
|
|
|
|
update(kvs[key])
|
|
|
|
return writeSecret(name, kvs)
|
|
}
|
|
|
|
type KVSecrets[T any] struct{ Name string }
|
|
|
|
func (s KVSecrets[T]) Data() (kvs map[string]T, err error) {
|
|
kvs = make(map[string]T)
|
|
err = readSecret(s.Name, &kvs)
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return
|
|
}
|
|
err = nil
|
|
}
|
|
return
|
|
}
|
|
|
|
func (s KVSecrets[T]) Keys(prefix string) (keys []string, err error) {
|
|
kvs, err := s.Data()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
keys = make([]string, 0, len(kvs))
|
|
|
|
for k := range kvs {
|
|
if !strings.HasPrefix(k, prefix) {
|
|
continue
|
|
}
|
|
keys = append(keys, k[len(prefix):])
|
|
}
|
|
|
|
sort.Strings(keys)
|
|
|
|
return
|
|
}
|
|
|
|
func (s KVSecrets[T]) Get(key string) (v T, found bool, err error) {
|
|
kvs, err := s.Data()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
v, found = kvs[key]
|
|
return
|
|
}
|
|
|
|
func (s KVSecrets[T]) Put(key string, v T) (err error) {
|
|
secL.Lock()
|
|
defer secL.Unlock()
|
|
|
|
kvs, err := s.Data()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
kvs[key] = v
|
|
err = writeSecret(s.Name, kvs)
|
|
return
|
|
}
|
|
|
|
func (s KVSecrets[T]) WsList(resp *restful.Response, prefix string) {
|
|
keys, err := s.Keys(prefix)
|
|
if err != nil {
|
|
httperr.New(http.StatusInternalServerError, err).WriteJSON(resp.ResponseWriter)
|
|
return
|
|
}
|
|
|
|
resp.WriteEntity(keys)
|
|
}
|
|
|
|
func (s KVSecrets[T]) WsGet(resp *restful.Response, key string) {
|
|
keys, found, err := s.Get(key)
|
|
if err != nil {
|
|
httperr.New(http.StatusInternalServerError, err).WriteJSON(resp.ResponseWriter)
|
|
return
|
|
}
|
|
|
|
if !found {
|
|
ErrNotFound.WriteJSON(resp.ResponseWriter)
|
|
return
|
|
}
|
|
|
|
resp.WriteEntity(keys)
|
|
}
|
|
|
|
func (s KVSecrets[T]) WsPut(req *restful.Request, resp *restful.Response, key string) {
|
|
v := new(T)
|
|
err := req.ReadEntity(v)
|
|
if err != nil {
|
|
httperr.New(http.StatusBadRequest, err).WriteJSON(resp.ResponseWriter)
|
|
return
|
|
}
|
|
|
|
err = s.Put(key, *v)
|
|
if err != nil {
|
|
httperr.New(http.StatusInternalServerError, err).WriteJSON(resp.ResponseWriter)
|
|
return
|
|
}
|
|
}
|