local-server/secretstore/secret-store.go

267 lines
4.5 KiB
Go
Raw Normal View History

2023-02-07 20:29:19 +00:00
package secretstore
import (
"bufio"
2023-09-10 14:47:54 +00:00
"bytes"
2023-02-07 20:29:19 +00:00
"crypto/aes"
"crypto/cipher"
"crypto/sha512"
2023-09-10 14:47:54 +00:00
"encoding/json"
2023-02-07 20:29:19 +00:00
"errors"
"fmt"
"io"
"log"
"os"
2023-09-10 14:47:54 +00:00
"strconv"
2023-02-07 20:29:19 +00:00
"syscall"
"golang.org/x/crypto/argon2"
)
type Store struct {
2023-09-10 14:47:54 +00:00
Salt [aes.BlockSize]byte
Keys []KeyEntry
2023-02-07 20:29:19 +00:00
unlocked bool
key [32]byte
}
2023-09-10 14:47:54 +00:00
type KeyEntry struct {
Name string
Hash [64]byte
EncKey [32]byte
2023-02-07 20:29:19 +00:00
}
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 {
2023-09-10 14:47:54 +00:00
return len(s.Keys) == 0
2023-02-07 20:29:19 +00:00
}
func (s *Store) Unlocked() bool {
return s.unlocked
}
2023-09-10 14:47:54 +00:00
func (s *Store) Init(name string, passphrase []byte) (err error) {
2023-02-07 20:29:19 +00:00
err = randRead(s.key[:])
if err != nil {
return
}
2023-09-10 14:47:54 +00:00
err = randRead(s.Salt[:])
2023-02-07 20:29:19 +00:00
if err != nil {
return
}
2023-09-10 14:47:54 +00:00
s.AddKey(name, passphrase)
2023-02-07 20:29:19 +00:00
s.unlocked = true
return
}
2023-09-10 14:47:54 +00:00
var jsonFormatHdr = []byte("{json}")
2023-02-07 20:29:19 +00:00
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)
}
2023-09-10 14:47:54 +00:00
// read the file's start (json header or start of salt)
readFull(s.Salt[:len(jsonFormatHdr)])
2023-02-07 20:29:19 +00:00
if err != nil {
return
}
2023-09-10 14:47:54 +00:00
if !bytes.Equal(s.Salt[:len(jsonFormatHdr)], jsonFormatHdr) {
// old key file
// finish reading the salt
readFull(s.Salt[len(jsonFormatHdr):])
2023-02-07 20:29:19 +00:00
if err != nil {
return
}
2023-09-10 14:47:54 +00:00
// 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)
}
2023-02-07 20:29:19 +00:00
}
2023-09-10 14:47:54 +00:00
err = json.NewDecoder(in).Decode(s)
return
2023-02-07 20:29:19 +00:00
}
func (s *Store) WriteTo(out io.Writer) (n int64, err error) {
2023-09-10 14:47:54 +00:00
_, err = out.Write(jsonFormatHdr)
2023-02-07 20:29:19 +00:00
if err != nil {
return
}
2023-09-10 14:47:54 +00:00
err = json.NewEncoder(out).Encode(s)
2023-02-07 20:29:19 +00:00
return
}
var ErrNoSuchKey = errors.New("no such key")
func (s *Store) Unlock(passphrase []byte) (ok bool) {
key, hash := s.keyPairFromPassword(passphrase)
memzero(passphrase)
defer memzero(key[:])
var idx = -1
2023-09-10 14:47:54 +00:00
for i := range s.Keys {
if hash == s.Keys[i].Hash {
2023-02-07 20:29:19 +00:00
idx = i
break
}
}
if idx == -1 {
return
}
2023-09-10 14:47:54 +00:00
s.decryptTo(s.key[:], s.Keys[idx].EncKey[:], &key)
2023-02-07 20:29:19 +00:00
s.unlocked = true
return true
}
2023-09-10 14:47:54 +00:00
func (s *Store) AddKey(name string, passphrase []byte) {
2023-02-07 20:29:19 +00:00
key, hash := s.keyPairFromPassword(passphrase)
memzero(passphrase)
defer memzero(key[:])
2023-09-10 14:47:54 +00:00
k := KeyEntry{Name: name, Hash: hash}
2023-02-07 20:29:19 +00:00
encKey := s.encrypt(s.key[:], &key)
2023-09-10 14:47:54 +00:00
copy(k.EncKey[:], encKey)
2023-02-07 20:29:19 +00:00
2023-09-10 14:47:54 +00:00
s.Keys = append(s.Keys, k)
2023-02-07 20:29:19 +00:00
}
func (s *Store) keyPairFromPassword(password []byte) (key [32]byte, hash [64]byte) {
2023-09-10 14:47:54 +00:00
keySlice := argon2.IDKey(password, s.Salt[:], 1, 64*1024, 4, 32)
2023-02-07 20:29:19 +00:00
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))
2023-09-10 14:47:54 +00:00
newEncrypter(s.Salt, key).XORKeyStream(dst, src)
2023-02-07 20:29:19 +00:00
return
}
func (s *Store) decryptTo(dst []byte, src []byte, key *[32]byte) {
2023-09-10 14:47:54 +00:00
newDecrypter(s.Salt, key).XORKeyStream(dst, src)
2023-02-07 20:29:19 +00:00
}
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[:])
}