40d08139db
This allows the user to call it even after the store has been unlock in order to get the admin token.
279 lines
4.6 KiB
Go
279 lines
4.6 KiB
Go
package secretstore
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/sha512"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"strconv"
|
|
"syscall"
|
|
|
|
"golang.org/x/crypto/argon2"
|
|
)
|
|
|
|
type Store struct {
|
|
Salt [aes.BlockSize]byte
|
|
Keys []KeyEntry
|
|
|
|
unlocked bool
|
|
key [32]byte
|
|
}
|
|
|
|
type KeyEntry struct {
|
|
Name string
|
|
Hash [64]byte
|
|
EncKey [32]byte
|
|
}
|
|
|
|
func New() (s *Store) {
|
|
s = &Store{}
|
|
syscall.Mlock(s.key[:])
|
|
return
|
|
}
|
|
|
|
func Open(path string) (s *Store, err error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
s = New()
|
|
_, err = s.ReadFrom(bufio.NewReader(f))
|
|
|
|
return
|
|
}
|
|
|
|
func (s *Store) SaveTo(path string) (err error) {
|
|
f, err := os.OpenFile(path, syscall.O_CREAT|syscall.O_TRUNC|syscall.O_WRONLY, 0600)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
out := bufio.NewWriter(f)
|
|
|
|
_, err = s.WriteTo(out)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
err = out.Flush()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (s *Store) Close() {
|
|
memzero(s.key[:])
|
|
syscall.Munlock(s.key[:])
|
|
s.unlocked = false
|
|
}
|
|
|
|
func (s *Store) IsNew() bool {
|
|
return len(s.Keys) == 0
|
|
}
|
|
|
|
func (s *Store) Unlocked() bool {
|
|
return s.unlocked
|
|
}
|
|
|
|
func (s *Store) Init(name string, passphrase []byte) (err error) {
|
|
err = randRead(s.key[:])
|
|
if err != nil {
|
|
return
|
|
}
|
|
err = randRead(s.Salt[:])
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
s.AddKey(name, passphrase)
|
|
|
|
s.unlocked = true
|
|
|
|
return
|
|
}
|
|
|
|
var jsonFormatHdr = []byte("{json}")
|
|
|
|
func (s *Store) ReadFrom(in io.Reader) (n int64, err error) {
|
|
memzero(s.key[:])
|
|
s.unlocked = false
|
|
|
|
defer func() {
|
|
if err != nil {
|
|
log.Output(2, fmt.Sprintf("failed after %d bytes", n))
|
|
}
|
|
}()
|
|
|
|
readFull := func(ba []byte) {
|
|
var nr int
|
|
nr, err = io.ReadFull(in, ba)
|
|
n += int64(nr)
|
|
}
|
|
|
|
// read the file's start (json header or start of 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 {
|
|
return
|
|
}
|
|
|
|
// read the (encrypted) keys
|
|
s.Keys = make([]KeyEntry, 0)
|
|
for {
|
|
k := KeyEntry{Name: "key-" + strconv.Itoa(len(s.Keys))}
|
|
readFull(k.Hash[:])
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
err = nil
|
|
}
|
|
return
|
|
}
|
|
readFull(k.EncKey[:])
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
s.Keys = append(s.Keys, k)
|
|
}
|
|
}
|
|
|
|
err = json.NewDecoder(in).Decode(s)
|
|
return
|
|
}
|
|
|
|
func (s *Store) WriteTo(out io.Writer) (n int64, err error) {
|
|
_, err = out.Write(jsonFormatHdr)
|
|
if err != nil {
|
|
return
|
|
}
|
|
err = json.NewEncoder(out).Encode(s)
|
|
return
|
|
}
|
|
|
|
var ErrNoSuchKey = errors.New("no such key")
|
|
|
|
func (s *Store) HasKey(passphrase []byte) bool {
|
|
key, hash := s.keyPairFromPassword(passphrase)
|
|
defer memzero(key[:])
|
|
|
|
for _, k := range s.Keys {
|
|
if k.Hash == hash {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (s *Store) Unlock(passphrase []byte) (ok bool) {
|
|
key, hash := s.keyPairFromPassword(passphrase)
|
|
defer memzero(key[:])
|
|
|
|
var idx = -1
|
|
for i := range s.Keys {
|
|
if hash == s.Keys[i].Hash {
|
|
idx = i
|
|
break
|
|
}
|
|
}
|
|
|
|
if idx == -1 {
|
|
return
|
|
}
|
|
|
|
s.decryptTo(s.key[:], s.Keys[idx].EncKey[:], &key)
|
|
|
|
s.unlocked = true
|
|
return true
|
|
}
|
|
|
|
func (s *Store) AddKey(name string, passphrase []byte) {
|
|
key, hash := s.keyPairFromPassword(passphrase)
|
|
memzero(passphrase)
|
|
|
|
defer memzero(key[:])
|
|
|
|
k := KeyEntry{Name: name, Hash: hash}
|
|
|
|
encKey := s.encrypt(s.key[:], &key)
|
|
copy(k.EncKey[:], encKey)
|
|
|
|
s.Keys = append(s.Keys, k)
|
|
}
|
|
|
|
func (s *Store) keyPairFromPassword(password []byte) (key [32]byte, hash [64]byte) {
|
|
keySlice := argon2.IDKey(password, s.Salt[:], 1, 64*1024, 4, 32)
|
|
|
|
copy(key[:], keySlice)
|
|
memzero(keySlice)
|
|
|
|
hash = sha512.Sum512(key[:])
|
|
|
|
return
|
|
}
|
|
|
|
func (s *Store) NewEncrypter(iv [aes.BlockSize]byte) cipher.Stream {
|
|
if !s.unlocked {
|
|
panic("not unlocked")
|
|
}
|
|
return newEncrypter(iv, &s.key)
|
|
}
|
|
|
|
func (s *Store) NewDecrypter(iv [aes.BlockSize]byte) cipher.Stream {
|
|
if !s.unlocked {
|
|
panic("not unlocked")
|
|
}
|
|
return newDecrypter(iv, &s.key)
|
|
}
|
|
|
|
func (s *Store) encrypt(src []byte, key *[32]byte) (dst []byte) {
|
|
dst = make([]byte, len(src))
|
|
newEncrypter(s.Salt, key).XORKeyStream(dst, src)
|
|
return
|
|
}
|
|
|
|
func (s *Store) decryptTo(dst []byte, src []byte, key *[32]byte) {
|
|
newDecrypter(s.Salt, key).XORKeyStream(dst, src)
|
|
}
|
|
|
|
func newEncrypter(iv [aes.BlockSize]byte, key *[32]byte) cipher.Stream {
|
|
c, err := aes.NewCipher(key[:])
|
|
if err != nil {
|
|
panic(fmt.Errorf("failed to init AES: %w", err))
|
|
}
|
|
|
|
return cipher.NewCFBEncrypter(c, iv[:])
|
|
}
|
|
|
|
func newDecrypter(iv [aes.BlockSize]byte, key *[32]byte) cipher.Stream {
|
|
c, err := aes.NewCipher(key[:])
|
|
if err != nil {
|
|
panic(fmt.Errorf("failed to init AES: %w", err))
|
|
}
|
|
|
|
return cipher.NewCFBDecrypter(c, iv[:])
|
|
}
|