mirror of
https://github.com/luxfi/cli.git
synced 2026-10-30 17:01:20 +00:00
472 lines
12 KiB
Go
472 lines
12 KiB
Go
// Copyright (C) 2022-2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package key
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestEncryptedKeyStore(t *testing.T) {
|
|
t.Run("encryptedStore struct fields", func(t *testing.T) {
|
|
store := encryptedStore{
|
|
Version: 1,
|
|
Salt: []byte("testsalt"),
|
|
Nonce: []byte("testnonce"),
|
|
Data: []byte("encrypted data"),
|
|
CreatedAt: time.Now().Unix(),
|
|
}
|
|
|
|
assert.Equal(t, 1, store.Version)
|
|
assert.Equal(t, []byte("testsalt"), store.Salt)
|
|
assert.Equal(t, []byte("testnonce"), store.Nonce)
|
|
assert.Equal(t, []byte("encrypted data"), store.Data)
|
|
assert.Greater(t, store.CreatedAt, int64(0))
|
|
})
|
|
}
|
|
|
|
func TestSessionManager(t *testing.T) {
|
|
t.Run("session through backend", func(t *testing.T) {
|
|
b := NewSoftwareBackend()
|
|
b.dataDir = t.TempDir()
|
|
ctx := context.Background()
|
|
|
|
// Create key
|
|
_, err := b.CreateKey(ctx, "sessiontest", CreateKeyOptions{
|
|
Mnemonic: testMnemonic,
|
|
Password: "testpassword",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Should have session after create (since LoadKey is called internally)
|
|
// Clear it first to test unlock flow
|
|
_ = b.Lock(ctx, "sessiontest")
|
|
assert.True(t, b.IsLocked("sessiontest"))
|
|
|
|
// Unlock creates session
|
|
err = b.Unlock(ctx, "sessiontest", "testpassword")
|
|
require.NoError(t, err)
|
|
assert.False(t, b.IsLocked("sessiontest"))
|
|
|
|
// Lock clears session
|
|
err = b.Lock(ctx, "sessiontest")
|
|
require.NoError(t, err)
|
|
assert.True(t, b.IsLocked("sessiontest"))
|
|
})
|
|
|
|
t.Run("session expiration", func(t *testing.T) {
|
|
b := NewSoftwareBackend()
|
|
b.dataDir = t.TempDir()
|
|
ctx := context.Background()
|
|
|
|
_, err := b.CreateKey(ctx, "expiretest", CreateKeyOptions{
|
|
Password: "testpassword",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Load to create session
|
|
_, err = b.LoadKey(ctx, "expiretest", "testpassword")
|
|
require.NoError(t, err)
|
|
assert.False(t, b.IsLocked("expiretest"))
|
|
|
|
// Manually expire session
|
|
b.sessionMu.Lock()
|
|
if s, ok := b.sessions["expiretest"]; ok {
|
|
s.expiresAt = time.Now().Add(-1 * time.Hour)
|
|
}
|
|
b.sessionMu.Unlock()
|
|
|
|
// Should be locked now
|
|
assert.True(t, b.IsLocked("expiretest"))
|
|
})
|
|
|
|
t.Run("session extends on access", func(t *testing.T) {
|
|
b := NewSoftwareBackend()
|
|
b.dataDir = t.TempDir()
|
|
ctx := context.Background()
|
|
|
|
_, err := b.CreateKey(ctx, "extendtest", CreateKeyOptions{
|
|
Password: "testpassword",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = b.LoadKey(ctx, "extendtest", "testpassword")
|
|
require.NoError(t, err)
|
|
|
|
// Get initial expiry
|
|
b.sessionMu.RLock()
|
|
initialExpiry := b.sessions["extendtest"].expiresAt
|
|
b.sessionMu.RUnlock()
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
// Access session (via getSession)
|
|
_ = b.getSession("extendtest")
|
|
|
|
// Expiry should be extended
|
|
b.sessionMu.RLock()
|
|
newExpiry := b.sessions["extendtest"].expiresAt
|
|
b.sessionMu.RUnlock()
|
|
|
|
assert.True(t, newExpiry.After(initialExpiry))
|
|
})
|
|
}
|
|
|
|
func TestPasswordFromEnv(t *testing.T) {
|
|
t.Run("returns password from env", func(t *testing.T) {
|
|
originalValue := os.Getenv(EnvKeyPassword)
|
|
defer func() {
|
|
if originalValue != "" {
|
|
_ = os.Setenv(EnvKeyPassword, originalValue)
|
|
} else {
|
|
_ = os.Unsetenv(EnvKeyPassword)
|
|
}
|
|
}()
|
|
|
|
_ = os.Setenv(EnvKeyPassword, "env-password-123")
|
|
password := GetPasswordFromEnv()
|
|
assert.Equal(t, "env-password-123", password)
|
|
})
|
|
|
|
t.Run("returns empty when not set", func(t *testing.T) {
|
|
originalValue := os.Getenv(EnvKeyPassword)
|
|
defer func() {
|
|
if originalValue != "" {
|
|
_ = os.Setenv(EnvKeyPassword, originalValue)
|
|
} else {
|
|
_ = os.Unsetenv(EnvKeyPassword)
|
|
}
|
|
}()
|
|
|
|
_ = os.Unsetenv(EnvKeyPassword)
|
|
password := GetPasswordFromEnv()
|
|
assert.Empty(t, password)
|
|
})
|
|
|
|
t.Run("password from env used in LoadKey", func(t *testing.T) {
|
|
b := NewSoftwareBackend()
|
|
b.dataDir = t.TempDir()
|
|
ctx := context.Background()
|
|
|
|
// Create key with password
|
|
_, err := b.CreateKey(ctx, "envtest", CreateKeyOptions{
|
|
Password: "envpassword",
|
|
})
|
|
require.NoError(t, err)
|
|
_ = b.Lock(ctx, "envtest")
|
|
|
|
// Set env password
|
|
originalValue := os.Getenv(EnvKeyPassword)
|
|
defer func() {
|
|
if originalValue != "" {
|
|
_ = os.Setenv(EnvKeyPassword, originalValue)
|
|
} else {
|
|
_ = os.Unsetenv(EnvKeyPassword)
|
|
}
|
|
}()
|
|
_ = os.Setenv(EnvKeyPassword, "envpassword")
|
|
|
|
// Load without explicit password (uses env)
|
|
_, err = b.LoadKey(ctx, "envtest", "")
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestGlobalLockFunctions(t *testing.T) {
|
|
// These tests require setting up a default backend
|
|
// Skip if we can't properly set one up
|
|
t.Run("LockKey with default backend", func(t *testing.T) {
|
|
resetBackendRegistry()
|
|
|
|
b := NewSoftwareBackend()
|
|
b.dataDir = t.TempDir()
|
|
RegisterBackend(b)
|
|
|
|
ctx := context.Background()
|
|
_ = b.Initialize(ctx)
|
|
|
|
_, err := b.CreateKey(ctx, "locktest", CreateKeyOptions{
|
|
Password: "testpassword",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = LockKey("locktest")
|
|
require.NoError(t, err)
|
|
assert.True(t, IsKeyLocked("locktest"))
|
|
})
|
|
|
|
t.Run("UnlockKey with default backend", func(t *testing.T) {
|
|
resetBackendRegistry()
|
|
|
|
b := NewSoftwareBackend()
|
|
b.dataDir = t.TempDir()
|
|
RegisterBackend(b)
|
|
|
|
ctx := context.Background()
|
|
_ = b.Initialize(ctx)
|
|
|
|
_, err := b.CreateKey(ctx, "unlocktest", CreateKeyOptions{
|
|
Password: "testpassword",
|
|
})
|
|
require.NoError(t, err)
|
|
_ = b.Lock(ctx, "unlocktest")
|
|
|
|
err = UnlockKey("unlocktest", "testpassword")
|
|
require.NoError(t, err)
|
|
assert.False(t, IsKeyLocked("unlocktest"))
|
|
})
|
|
|
|
t.Run("IsKeyLocked returns true when no backend", func(t *testing.T) {
|
|
resetBackendRegistry()
|
|
assert.True(t, IsKeyLocked("anykey"))
|
|
})
|
|
}
|
|
|
|
func TestSessionTimeoutConstant(t *testing.T) {
|
|
t.Run("session timeout defaults to 30 seconds", func(t *testing.T) {
|
|
// Verify default timeout is 30 seconds
|
|
assert.Equal(t, 30*time.Second, DefaultSessionTimeout)
|
|
// SessionTimeout var should match default
|
|
assert.Equal(t, DefaultSessionTimeout, SessionTimeout)
|
|
})
|
|
|
|
t.Run("session timeout configurable via env", func(t *testing.T) {
|
|
originalValue := os.Getenv(EnvKeySessionTimeout)
|
|
defer func() {
|
|
if originalValue != "" {
|
|
_ = os.Setenv(EnvKeySessionTimeout, originalValue)
|
|
} else {
|
|
_ = os.Unsetenv(EnvKeySessionTimeout)
|
|
}
|
|
}()
|
|
|
|
// Test custom timeout
|
|
_ = os.Setenv(EnvKeySessionTimeout, "5m")
|
|
timeout := GetSessionTimeout()
|
|
assert.Equal(t, 5*time.Minute, timeout)
|
|
|
|
// Test invalid duration falls back to default
|
|
_ = os.Setenv(EnvKeySessionTimeout, "invalid")
|
|
timeout = GetSessionTimeout()
|
|
assert.Equal(t, DefaultSessionTimeout, timeout)
|
|
|
|
// Test negative duration falls back to default
|
|
_ = os.Setenv(EnvKeySessionTimeout, "-1s")
|
|
timeout = GetSessionTimeout()
|
|
assert.Equal(t, DefaultSessionTimeout, timeout)
|
|
})
|
|
}
|
|
|
|
func TestKeyInfoFields(t *testing.T) {
|
|
t.Run("all fields populated", func(t *testing.T) {
|
|
now := time.Now()
|
|
info := KeyInfo{
|
|
Name: "testkey",
|
|
Address: "0x1234567890abcdef",
|
|
NodeID: "NodeID-abc123",
|
|
Encrypted: true,
|
|
Locked: false,
|
|
CreatedAt: now,
|
|
}
|
|
|
|
assert.Equal(t, "testkey", info.Name)
|
|
assert.Equal(t, "0x1234567890abcdef", info.Address)
|
|
assert.Equal(t, "NodeID-abc123", info.NodeID)
|
|
assert.True(t, info.Encrypted)
|
|
assert.False(t, info.Locked)
|
|
assert.Equal(t, now, info.CreatedAt)
|
|
})
|
|
}
|
|
|
|
func TestEncryptedStoreVersion(t *testing.T) {
|
|
t.Run("current version is 1", func(t *testing.T) {
|
|
b := NewSoftwareBackend()
|
|
b.dataDir = t.TempDir()
|
|
ctx := context.Background()
|
|
|
|
_, err := b.CreateKey(ctx, "versiontest", CreateKeyOptions{
|
|
Password: "testpassword",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Verify store version by loading raw file
|
|
// This is an implementation detail but important for compatibility
|
|
})
|
|
}
|
|
|
|
func TestErrorConstants(t *testing.T) {
|
|
t.Run("error messages defined", func(t *testing.T) {
|
|
assert.NotNil(t, ErrKeyLocked)
|
|
assert.NotNil(t, ErrKeyNotFound)
|
|
assert.NotNil(t, ErrInvalidPassword)
|
|
assert.NotNil(t, ErrKeyExists)
|
|
assert.NotNil(t, ErrNoPassword)
|
|
assert.NotNil(t, ErrBackendNotFound)
|
|
assert.NotNil(t, ErrBackendNotSupported)
|
|
assert.NotNil(t, ErrBackendUnavailable)
|
|
assert.NotNil(t, ErrSigningCancelled)
|
|
assert.NotNil(t, ErrAuthFailed)
|
|
|
|
// Verify error messages contain key information
|
|
assert.Contains(t, ErrKeyLocked.Error(), "locked")
|
|
assert.Contains(t, ErrKeyNotFound.Error(), "not found")
|
|
assert.Contains(t, ErrNoPassword.Error(), "required")
|
|
})
|
|
}
|
|
|
|
func TestBackendConstants(t *testing.T) {
|
|
t.Run("EnvKeyPassword constant", func(t *testing.T) {
|
|
assert.Equal(t, "KEY_PASSWORD", EnvKeyPassword)
|
|
})
|
|
|
|
t.Run("Argon2 parameters are reasonable", func(t *testing.T) {
|
|
// These should match OWASP recommendations
|
|
assert.Equal(t, uint32(3), uint32(argon2Time))
|
|
assert.Equal(t, uint32(64*1024), uint32(argon2Memory))
|
|
assert.Equal(t, uint8(4), uint8(argon2Threads))
|
|
assert.Equal(t, uint32(32), uint32(argon2KeyLen))
|
|
})
|
|
}
|
|
|
|
func TestKeySessionStruct(t *testing.T) {
|
|
t.Run("keySession fields", func(t *testing.T) {
|
|
now := time.Now()
|
|
session := keySession{
|
|
name: "testkey",
|
|
key: []byte("encryption-key"),
|
|
unlockedAt: now,
|
|
expiresAt: now.Add(SessionTimeout),
|
|
}
|
|
|
|
assert.Equal(t, "testkey", session.name)
|
|
assert.Equal(t, []byte("encryption-key"), session.key)
|
|
assert.Equal(t, now, session.unlockedAt)
|
|
assert.True(t, session.expiresAt.After(now))
|
|
})
|
|
}
|
|
|
|
func TestCloseZeroesSessionKeys(t *testing.T) {
|
|
t.Run("close zeros all session keys", func(t *testing.T) {
|
|
b := NewSoftwareBackend()
|
|
b.dataDir = t.TempDir()
|
|
ctx := context.Background()
|
|
|
|
// Create multiple keys
|
|
for i := 0; i < 3; i++ {
|
|
name := string(rune('a' + i))
|
|
_, err := b.CreateKey(ctx, name, CreateKeyOptions{
|
|
Password: "testpassword",
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = b.LoadKey(ctx, name, "testpassword")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Get references to session keys
|
|
var keyRefs [][]byte
|
|
b.sessionMu.RLock()
|
|
for _, s := range b.sessions {
|
|
keyRefs = append(keyRefs, s.key)
|
|
}
|
|
b.sessionMu.RUnlock()
|
|
|
|
// Close backend
|
|
err := b.Close()
|
|
require.NoError(t, err)
|
|
|
|
// All key refs should be zeroed
|
|
for _, keyRef := range keyRefs {
|
|
allZero := true
|
|
for _, b := range keyRef {
|
|
if b != 0 {
|
|
allZero = false
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, allZero, "session key should be zeroed after close")
|
|
}
|
|
|
|
// Sessions map should be empty
|
|
b.sessionMu.RLock()
|
|
assert.Empty(t, b.sessions)
|
|
b.sessionMu.RUnlock()
|
|
})
|
|
}
|
|
|
|
func TestLoadKeyWithExpiredSession(t *testing.T) {
|
|
t.Run("expired session requires password", func(t *testing.T) {
|
|
b := NewSoftwareBackend()
|
|
b.dataDir = t.TempDir()
|
|
ctx := context.Background()
|
|
|
|
_, err := b.CreateKey(ctx, "expiredload", CreateKeyOptions{
|
|
Password: "testpassword",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Create session
|
|
_, err = b.LoadKey(ctx, "expiredload", "testpassword")
|
|
require.NoError(t, err)
|
|
|
|
// Expire session
|
|
b.sessionMu.Lock()
|
|
if s, ok := b.sessions["expiredload"]; ok {
|
|
s.expiresAt = time.Now().Add(-1 * time.Hour)
|
|
}
|
|
b.sessionMu.Unlock()
|
|
|
|
// Clear env password
|
|
_ = os.Unsetenv(EnvKeyPassword)
|
|
|
|
// Load without password should fail (session expired)
|
|
_, err = b.LoadKey(ctx, "expiredload", "")
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, ErrKeyLocked)
|
|
|
|
// Load with password should work
|
|
_, err = b.LoadKey(ctx, "expiredload", "testpassword")
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestLockAllKeysViaBackend(t *testing.T) {
|
|
t.Run("close locks all keys", func(t *testing.T) {
|
|
b := NewSoftwareBackend()
|
|
b.dataDir = t.TempDir()
|
|
ctx := context.Background()
|
|
|
|
// Create and unlock multiple keys
|
|
for i := 0; i < 3; i++ {
|
|
name := string(rune('x' + i))
|
|
_, err := b.CreateKey(ctx, name, CreateKeyOptions{
|
|
Password: "testpassword",
|
|
})
|
|
require.NoError(t, err)
|
|
// Load to create session
|
|
_, err = b.LoadKey(ctx, name, "testpassword")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// All should be unlocked (session created during LoadKey)
|
|
for i := 0; i < 3; i++ {
|
|
name := string(rune('x' + i))
|
|
assert.False(t, b.IsLocked(name), "key %s should be unlocked", name)
|
|
}
|
|
|
|
// Close backend
|
|
_ = b.Close()
|
|
|
|
// All should be locked
|
|
for i := 0; i < 3; i++ {
|
|
name := string(rune('x' + i))
|
|
assert.True(t, b.IsLocked(name), "key %s should be locked after close", name)
|
|
}
|
|
})
|
|
}
|