Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
253
native/desktop/maplefile/internal/config/integrity.go
Normal file
253
native/desktop/maplefile/internal/config/integrity.go
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
// integrityKeyFile is the filename for the integrity key
|
||||
integrityKeyFile = ".config_key"
|
||||
// integrityKeyLength is the length of the HMAC key in bytes
|
||||
integrityKeyLength = 32
|
||||
// hmacField is the JSON field name for the HMAC signature
|
||||
hmacField = "_integrity"
|
||||
)
|
||||
|
||||
// ConfigWithIntegrity wraps a Config with an integrity signature
|
||||
type ConfigWithIntegrity struct {
|
||||
Config
|
||||
Integrity string `json:"_integrity,omitempty"`
|
||||
}
|
||||
|
||||
// IntegrityService provides HMAC-based integrity verification for config files
|
||||
type IntegrityService struct {
|
||||
keyPath string
|
||||
key []byte
|
||||
}
|
||||
|
||||
// NewIntegrityService creates a new integrity service for the given app
|
||||
func NewIntegrityService(appName string) (*IntegrityService, error) {
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get config directory: %w", err)
|
||||
}
|
||||
|
||||
appConfigDir := filepath.Join(configDir, appName)
|
||||
keyPath := filepath.Join(appConfigDir, integrityKeyFile)
|
||||
|
||||
svc := &IntegrityService{
|
||||
keyPath: keyPath,
|
||||
}
|
||||
|
||||
// Load or generate key
|
||||
if err := svc.loadOrGenerateKey(); err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize integrity key: %w", err)
|
||||
}
|
||||
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
// loadOrGenerateKey loads the HMAC key from file or generates a new one
|
||||
func (s *IntegrityService) loadOrGenerateKey() error {
|
||||
// Try to load existing key
|
||||
data, err := os.ReadFile(s.keyPath)
|
||||
if err == nil {
|
||||
// Key exists, decode it
|
||||
s.key, err = base64.StdEncoding.DecodeString(string(data))
|
||||
if err != nil || len(s.key) != integrityKeyLength {
|
||||
// Invalid key, regenerate
|
||||
return s.generateNewKey()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to read integrity key: %w", err)
|
||||
}
|
||||
|
||||
// Key doesn't exist, generate new one
|
||||
return s.generateNewKey()
|
||||
}
|
||||
|
||||
// generateNewKey generates a new HMAC key and saves it
|
||||
func (s *IntegrityService) generateNewKey() error {
|
||||
s.key = make([]byte, integrityKeyLength)
|
||||
if _, err := rand.Read(s.key); err != nil {
|
||||
return fmt.Errorf("failed to generate key: %w", err)
|
||||
}
|
||||
|
||||
// Ensure directory exists with restrictive permissions
|
||||
if err := os.MkdirAll(filepath.Dir(s.keyPath), 0700); err != nil {
|
||||
return fmt.Errorf("failed to create key directory: %w", err)
|
||||
}
|
||||
|
||||
// Save key with restrictive permissions (owner read only)
|
||||
encoded := base64.StdEncoding.EncodeToString(s.key)
|
||||
if err := os.WriteFile(s.keyPath, []byte(encoded), 0400); err != nil {
|
||||
return fmt.Errorf("failed to save integrity key: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ComputeHMAC computes the HMAC signature for config data
|
||||
func (s *IntegrityService) ComputeHMAC(config *Config) (string, error) {
|
||||
// Serialize config without the integrity field
|
||||
data, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to serialize config: %w", err)
|
||||
}
|
||||
|
||||
// Compute HMAC-SHA256
|
||||
h := hmac.New(sha256.New, s.key)
|
||||
h.Write(data)
|
||||
signature := h.Sum(nil)
|
||||
|
||||
return base64.StdEncoding.EncodeToString(signature), nil
|
||||
}
|
||||
|
||||
// VerifyHMAC verifies the HMAC signature of config data
|
||||
func (s *IntegrityService) VerifyHMAC(config *Config, providedHMAC string) error {
|
||||
// Compute expected HMAC
|
||||
expectedHMAC, err := s.ComputeHMAC(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to compute HMAC: %w", err)
|
||||
}
|
||||
|
||||
// Decode provided HMAC
|
||||
providedBytes, err := base64.StdEncoding.DecodeString(providedHMAC)
|
||||
if err != nil {
|
||||
return errors.New("invalid HMAC format")
|
||||
}
|
||||
|
||||
expectedBytes, err := base64.StdEncoding.DecodeString(expectedHMAC)
|
||||
if err != nil {
|
||||
return errors.New("internal error computing HMAC")
|
||||
}
|
||||
|
||||
// Constant-time comparison to prevent timing attacks
|
||||
if !hmac.Equal(providedBytes, expectedBytes) {
|
||||
return errors.New("config integrity check failed: file may have been tampered with")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SignConfig adds an HMAC signature to the config
|
||||
func (s *IntegrityService) SignConfig(config *Config) (*ConfigWithIntegrity, error) {
|
||||
signature, err := s.ComputeHMAC(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ConfigWithIntegrity{
|
||||
Config: *config,
|
||||
Integrity: signature,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// VerifyAndExtractConfig verifies the integrity and returns the config
|
||||
func (s *IntegrityService) VerifyAndExtractConfig(configWithInt *ConfigWithIntegrity) (*Config, error) {
|
||||
if configWithInt.Integrity == "" {
|
||||
// No integrity field - config was created before integrity checking was added
|
||||
// Allow it but log a warning (caller should handle this)
|
||||
return &configWithInt.Config, nil
|
||||
}
|
||||
|
||||
// Verify the HMAC
|
||||
if err := s.VerifyHMAC(&configWithInt.Config, configWithInt.Integrity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &configWithInt.Config, nil
|
||||
}
|
||||
|
||||
// integrityAwareRepository wraps a repository with integrity checking
|
||||
type integrityAwareRepository struct {
|
||||
inner repository
|
||||
integritySvc *IntegrityService
|
||||
configPath string
|
||||
warnOnMissingMAC bool // If true, allows configs without MAC (for migration)
|
||||
}
|
||||
|
||||
// NewIntegrityAwareRepository creates a repository wrapper with integrity checking
|
||||
func NewIntegrityAwareRepository(inner repository, appName string, configPath string) (repository, error) {
|
||||
integritySvc, err := NewIntegrityService(appName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &integrityAwareRepository{
|
||||
inner: inner,
|
||||
integritySvc: integritySvc,
|
||||
configPath: configPath,
|
||||
warnOnMissingMAC: true, // Allow migration from old configs
|
||||
}, nil
|
||||
}
|
||||
|
||||
// LoadConfig loads and verifies the config
|
||||
func (r *integrityAwareRepository) LoadConfig(ctx context.Context) (*Config, error) {
|
||||
// Check if config file exists
|
||||
if _, err := os.Stat(r.configPath); os.IsNotExist(err) {
|
||||
// Load from inner (will create defaults)
|
||||
config, err := r.inner.LoadConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Save with integrity
|
||||
return config, r.SaveConfig(ctx, config)
|
||||
}
|
||||
|
||||
// Read raw file to check for integrity field
|
||||
data, err := os.ReadFile(r.configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config: %w", err)
|
||||
}
|
||||
|
||||
// Try to parse with integrity field
|
||||
var configWithInt ConfigWithIntegrity
|
||||
if err := json.Unmarshal(data, &configWithInt); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse config: %w", err)
|
||||
}
|
||||
|
||||
// Verify integrity
|
||||
config, err := r.integritySvc.VerifyAndExtractConfig(&configWithInt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If config had no integrity field, save it with one (migration)
|
||||
if configWithInt.Integrity == "" && r.warnOnMissingMAC {
|
||||
// Re-save with integrity
|
||||
_ = r.SaveConfig(ctx, config) // Ignore error, not critical
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// SaveConfig saves the config with integrity signature
|
||||
func (r *integrityAwareRepository) SaveConfig(ctx context.Context, config *Config) error {
|
||||
// Sign the config
|
||||
signedConfig, err := r.integritySvc.SignConfig(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to sign config: %w", err)
|
||||
}
|
||||
|
||||
// Serialize with integrity field
|
||||
data, err := json.MarshalIndent(signedConfig, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to serialize config: %w", err)
|
||||
}
|
||||
|
||||
// Write with restrictive permissions
|
||||
return os.WriteFile(r.configPath, data, 0600)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue