named passphrases (+deletion by name)
This commit is contained in:
parent
34afe03818
commit
ee5629643c
@ -26,7 +26,7 @@ func authorizeToken(r *http.Request, token string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func forbidden(w http.ResponseWriter, r *http.Request) {
|
func forbidden(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Printf("denied access to %s from %s", r.RequestURI, r.RemoteAddr)
|
log.Printf("denied access to %s from %s", r.URL.Path, r.RemoteAddr)
|
||||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,12 +52,12 @@ func main() {
|
|||||||
}
|
}
|
||||||
if autoUnlock != "" {
|
if autoUnlock != "" {
|
||||||
log.Printf("auto-unlocking the store")
|
log.Printf("auto-unlocking the store")
|
||||||
err := unlockSecretStore([]byte(autoUnlock))
|
err := unlockSecretStore("test", []byte(autoUnlock))
|
||||||
if err.Any() {
|
if err.Any() {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Print("store auto-unlocked, admin token is ", adminToken)
|
log.Print("store auto-unlocked, token is ", adminToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Setenv("DLS_AUTO_UNLOCK", "")
|
os.Setenv("DLS_AUTO_UNLOCK", "")
|
||||||
|
@ -63,7 +63,7 @@ var (
|
|||||||
ErrInvalidPassphrase = httperr.NewStd(2, http.StatusBadRequest, "invalid passphrase")
|
ErrInvalidPassphrase = httperr.NewStd(2, http.StatusBadRequest, "invalid passphrase")
|
||||||
)
|
)
|
||||||
|
|
||||||
func unlockSecretStore(passphrase []byte) (err httperr.Error) {
|
func unlockSecretStore(name string, passphrase []byte) (err httperr.Error) {
|
||||||
unlockMutex.Lock()
|
unlockMutex.Lock()
|
||||||
defer unlockMutex.Unlock()
|
defer unlockMutex.Unlock()
|
||||||
|
|
||||||
@ -72,7 +72,7 @@ func unlockSecretStore(passphrase []byte) (err httperr.Error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if secStore.IsNew() {
|
if secStore.IsNew() {
|
||||||
err := secStore.Init(passphrase)
|
err := secStore.Init(name, passphrase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperr.Internal(err)
|
return httperr.Internal(err)
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ type State struct {
|
|||||||
|
|
||||||
Store struct {
|
Store struct {
|
||||||
DownloadToken string
|
DownloadToken string
|
||||||
|
KeyNames []string
|
||||||
}
|
}
|
||||||
|
|
||||||
Clusters []ClusterState
|
Clusters []ClusterState
|
||||||
@ -59,14 +60,21 @@ func init() {
|
|||||||
func updateState() {
|
func updateState() {
|
||||||
log.Print("updating state")
|
log.Print("updating state")
|
||||||
|
|
||||||
|
// store key names
|
||||||
|
keyNames := make([]string, 0, len(secStore.Keys))
|
||||||
|
for _, key := range secStore.Keys {
|
||||||
|
keyNames = append(keyNames, key.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// config
|
||||||
cfg, err := readConfig()
|
cfg, err := readConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wState.Change(func(v *State) { v.HasConfig = false; v.Config = nil })
|
wState.Change(func(v *State) { v.HasConfig = false; v.Config = nil; v.Store.KeyNames = keyNames })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if secStore.IsNew() || !secStore.Unlocked() {
|
if secStore.IsNew() || !secStore.Unlocked() {
|
||||||
wState.Change(func(v *State) { v.HasConfig = false; v.Config = nil })
|
wState.Change(func(v *State) { v.HasConfig = false; v.Config = nil; v.Store.KeyNames = keyNames })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,7 +130,7 @@ func updateState() {
|
|||||||
// done
|
// done
|
||||||
wState.Change(func(v *State) {
|
wState.Change(func(v *State) {
|
||||||
v.HasConfig = true
|
v.HasConfig = true
|
||||||
//v.Config = cfg
|
v.Store.KeyNames = keyNames
|
||||||
v.Clusters = clusters
|
v.Clusters = clusters
|
||||||
v.Hosts = hosts
|
v.Hosts = hosts
|
||||||
})
|
})
|
||||||
|
@ -14,15 +14,32 @@ import (
|
|||||||
"m.cluseau.fr/go/httperr"
|
"m.cluseau.fr/go/httperr"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type NamedPassphrase struct {
|
||||||
|
Name string
|
||||||
|
Passphrase []byte
|
||||||
|
}
|
||||||
|
|
||||||
func wsUnlockStore(req *restful.Request, resp *restful.Response) {
|
func wsUnlockStore(req *restful.Request, resp *restful.Response) {
|
||||||
var passphrase string
|
np := NamedPassphrase{}
|
||||||
err := req.ReadEntity(&passphrase)
|
err := req.ReadEntity(&np)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.WriteError(http.StatusBadRequest, err)
|
resp.WriteError(http.StatusBadRequest, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := unlockSecretStore([]byte(passphrase)); err.Any() {
|
if secStore.IsNew() {
|
||||||
|
if len(np.Name) == 0 {
|
||||||
|
wsBadRequest(resp, "no name given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(np.Passphrase) == 0 {
|
||||||
|
wsBadRequest(resp, "no passphrase given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := unlockSecretStore(np.Name, np.Passphrase); err.Any() {
|
||||||
err.WriteJSON(resp.ResponseWriter)
|
err.WriteJSON(resp.ResponseWriter)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,77 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
restful "github.com/emicklei/go-restful"
|
restful "github.com/emicklei/go-restful"
|
||||||
|
"novit.tech/direktil/local-server/secretstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
func wsStoreAddKey(req *restful.Request, resp *restful.Response) {
|
func wsStoreAddKey(req *restful.Request, resp *restful.Response) {
|
||||||
var passphrase string
|
np := NamedPassphrase{}
|
||||||
|
|
||||||
err := req.ReadEntity(&passphrase)
|
err := req.ReadEntity(&np)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wsBadRequest(resp, err.Error())
|
wsBadRequest(resp, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(passphrase) == 0 {
|
np.Name = strings.TrimSpace(np.Name)
|
||||||
|
|
||||||
|
if len(np.Name) == 0 {
|
||||||
|
wsBadRequest(resp, "no name given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(np.Passphrase) == 0 {
|
||||||
wsBadRequest(resp, "no passphrase given")
|
wsBadRequest(resp, "no passphrase given")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
secStore.AddKey([]byte(passphrase))
|
secStore.AddKey(np.Name, np.Passphrase)
|
||||||
|
defer updateState()
|
||||||
|
|
||||||
|
for _, k := range secStore.Keys {
|
||||||
|
if k.Name == np.Name {
|
||||||
|
wsBadRequest(resp, "there's already a passphrase named "+strconv.Quote(np.Name))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = secStore.SaveTo(secKeysStorePath())
|
||||||
|
if err != nil {
|
||||||
|
wsError(resp, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func wsStoreDelKey(req *restful.Request, resp *restful.Response) {
|
||||||
|
name := ""
|
||||||
|
|
||||||
|
err := req.ReadEntity(&name)
|
||||||
|
if err != nil {
|
||||||
|
wsBadRequest(resp, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newKeys := make([]secretstore.KeyEntry, 0, len(secStore.Keys))
|
||||||
|
for _, k := range secStore.Keys {
|
||||||
|
if k.Name == name {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
newKeys = append(newKeys, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newKeys) == 0 {
|
||||||
|
wsBadRequest(resp, "can't remove the last key from the store")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
secStore.Keys = newKeys
|
||||||
|
defer updateState()
|
||||||
|
|
||||||
err = secStore.SaveTo(secKeysStorePath())
|
err = secStore.SaveTo(secKeysStorePath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wsError(resp, err)
|
wsError(resp, err)
|
||||||
|
@ -27,7 +27,7 @@ func registerWS(rest *restful.Container) {
|
|||||||
Produces(mime.JSON).
|
Produces(mime.JSON).
|
||||||
Consumes(mime.JSON).
|
Consumes(mime.JSON).
|
||||||
Route(ws.POST("/unlock-store").To(wsUnlockStore).
|
Route(ws.POST("/unlock-store").To(wsUnlockStore).
|
||||||
Reads("").
|
Reads(NamedPassphrase{}).
|
||||||
Writes("").
|
Writes("").
|
||||||
Doc("Try to unlock the store")).
|
Doc("Try to unlock the store")).
|
||||||
Route(ws.GET("/store.tar").To(wsStoreDownload).
|
Route(ws.GET("/store.tar").To(wsStoreDownload).
|
||||||
@ -54,8 +54,11 @@ func registerWS(rest *restful.Container) {
|
|||||||
|
|
||||||
// - store management
|
// - store management
|
||||||
ws.Route(ws.POST("/store/add-key").To(wsStoreAddKey).
|
ws.Route(ws.POST("/store/add-key").To(wsStoreAddKey).
|
||||||
Consumes(mime.JSON).Reads("").
|
Consumes(mime.JSON).Reads(NamedPassphrase{}).
|
||||||
Doc("Add an unlock key to the store"))
|
Doc("Add an unlock key to the store"))
|
||||||
|
ws.Route(ws.POST("/store/delete-key").To(wsStoreDelKey).
|
||||||
|
Consumes(mime.JSON).Reads("").
|
||||||
|
Doc("Remove an unlock key to the store (by its name)"))
|
||||||
|
|
||||||
// - downloads
|
// - downloads
|
||||||
ws.Route(ws.POST("/authorize-download").To(wsAuthorizeDownload).
|
ws.Route(ws.POST("/authorize-download").To(wsAuthorizeDownload).
|
||||||
|
@ -21,21 +21,3 @@
|
|||||||
.cluster {
|
.cluster {
|
||||||
max-width: 50%;
|
max-width: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#store-infos {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row wrap;
|
|
||||||
align-content: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
border-bottom: dashed 1pt;
|
|
||||||
margin-bottom: 1ex;
|
|
||||||
}
|
|
||||||
#store-infos > * {
|
|
||||||
display: block;
|
|
||||||
font-size: medium;
|
|
||||||
padding: 2pt 1ex;
|
|
||||||
margin: 0 0 0 1ex;
|
|
||||||
}
|
|
||||||
#store-infos > *:first-child {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
@ -41,8 +41,9 @@
|
|||||||
<p>Store is new.</p>
|
<p>Store is new.</p>
|
||||||
<p>Option 1: initialize a new store</p>
|
<p>Option 1: initialize a new store</p>
|
||||||
<form @submit="unlockStore">
|
<form @submit="unlockStore">
|
||||||
<input type="password" v-model="forms.store.pass1" name="passphrase" required />
|
<input type="text" v-model="forms.store.name" name="name" placeholder="Name" /><br/>
|
||||||
<input type="password" v-model="forms.store.pass2" required />
|
<input type="password" v-model="forms.store.pass1" name="passphrase" required placeholder="Passphrase" />
|
||||||
|
<input type="password" v-model="forms.store.pass2" required placeholder="Passphrase confirmation" />
|
||||||
<input type="submit" value="initialize" :disabled="!forms.store.pass1 || forms.store.pass1 != forms.store.pass2" />
|
<input type="submit" value="initialize" :disabled="!forms.store.pass1 || forms.store.pass1 != forms.store.pass2" />
|
||||||
</form>
|
</form>
|
||||||
<p>Option 2: upload a previously downloaded store</p>
|
<p>Option 2: upload a previously downloaded store</p>
|
||||||
@ -54,7 +55,7 @@
|
|||||||
<template v-else-if="!publicState.Store.Open">
|
<template v-else-if="!publicState.Store.Open">
|
||||||
<p>Store is not open.</p>
|
<p>Store is not open.</p>
|
||||||
<form @submit="unlockStore">
|
<form @submit="unlockStore">
|
||||||
<input type="password" name="passphrase" v-model="forms.store.pass1" required />
|
<input type="password" name="passphrase" v-model="forms.store.pass1" required placeholder="Passphrase" />
|
||||||
<input type="submit" value="unlock" :disabled="!forms.store.pass1" />
|
<input type="submit" value="unlock" :disabled="!forms.store.pass1" />
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
@ -63,28 +64,12 @@
|
|||||||
<p v-else>Invalid token</p>
|
<p v-else>Invalid token</p>
|
||||||
|
|
||||||
<form @submit="setToken">
|
<form @submit="setToken">
|
||||||
<input type="password" v-model="forms.setToken" required />
|
<input type="password" v-model="forms.setToken" required placeholder="Token" />
|
||||||
<input type="submit" value="set token"/>
|
<input type="submit" value="set token"/>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div id="store-infos">
|
|
||||||
<h2>Config</h2>
|
|
||||||
<form @submit="uploadConfig">
|
|
||||||
<input type="file" ref="configUpload" required />
|
|
||||||
<input type="submit" value="upload config" />
|
|
||||||
</form>
|
|
||||||
<h2>Store</h2>
|
|
||||||
<a :href="'/public/store.tar?token='+state.Store.DownloadToken" target="_blank">download</a>
|
|
||||||
<form @submit="storeAddKey" action="/store/add-key">
|
|
||||||
<input type="password" v-model="forms.store.pass1" name="passphrase" autocomplete="new-password" required />
|
|
||||||
<input type="password" v-model="forms.store.pass2" autocomplete="new-password" required />
|
|
||||||
<input type="submit" value="add unlock phrase" :disabled="!forms.store.pass1 || forms.store.pass1 != forms.store.pass2" />
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="state.Clusters" id="clusters">
|
<div v-if="state.Clusters" id="clusters">
|
||||||
|
|
||||||
<h2>Clusters</h2>
|
<h2>Clusters</h2>
|
||||||
|
|
||||||
<div class="sheets">
|
<div class="sheets">
|
||||||
@ -100,7 +85,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<pre v-if="false">{{ state }}</pre>
|
<h2>Admin actions</h2>
|
||||||
|
<h3>Config</h3>
|
||||||
|
<form @submit="uploadConfig">
|
||||||
|
<input type="file" ref="configUpload" required />
|
||||||
|
<input type="submit" value="upload config" />
|
||||||
|
</form>
|
||||||
|
<h3>Store</h3>
|
||||||
|
<p><a :href="'/public/store.tar?token='+state.Store.DownloadToken" target="_blank">Download</a></p>
|
||||||
|
<form @submit="storeAddKey" action="/store/add-key">
|
||||||
|
<p>Add an unlock phrase:</p>
|
||||||
|
<input type="text" v-model="forms.store.name" name="name" required placeholder="Name" /><br/>
|
||||||
|
<input type="password" v-model="forms.store.pass1" name="passphrase" autocomplete="new-password" required placeholder="Phrase" />
|
||||||
|
<input type="password" v-model="forms.store.pass2" autocomplete="new-password" required placeholder="Phrase confirmation" />
|
||||||
|
<input type="submit" value="add unlock phrase" :disabled="!forms.store.pass1 || forms.store.pass1 != forms.store.pass2" />
|
||||||
|
</form>
|
||||||
|
<form @submit="storeDelKey" action="/store/delete-key">
|
||||||
|
<p>Remove an unlock phrase:</p>
|
||||||
|
<input type="text" v-model="forms.delKey.name" name="name" required placeholder="Name" />
|
||||||
|
<input type="submit" value="remove unlock phrase" />
|
||||||
|
|
||||||
|
<p v-if="state.Store.KeyNames">Available names:
|
||||||
|
<template v-for="k,i in state.Store.KeyNames">{{i?", ":""}}<code @click="forms.delKey.name=k">{{k}}</code></template>.</p>
|
||||||
|
</form>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -9,8 +9,9 @@ createApp({
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
forms: {
|
forms: {
|
||||||
store: { },
|
store: {},
|
||||||
storeUpload: {},
|
storeUpload: {},
|
||||||
|
delKey: {},
|
||||||
},
|
},
|
||||||
session: {},
|
session: {},
|
||||||
error: null,
|
error: null,
|
||||||
@ -67,13 +68,26 @@ createApp({
|
|||||||
this.forms.store = {}
|
this.forms.store = {}
|
||||||
}, "application/tar")
|
}, "application/tar")
|
||||||
},
|
},
|
||||||
|
namedPassphrase(name, passphrase) {
|
||||||
|
return {Name: this.forms.store.name, Passphrase: btoa(this.forms.store.pass1)}
|
||||||
|
},
|
||||||
storeAddKey() {
|
storeAddKey() {
|
||||||
this.apiPost('/store/add-key', this.forms.store.pass1, (v) => {
|
this.apiPost('/store/add-key', this.namedPassphrase(), (v) => {
|
||||||
this.forms.store = {}
|
this.forms.store = {}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
storeDelKey() {
|
||||||
|
let name = this.forms.delKey.name
|
||||||
|
|
||||||
|
if (!confirm("Remove key named "+JSON.stringify(name)+"?")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.apiPost('/store/delete-key', name , (v) => {
|
||||||
|
this.forms.delKey = {}
|
||||||
|
})
|
||||||
|
},
|
||||||
unlockStore() {
|
unlockStore() {
|
||||||
this.apiPost('/public/unlock-store', this.forms.store.pass1, (v) => {
|
this.apiPost('/public/unlock-store', this.namedPassphrase(), (v) => {
|
||||||
this.forms.store = {}
|
this.forms.store = {}
|
||||||
|
|
||||||
if (v) {
|
if (v) {
|
||||||
|
@ -2,29 +2,34 @@ package secretstore
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"golang.org/x/crypto/argon2"
|
"golang.org/x/crypto/argon2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Store struct {
|
type Store struct {
|
||||||
|
Salt [aes.BlockSize]byte
|
||||||
|
Keys []KeyEntry
|
||||||
|
|
||||||
unlocked bool
|
unlocked bool
|
||||||
key [32]byte
|
key [32]byte
|
||||||
salt [aes.BlockSize]byte
|
|
||||||
keys []keyEntry
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type keyEntry struct {
|
type KeyEntry struct {
|
||||||
hash [64]byte
|
Name string
|
||||||
encKey [32]byte
|
Hash [64]byte
|
||||||
|
EncKey [32]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() (s *Store) {
|
func New() (s *Store) {
|
||||||
@ -77,30 +82,32 @@ func (s *Store) Close() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) IsNew() bool {
|
func (s *Store) IsNew() bool {
|
||||||
return len(s.keys) == 0
|
return len(s.Keys) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) Unlocked() bool {
|
func (s *Store) Unlocked() bool {
|
||||||
return s.unlocked
|
return s.unlocked
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) Init(passphrase []byte) (err error) {
|
func (s *Store) Init(name string, passphrase []byte) (err error) {
|
||||||
err = randRead(s.key[:])
|
err = randRead(s.key[:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = randRead(s.salt[:])
|
err = randRead(s.Salt[:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.AddKey(passphrase)
|
s.AddKey(name, passphrase)
|
||||||
|
|
||||||
s.unlocked = true
|
s.unlocked = true
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var jsonFormatHdr = []byte("{json}")
|
||||||
|
|
||||||
func (s *Store) ReadFrom(in io.Reader) (n int64, err error) {
|
func (s *Store) ReadFrom(in io.Reader) (n int64, err error) {
|
||||||
memzero(s.key[:])
|
memzero(s.key[:])
|
||||||
s.unlocked = false
|
s.unlocked = false
|
||||||
@ -117,56 +124,52 @@ func (s *Store) ReadFrom(in io.Reader) (n int64, err error) {
|
|||||||
n += int64(nr)
|
n += int64(nr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// read the salt
|
// read the file's start (json header or start of salt)
|
||||||
readFull(s.salt[:])
|
|
||||||
|
readFull(s.Salt[:len(jsonFormatHdr)])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(s.Salt[:len(jsonFormatHdr)], jsonFormatHdr) {
|
||||||
|
// old key file
|
||||||
|
|
||||||
|
// finish reading the salt
|
||||||
|
readFull(s.Salt[len(jsonFormatHdr):])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// read the (encrypted) keys
|
// read the (encrypted) keys
|
||||||
s.keys = make([]keyEntry, 0)
|
s.Keys = make([]KeyEntry, 0)
|
||||||
for {
|
for {
|
||||||
k := keyEntry{}
|
k := KeyEntry{Name: "key-" + strconv.Itoa(len(s.Keys))}
|
||||||
readFull(k.hash[:])
|
readFull(k.Hash[:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
err = nil
|
err = nil
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
readFull(k.encKey[:])
|
readFull(k.EncKey[:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.keys = append(s.keys, k)
|
s.Keys = append(s.Keys, k)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.NewDecoder(in).Decode(s)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) WriteTo(out io.Writer) (n int64, err error) {
|
func (s *Store) WriteTo(out io.Writer) (n int64, err error) {
|
||||||
write := func(ba []byte) {
|
_, err = out.Write(jsonFormatHdr)
|
||||||
var nr int
|
|
||||||
nr, err = out.Write(ba)
|
|
||||||
n += int64(nr)
|
|
||||||
}
|
|
||||||
|
|
||||||
write(s.salt[:])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
err = json.NewEncoder(out).Encode(s)
|
||||||
for _, k := range s.keys {
|
|
||||||
write(k.hash[:])
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
write(k.encKey[:])
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,8 +181,8 @@ func (s *Store) Unlock(passphrase []byte) (ok bool) {
|
|||||||
defer memzero(key[:])
|
defer memzero(key[:])
|
||||||
|
|
||||||
var idx = -1
|
var idx = -1
|
||||||
for i := range s.keys {
|
for i := range s.Keys {
|
||||||
if hash == s.keys[i].hash {
|
if hash == s.Keys[i].Hash {
|
||||||
idx = i
|
idx = i
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -189,28 +192,28 @@ func (s *Store) Unlock(passphrase []byte) (ok bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.decryptTo(s.key[:], s.keys[idx].encKey[:], &key)
|
s.decryptTo(s.key[:], s.Keys[idx].EncKey[:], &key)
|
||||||
|
|
||||||
s.unlocked = true
|
s.unlocked = true
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) AddKey(passphrase []byte) {
|
func (s *Store) AddKey(name string, passphrase []byte) {
|
||||||
key, hash := s.keyPairFromPassword(passphrase)
|
key, hash := s.keyPairFromPassword(passphrase)
|
||||||
memzero(passphrase)
|
memzero(passphrase)
|
||||||
|
|
||||||
defer memzero(key[:])
|
defer memzero(key[:])
|
||||||
|
|
||||||
k := keyEntry{hash: hash}
|
k := KeyEntry{Name: name, Hash: hash}
|
||||||
|
|
||||||
encKey := s.encrypt(s.key[:], &key)
|
encKey := s.encrypt(s.key[:], &key)
|
||||||
copy(k.encKey[:], encKey)
|
copy(k.EncKey[:], encKey)
|
||||||
|
|
||||||
s.keys = append(s.keys, k)
|
s.Keys = append(s.Keys, k)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) keyPairFromPassword(password []byte) (key [32]byte, hash [64]byte) {
|
func (s *Store) keyPairFromPassword(password []byte) (key [32]byte, hash [64]byte) {
|
||||||
keySlice := argon2.IDKey(password, s.salt[:], 1, 64*1024, 4, 32)
|
keySlice := argon2.IDKey(password, s.Salt[:], 1, 64*1024, 4, 32)
|
||||||
|
|
||||||
copy(key[:], keySlice)
|
copy(key[:], keySlice)
|
||||||
memzero(keySlice)
|
memzero(keySlice)
|
||||||
@ -236,12 +239,12 @@ func (s *Store) NewDecrypter(iv [aes.BlockSize]byte) cipher.Stream {
|
|||||||
|
|
||||||
func (s *Store) encrypt(src []byte, key *[32]byte) (dst []byte) {
|
func (s *Store) encrypt(src []byte, key *[32]byte) (dst []byte) {
|
||||||
dst = make([]byte, len(src))
|
dst = make([]byte, len(src))
|
||||||
newEncrypter(s.salt, key).XORKeyStream(dst, src)
|
newEncrypter(s.Salt, key).XORKeyStream(dst, src)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) decryptTo(dst []byte, src []byte, key *[32]byte) {
|
func (s *Store) decryptTo(dst []byte, src []byte, key *[32]byte) {
|
||||||
newDecrypter(s.salt, key).XORKeyStream(dst, src)
|
newDecrypter(s.Salt, key).XORKeyStream(dst, src)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newEncrypter(iv [aes.BlockSize]byte, key *[32]byte) cipher.Stream {
|
func newEncrypter(iv [aes.BlockSize]byte, key *[32]byte) cipher.Stream {
|
||||||
|
Loading…
Reference in New Issue
Block a user