165 lines
4.3 KiB
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
|
|
}
|