owpengram-server/internal/loadharness/storage.go

165 lines
4.3 KiB
Go

package loadharness
import (
"context"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"github.com/iamxvbaba/td/session"
)
const encryptedSessionMagic = "TLSLOAD1"
// EncryptedFileStorage encrypts gotd's complete session blob with AES-256-GCM.
// A unique random nonce is generated on every replacement and the file is
// written with owner-only permissions.
type EncryptedFileStorage struct {
Path string
Key [32]byte
mu sync.Mutex
}
func (s *EncryptedFileStorage) LoadSession(context.Context) ([]byte, error) {
if s == nil || strings.TrimSpace(s.Path) == "" {
return nil, errors.New("invalid encrypted session storage")
}
s.mu.Lock()
defer s.mu.Unlock()
data, err := os.ReadFile(s.Path)
if os.IsNotExist(err) {
return nil, session.ErrNotFound
}
if err != nil {
return nil, fmt.Errorf("read encrypted session: %w", err)
}
block, err := aes.NewCipher(s.Key[:])
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
header := len(encryptedSessionMagic) + gcm.NonceSize()
if len(data) < header || string(data[:len(encryptedSessionMagic)]) != encryptedSessionMagic {
return nil, errors.New("encrypted session has an invalid header")
}
nonce := data[len(encryptedSessionMagic):header]
plain, err := gcm.Open(nil, nonce, data[header:], []byte(encryptedSessionMagic))
if err != nil {
return nil, errors.New("encrypted session authentication failed")
}
return plain, nil
}
func (s *EncryptedFileStorage) StoreSession(_ context.Context, plain []byte) error {
if s == nil || strings.TrimSpace(s.Path) == "" {
return errors.New("invalid encrypted session storage")
}
s.mu.Lock()
defer s.mu.Unlock()
block, err := aes.NewCipher(s.Key[:])
if err != nil {
return err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return fmt.Errorf("generate session nonce: %w", err)
}
data := make([]byte, 0, len(encryptedSessionMagic)+len(nonce)+len(plain)+gcm.Overhead())
data = append(data, encryptedSessionMagic...)
data = append(data, nonce...)
data = gcm.Seal(data, nonce, plain, []byte(encryptedSessionMagic))
return writeFileAtomic(s.Path, data, 0o600)
}
func GenerateSessionKey(path string) error {
if _, err := os.Stat(path); err == nil {
return fmt.Errorf("refusing to overwrite existing session key %q", path)
} else if !os.IsNotExist(err) {
return err
}
var key [32]byte
if _, err := io.ReadFull(rand.Reader, key[:]); err != nil {
return err
}
encoded := base64.StdEncoding.EncodeToString(key[:]) + "\n"
return writeFileAtomic(path, []byte(encoded), 0o600)
}
func LoadSessionKey(path string) ([32]byte, error) {
var key [32]byte
info, err := os.Stat(path)
if err != nil {
return key, fmt.Errorf("stat session key: %w", err)
}
if runtime.GOOS != "windows" && info.Mode().Perm()&0o077 != 0 {
return key, fmt.Errorf("session key %q must not be group/world accessible (mode %o)", path, info.Mode().Perm())
}
data, err := os.ReadFile(path)
if err != nil {
return key, err
}
decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(string(data)))
if err != nil || len(decoded) != len(key) {
return key, errors.New("session key must be base64-encoded 32 bytes")
}
copy(key[:], decoded)
return key, nil
}
func writeFileAtomic(path string, data []byte, mode os.FileMode) (retErr error) {
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0o700); err != nil {
return err
}
tmp, err := os.CreateTemp(dir, ".telesrv-load-*")
if err != nil {
return err
}
tmpName := tmp.Name()
defer func() {
_ = tmp.Close()
if retErr != nil {
_ = os.Remove(tmpName)
}
}()
if err := tmp.Chmod(mode); err != nil {
return err
}
if _, err := tmp.Write(data); err != nil {
return err
}
if err := tmp.Sync(); err != nil {
return err
}
if err := tmp.Close(); err != nil {
return err
}
// On Unix rename atomically replaces. Windows requires removing the old
// destination first; session files remain recoverable from the complete temp
// file if that narrow replacement fails.
if runtime.GOOS == "windows" {
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
return err
}
}
if err := os.Rename(tmpName, path); err != nil {
return err
}
return nil
}