Harden config perms and key derivation (#8)
Closes #4.
This commit was merged in pull request #8.
This commit is contained in:
@@ -70,7 +70,7 @@ Both services share the same encrypted tunnel connection!
|
||||
|
||||
## Features
|
||||
|
||||
- **Secure Encryption**: All traffic is encrypted using AES-GCM encryption with PBKDF2 key derivation
|
||||
- **Secure Encryption**: All traffic is encrypted using AES-GCM. New keys are raw 256-bit AES keys; passphrase-style configs still use PBKDF2
|
||||
- **Port Forwarding**: Forward multiple ports with different protocols (TCP and UDP)
|
||||
- **Configuration-based**: Easy configuration via YAML files
|
||||
- **Bidirectional**: Full bidirectional port forwarding
|
||||
@@ -186,8 +186,8 @@ dns_server:
|
||||
- `"tcp://80:8080"` (client) - listen on `127.0.0.1:8080`, forward to teleport server's port 80
|
||||
- `"tcp://22:0.0.0.0:2222"` (client) - listen on all interfaces port 2222
|
||||
- `"udp://53:5353"` (client) - listen on `127.0.0.1:5353`, forward to teleport server's port 53
|
||||
- `encryption_key`: Shared secret key for encryption (must be the same on both sides)
|
||||
- `keep_alive`: Enable TCP keep-alive
|
||||
- `encryption_key`: Shared secret (must be the same on both sides). New keys from `--generate-key` / `--generate-config` are `raw:` plus 64 hex characters (32-byte AES-256 key, no PBKDF2). Unprefixed values — passphrases and hex strings from older `--generate-key` — still go through PBKDF2 so existing configs keep working. To migrate an old hex key to raw AES, re-generate with `--generate-key` and update both sides together; do not strip `raw:` from a new key or add `raw:` to an old hex string.
|
||||
- `keep_alive`: Enable TCP keep-alive on tunnel and forwarded TCP connections (helps long-lived forwards through NAT)
|
||||
- `read_timeout`: Read timeout duration
|
||||
- `write_timeout`: Write timeout duration
|
||||
- `max_connections`: Maximum concurrent connections (default: 1000 for server, 100 for client)
|
||||
@@ -265,7 +265,9 @@ dns_server:
|
||||
./teleport -k
|
||||
```
|
||||
|
||||
This generates a cryptographically secure 256-bit encryption key that you can use in your configuration files.
|
||||
This prints a `raw:` prefixed 256-bit key. Paste the entire value into `encryption_key` on both server and client. It is used as an AES-256 key directly (no PBKDF2). Existing configs that store a passphrase or an unprefixed hex string still use PBKDF2.
|
||||
|
||||
`--generate-config` writes the file mode `0600` because the file embeds a live encryption key.
|
||||
|
||||
### Logging Options
|
||||
|
||||
@@ -503,6 +505,8 @@ rate_limit:
|
||||
window_size: 1s # Time window for rate limiting
|
||||
```
|
||||
|
||||
The token bucket is process-global (not per source IP). One noisy peer can consume the budget for everyone; that is enough for a single-user homelab.
|
||||
|
||||
|
||||
### Advanced Logging
|
||||
|
||||
@@ -523,7 +527,7 @@ Teleport includes sophisticated logging with:
|
||||
## Notes
|
||||
|
||||
- The encryption key must be identical on both server and client
|
||||
- Use `./teleport --generate-key` to create a secure random encryption key
|
||||
- Use `./teleport --generate-key` to create a raw 256-bit AES key (`raw:` + 64 hex). Unprefixed keys in existing configs still use PBKDF2
|
||||
- The server listens on the specified `listen_address` for incoming tunnel connections (examples bind loopback; `0.0.0.0` must be explicit)
|
||||
- The client connects to the remote server and forwards local connections from `bind_address` (default `127.0.0.1`)
|
||||
- All port forwarding is bidirectional
|
||||
|
||||
+6
-14
@@ -1,8 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -62,7 +60,8 @@ func main() {
|
||||
return
|
||||
}
|
||||
fmt.Printf("Generated encryption key: %s\n", key)
|
||||
fmt.Println("Use this key in your configuration file for both server and client.")
|
||||
fmt.Println("Paste the entire value into encryption_key on both server and client.")
|
||||
fmt.Println("This is a raw 256-bit AES key (no PBKDF2). Unprefixed keys in existing configs still use PBKDF2.")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -116,21 +115,14 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// generateRandomKey generates a cryptographically secure random encryption key
|
||||
// generateRandomKey generates a raw 256-bit AES key (raw: + 64 hex chars).
|
||||
func generateRandomKey() (string, error) {
|
||||
// Generate 32 random bytes (256 bits) for a strong encryption key
|
||||
bytes := make([]byte, 32)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "", fmt.Errorf("failed to generate random key: %v", err)
|
||||
key, err := encryption.GenerateRawKey()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Convert to hexadecimal string for easy copying
|
||||
key := hex.EncodeToString(bytes)
|
||||
|
||||
// Validate the generated key
|
||||
if err := encryption.ValidateEncryptionKey(key); err != nil {
|
||||
return "", fmt.Errorf("generated key failed validation: %v", err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ func NewTeleportClient(config *config.Config) *TeleportClient {
|
||||
cancel: cancel,
|
||||
connectionPool: make(chan net.Conn, maxPoolSize),
|
||||
maxPoolSize: maxPoolSize,
|
||||
derivedKey: encryption.DeriveKey(config.EncryptionKey),
|
||||
derivedKey: mustResolveKey(config.EncryptionKey),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ func (tc *TeleportClient) Start() error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to server: %v", err)
|
||||
}
|
||||
config.ApplyTCPKeepAlive(conn, tc.config.KeepAlive)
|
||||
tc.serverConn = conn
|
||||
|
||||
// Start DNS server if enabled
|
||||
@@ -187,6 +188,7 @@ func (tc *TeleportClient) getConnection() (net.Conn, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create connection: %v", err)
|
||||
}
|
||||
config.ApplyTCPKeepAlive(conn, tc.config.KeepAlive)
|
||||
|
||||
// Set connection timeouts
|
||||
if tc.config.ReadTimeout > 0 {
|
||||
@@ -343,6 +345,7 @@ func (tc *TeleportClient) sendTaggedUDPPacketWithResponse(packet types.TaggedUDP
|
||||
return
|
||||
}
|
||||
defer serverConn.Close()
|
||||
config.ApplyTCPKeepAlive(serverConn, tc.config.KeepAlive)
|
||||
|
||||
logger.WithField("packetID", packet.Header.PacketID).Debug("UDP CLIENT: Connected to server, sending port forward request")
|
||||
|
||||
@@ -399,9 +402,8 @@ func (tc *TeleportClient) sendTaggedUDPPacketToConnection(conn net.Conn, packet
|
||||
"data_length": len(data),
|
||||
}).Debug("UDP CLIENT: Serialized packet")
|
||||
|
||||
// Encrypt the data
|
||||
key := encryption.DeriveKey(tc.config.EncryptionKey)
|
||||
encryptedData, err := encryption.EncryptData(data, key)
|
||||
// Encrypt the data (key resolved once at process start)
|
||||
encryptedData, err := encryption.EncryptData(data, tc.derivedKey)
|
||||
if err != nil {
|
||||
logger.WithFields(map[string]interface{}{
|
||||
"packetID": packet.Header.PacketID,
|
||||
@@ -453,9 +455,8 @@ func (tc *TeleportClient) waitForUDPResponseAndForward(conn net.Conn, expectedPa
|
||||
"bytes_received": n,
|
||||
}).Debug("UDP CLIENT: Received response bytes")
|
||||
|
||||
// Decrypt the response
|
||||
key := encryption.DeriveKey(tc.config.EncryptionKey)
|
||||
decryptedData, err := encryption.DecryptData(buffer[:n], key)
|
||||
// Decrypt the response (key resolved once at process start)
|
||||
decryptedData, err := encryption.DecryptData(buffer[:n], tc.derivedKey)
|
||||
if err != nil {
|
||||
logger.WithFields(map[string]interface{}{
|
||||
"packetID": expectedPacketID,
|
||||
@@ -638,6 +639,7 @@ func (tc *TeleportClient) deserializeTaggedUDPPacket(data []byte) (types.TaggedU
|
||||
// handleTCPConnection handles a TCP connection from a local client
|
||||
func (tc *TeleportClient) handleTCPConnection(clientConn net.Conn, rule config.PortRule) {
|
||||
defer clientConn.Close()
|
||||
config.ApplyTCPKeepAlive(clientConn, tc.config.KeepAlive)
|
||||
|
||||
// Get a connection from the pool or create a new one
|
||||
serverConn, err := tc.getConnection()
|
||||
@@ -825,3 +827,12 @@ func (tc *TeleportClient) serializeRequest(request types.PortForwardRequest) ([]
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func mustResolveKey(material string) []byte {
|
||||
key, err := encryption.ResolveKey(material)
|
||||
if err != nil {
|
||||
logger.WithField("error", err).Error("Failed to resolve encryption key")
|
||||
return nil
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ func NewTeleportServer(config *config.Config) *TeleportServer {
|
||||
goroutineSem: make(chan struct{}, maxGoroutines),
|
||||
maxGoroutines: maxGoroutines,
|
||||
metrics: metricsInstance,
|
||||
derivedKey: encryption.DeriveKey(config.EncryptionKey),
|
||||
derivedKey: mustResolveKey(config.EncryptionKey),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,9 +254,8 @@ func (ts *TeleportServer) sendTaggedUDPPacket(clientConn *net.UDPConn, packet ty
|
||||
return
|
||||
}
|
||||
|
||||
// Encrypt the data
|
||||
key := encryption.DeriveKey(ts.config.EncryptionKey)
|
||||
encryptedData, err := encryption.EncryptData(data, key)
|
||||
// Encrypt the data (key resolved once at process start)
|
||||
encryptedData, err := encryption.EncryptData(data, ts.derivedKey)
|
||||
if err != nil {
|
||||
logger.WithField("error", err).Debug("Failed to encrypt UDP packet")
|
||||
return
|
||||
@@ -310,6 +309,8 @@ func (ts *TeleportServer) handleConnectionWithLimit(conn net.Conn) {
|
||||
ts.metrics.DecrementActiveConnections()
|
||||
}()
|
||||
|
||||
config.ApplyTCPKeepAlive(conn, ts.config.KeepAlive)
|
||||
|
||||
// Set connection timeouts
|
||||
if ts.config.ReadTimeout > 0 {
|
||||
conn.SetReadDeadline(time.Now().Add(ts.config.ReadTimeout))
|
||||
@@ -393,6 +394,7 @@ func (ts *TeleportServer) handleTCPForward(clientConn net.Conn, rule *config.Por
|
||||
return
|
||||
}
|
||||
defer targetConn.Close()
|
||||
config.ApplyTCPKeepAlive(targetConn, ts.config.KeepAlive)
|
||||
|
||||
logger.WithFields(map[string]interface{}{
|
||||
"client": clientConn.RemoteAddr(),
|
||||
@@ -478,9 +480,8 @@ func (ts *TeleportServer) handleUDPForward(clientConn net.Conn, rule *config.Por
|
||||
|
||||
logger.WithField("bytes_received", n).Debug("UDP SERVER: Received bytes from client")
|
||||
|
||||
// Decrypt the data
|
||||
key := encryption.DeriveKey(ts.config.EncryptionKey)
|
||||
decryptedData, err := encryption.DecryptData(buffer[:n], key)
|
||||
// Decrypt the data (key resolved once at process start)
|
||||
decryptedData, err := encryption.DecryptData(buffer[:n], ts.derivedKey)
|
||||
if err != nil {
|
||||
logger.WithField("error", err).Debug("UDP SERVER: Failed to decrypt UDP packet")
|
||||
continue
|
||||
@@ -595,8 +596,7 @@ func (ts *TeleportServer) handleUDPForward(clientConn net.Conn, rule *config.Por
|
||||
continue
|
||||
}
|
||||
|
||||
key := encryption.DeriveKey(ts.config.EncryptionKey)
|
||||
encryptedData, err := encryption.EncryptData(data, key)
|
||||
encryptedData, err := encryption.EncryptData(data, ts.derivedKey)
|
||||
if err != nil {
|
||||
logger.WithFields(map[string]interface{}{
|
||||
"packetID": originalPacketID,
|
||||
@@ -897,3 +897,12 @@ func (ts *TeleportServer) deserializeRequest(data []byte, request *types.PortFor
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mustResolveKey(material string) []byte {
|
||||
key, err := encryption.ResolveKey(material)
|
||||
if err != nil {
|
||||
logger.WithField("error", err).Error("Failed to resolve encryption key")
|
||||
return nil
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
+19
-12
@@ -1,8 +1,6 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
@@ -609,24 +607,33 @@ func GenerateExampleConfig(filename string) error {
|
||||
return fmt.Errorf("failed to marshal config: %v", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(filename, data, 0644)
|
||||
err = os.WriteFile(filename, data, 0o600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write config file: %v", err)
|
||||
}
|
||||
if err := os.Chmod(filename, 0o600); err != nil {
|
||||
return fmt.Errorf("failed to set config file permissions: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Generated example configuration: %s\n", filename)
|
||||
fmt.Printf("Edit the configuration file and run: ./teleport -config %s\n", filename)
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateStrongEncryptionKey generates a cryptographically secure encryption key
|
||||
// generateStrongEncryptionKey generates a raw 256-bit key (raw: + 64 hex chars).
|
||||
func generateStrongEncryptionKey() (string, error) {
|
||||
// Generate 32 random bytes (256 bits) for a strong encryption key
|
||||
bytes := make([]byte, 32)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "", fmt.Errorf("failed to generate random key: %v", err)
|
||||
}
|
||||
|
||||
// Convert to hexadecimal string for easy copying
|
||||
return hex.EncodeToString(bytes), nil
|
||||
return encryption.GenerateRawKey()
|
||||
}
|
||||
|
||||
// ApplyTCPKeepAlive enables TCP keep-alive when enabled is true.
|
||||
func ApplyTCPKeepAlive(conn net.Conn, enabled bool) {
|
||||
if conn == nil || !enabled {
|
||||
return
|
||||
}
|
||||
tcp, ok := conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
_ = tcp.SetKeepAlive(true)
|
||||
_ = tcp.SetKeepAlivePeriod(30 * time.Second)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"teleport/pkg/encryption"
|
||||
)
|
||||
|
||||
func TestLoadConfig(t *testing.T) {
|
||||
@@ -585,3 +587,61 @@ func TestGenerateExampleConfigUsesLoopback(t *testing.T) {
|
||||
t.Fatalf("generated client local listen %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateExampleConfigMode0600(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
configFile := filepath.Join(tempDir, "server.yaml")
|
||||
if err := GenerateExampleConfig(configFile); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st, err := os.Stat(configFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if st.Mode().Perm() != 0o600 {
|
||||
t.Fatalf("generated config mode %04o want 0600", st.Mode().Perm())
|
||||
}
|
||||
cfg, err := LoadConfig(configFile)
|
||||
if err != nil {
|
||||
t.Fatalf("reload generated config: %v", err)
|
||||
}
|
||||
if !encryption.IsRawKey(cfg.EncryptionKey) {
|
||||
t.Fatalf("generated encryption_key is not raw: %q", cfg.EncryptionKey[:min(8, len(cfg.EncryptionKey))])
|
||||
}
|
||||
key, err := encryption.ResolveKey(cfg.EncryptionKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(key) != 32 {
|
||||
t.Fatalf("resolved key len %d", len(key))
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyTCPKeepAlive(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
ApplyTCPKeepAlive(c, true)
|
||||
errCh <- nil
|
||||
}()
|
||||
conn, err := net.Dial("tcp", ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
ApplyTCPKeepAlive(conn, true)
|
||||
ApplyTCPKeepAlive(conn, false)
|
||||
if err := <-errCh; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -24,22 +26,69 @@ const (
|
||||
// Replay protection parameters
|
||||
MaxPacketAge = 5 * time.Minute // Maximum age for UDP packets
|
||||
NonceWindow = 1000 // Number of nonces to track for replay protection
|
||||
|
||||
// RawKeyPrefix marks a hex-encoded 32-byte AES key that must not go through PBKDF2.
|
||||
RawKeyPrefix = "raw:"
|
||||
RawKeyHexLen = 64 // 32 bytes
|
||||
)
|
||||
|
||||
// DeriveKey derives an encryption key from a password using PBKDF2
|
||||
// deriveCache memoizes PBKDF2 so handshake/UDP paths never pay 100k iterations twice.
|
||||
var deriveCache sync.Map // map[string][]byte
|
||||
|
||||
// GenerateRawKey returns a raw: prefixed hex encoding of 32 random bytes.
|
||||
// Paste the entire string into encryption_key; it is used as an AES-256 key (no PBKDF2).
|
||||
func GenerateRawKey() (string, error) {
|
||||
key := make([]byte, PBKDF2KeyLength)
|
||||
if _, err := io.ReadFull(rand.Reader, key); err != nil {
|
||||
return "", fmt.Errorf("failed to generate random key: %v", err)
|
||||
}
|
||||
return RawKeyPrefix + hex.EncodeToString(key), nil
|
||||
}
|
||||
|
||||
// IsRawKey reports whether material is a raw 256-bit key (raw: + 64 hex chars).
|
||||
func IsRawKey(material string) bool {
|
||||
return strings.HasPrefix(material, RawKeyPrefix)
|
||||
}
|
||||
|
||||
// ResolveKey returns a 32-byte AES key from config material.
|
||||
//
|
||||
// - raw:<64 hex>: hex-decode, no PBKDF2 (new --generate-key / generated configs)
|
||||
// - anything else, including legacy unprefixed 64-hex from older --generate-key:
|
||||
// PBKDF2 with the historical password-derived salt (existing configs keep working)
|
||||
func ResolveKey(material string) ([]byte, error) {
|
||||
if strings.HasPrefix(material, RawKeyPrefix) {
|
||||
hexStr := material[len(RawKeyPrefix):]
|
||||
key, err := hex.DecodeString(hexStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid raw encryption key: %w", err)
|
||||
}
|
||||
if len(key) != PBKDF2KeyLength {
|
||||
return nil, fmt.Errorf("raw encryption key must be %d bytes, got %d", PBKDF2KeyLength, len(key))
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
return DeriveKey(material), nil
|
||||
}
|
||||
|
||||
// DeriveKey derives an encryption key from a password using PBKDF2.
|
||||
// The first call for a given password runs 100k iterations; later calls return the cached key.
|
||||
func DeriveKey(password string) []byte {
|
||||
// Use a deterministic salt derived from the password hash for consistent key derivation
|
||||
// This ensures the same password always produces the same key while avoiding rainbow tables
|
||||
if v, ok := deriveCache.Load(password); ok {
|
||||
return v.([]byte)
|
||||
}
|
||||
|
||||
// Historical salt is SHA256(password)[:16]. That is not a random salt; keep it only
|
||||
// so existing passphrase and unprefixed hex configs still derive the same key.
|
||||
hasher := sha256.New()
|
||||
hasher.Write([]byte(password))
|
||||
passwordHash := hasher.Sum(nil)
|
||||
|
||||
// Create a deterministic salt from the password hash
|
||||
salt := make([]byte, PBKDF2SaltLength)
|
||||
copy(salt, passwordHash[:PBKDF2SaltLength])
|
||||
|
||||
key := pbkdf2.Key([]byte(password), salt, PBKDF2Iterations, PBKDF2KeyLength, sha256.New)
|
||||
return key
|
||||
actual, _ := deriveCache.LoadOrStore(password, key)
|
||||
return actual.([]byte)
|
||||
}
|
||||
|
||||
// DeriveKeyWithSalt derives an encryption key from a password using PBKDF2 with a custom salt
|
||||
@@ -176,6 +225,21 @@ func ConstantTimeCompare(a, b []byte) bool {
|
||||
|
||||
// ValidateEncryptionKey validates that an encryption key meets security requirements
|
||||
func ValidateEncryptionKey(key string) error {
|
||||
if strings.HasPrefix(key, RawKeyPrefix) {
|
||||
hexStr := key[len(RawKeyPrefix):]
|
||||
if len(hexStr) != RawKeyHexLen {
|
||||
return fmt.Errorf("raw encryption key must be %d hex characters (32 bytes), got %d", RawKeyHexLen, len(hexStr))
|
||||
}
|
||||
decoded, err := hex.DecodeString(hexStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("raw encryption key is not valid hex: %w", err)
|
||||
}
|
||||
if len(decoded) != PBKDF2KeyLength {
|
||||
return fmt.Errorf("raw encryption key must decode to %d bytes", PBKDF2KeyLength)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(key) < 32 {
|
||||
return fmt.Errorf("encryption key must be at least 32 characters long")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package encryption
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -240,3 +242,95 @@ func TestConstantTimeCompare(t *testing.T) {
|
||||
t.Error("Empty slices should compare equal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveKeyRaw(t *testing.T) {
|
||||
raw, err := GenerateRawKey()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateRawKey: %v", err)
|
||||
}
|
||||
if !IsRawKey(raw) {
|
||||
t.Fatalf("generated key is not raw: %q", raw[:4])
|
||||
}
|
||||
key, err := ResolveKey(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveKey raw: %v", err)
|
||||
}
|
||||
if len(key) != 32 {
|
||||
t.Fatalf("raw key length %d", len(key))
|
||||
}
|
||||
decoded, err := hex.DecodeString(raw[len(RawKeyPrefix):])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(key, decoded) {
|
||||
t.Fatal("raw key was not hex-decoded as-is")
|
||||
}
|
||||
if bytes.Equal(key, DeriveKey(raw)) {
|
||||
t.Fatal("raw key must not go through PBKDF2")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveKeyLegacyHexStillPBKDF2(t *testing.T) {
|
||||
legacy := "a0e3dd20a761b118ca234160dd8b87230a001e332a97c9cfe3b8b9c99efaae03"
|
||||
decoded, err := hex.DecodeString(legacy)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := ResolveKey(legacy)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveKey legacy hex: %v", err)
|
||||
}
|
||||
want := DeriveKey(legacy)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Fatal("unprefixed 64-hex must still use PBKDF2 (old --generate-key configs)")
|
||||
}
|
||||
if bytes.Equal(got, decoded) {
|
||||
t.Fatal("unprefixed 64-hex must not be treated as a raw AES key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveKeyPassphrase(t *testing.T) {
|
||||
pw := "test-passphrase-not-a-hex-key-value"
|
||||
got, err := ResolveKey(pw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(got, DeriveKey(pw)) {
|
||||
t.Fatal("passphrase should use PBKDF2")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeriveKeyCached(t *testing.T) {
|
||||
pw := "cache-me-please-this-is-long-enough"
|
||||
start := time.Now()
|
||||
k1 := DeriveKey(pw)
|
||||
first := time.Since(start)
|
||||
start = time.Now()
|
||||
k2 := DeriveKey(pw)
|
||||
second := time.Since(start)
|
||||
if !bytes.Equal(k1, k2) {
|
||||
t.Fatal("cached key mismatch")
|
||||
}
|
||||
if first < 10*time.Millisecond {
|
||||
t.Logf("first PBKDF2 unexpectedly fast: %v", first)
|
||||
}
|
||||
if second > 5*time.Millisecond {
|
||||
t.Fatalf("cached DeriveKey too slow: first=%v second=%v", first, second)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRawEncryptionKey(t *testing.T) {
|
||||
raw, err := GenerateRawKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ValidateEncryptionKey(raw); err != nil {
|
||||
t.Fatalf("valid raw key rejected: %v", err)
|
||||
}
|
||||
if err := ValidateEncryptionKey("raw:not-hex"); err == nil {
|
||||
t.Fatal("invalid raw hex should fail")
|
||||
}
|
||||
if err := ValidateEncryptionKey("raw:abcd"); err == nil {
|
||||
t.Fatal("short raw key should fail")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user