# SOPS Decryption Cache System # Caches decrypted SOPS content with strict 0600 permissions # SECURITY: All SOPS cache files must have 0600 permissions # TTL: 15 minutes (configurable, balances security and performance) # Follows Nushell 0.109.0+ guidelines # Selective imports (ADR-025 Phase 3 Layer 2). # cache/metadata star-import was dead — dropped. use domain/cache/core.nu [cache-clear-type cache-lookup cache-write] # Helper: Compute hash of SOPS file path def compute-sops-hash [file_path: string] { let hash_result = (do { $file_path | ^openssl dgst -sha256 -hex } | complete) if $hash_result.exit_code == 0 { ($hash_result.stdout | str trim | split column " " | get column1 | get 0) } else { ($file_path | hash md5 | str substring 0..32) } } # Helper: Set file permissions to 0600 (owner read-write only) def set-sops-file-permissions [cache_file: string] { do { ^chmod 0600 $cache_file } | complete | ignore } # Helper: Set directory permissions to 0700 (owner read-write-execute) def set-sops-dir-permissions [cache_dir: string] { do { ^chmod 0700 $cache_dir } | complete | ignore } # Helper: Validate file permissions def validate-sops-permissions [cache_file: string] { let stat_result = (do { if ($cache_file | path exists) { $cache_file | stat } else { error make { msg: "File not found" } } } | complete) if $stat_result.exit_code != 0 { return { valid: false, reason: "file_stat_error" } } let perms = ($stat_result.stdout | get permissions) # Check for 0600 permissions (read/write for owner only) # Acceptable: -rw------- or similar if ((not ($perms | str contains "rw")) or (($perms | str length) > 1 and (($perms | str substring 4..-1) != "------"))) { return { valid: false, reason: "permissions_too_permissive" } } { valid: true, reason: "permissions_valid" } } # ============================================================================ # PUBLIC API: SOPS Cache Operations # ============================================================================ # Cache decrypted SOPS content with security export def cache-sops-decrypt [ file_path: string decrypted_content: string ] { let cache_key = (compute-sops-hash $file_path) let source_files = [$file_path] # Write cache with 15-minute TTL (security sensitive) cache-write "sops" $cache_key $decrypted_content $source_files --ttl 900 # CRITICAL: Set 0600 permissions on cache file let cache_file = (let home = ($env.HOME? | default "~" | path expand); $home | path join ".provisioning" "cache" "config" "sops" $cache_key) if ($cache_file | path exists) { set-sops-file-permissions $cache_file } # Set directory permissions to 0700 let cache_dir = ($cache_file | path dirname) if ($cache_dir | path exists) { set-sops-dir-permissions $cache_dir } } # Lookup cached SOPS decryption export def lookup-sops-cache [ file_path: string ] { if not ($file_path | path exists) { return { valid: false, reason: "file_not_found", data: null } } let cache_key = (compute-sops-hash $file_path) # Try to lookup in cache let cache_result = (cache-lookup "sops" $cache_key) if not $cache_result.valid { return { valid: false, reason: $cache_result.reason, data: null } } # SECURITY: Validate permissions before returning let home = ($env.HOME? | default "~" | path expand) let cache_file = ($home | path join ".provisioning" "cache" "config" "sops" $cache_key) let perm_check = (validate-sops-permissions $cache_file) if not $perm_check.valid { # Permissions compromised - invalidate cache return { valid: false, reason: $"permission_violation: ($perm_check.reason)", data: null } } { valid: true, reason: "cache_hit", data: $cache_result.data } } # Validate SOPS cache (permissions + TTL + mtime) def validate-sops-cache [ cache_file: string ] { # Check permissions let perm_check = (validate-sops-permissions $cache_file) if not $perm_check.valid { return $perm_check } # Load metadata let meta_file = $"($cache_file).meta" let meta_load = (do { open $meta_file } | complete) if $meta_load.exit_code != 0 { return { valid: false, reason: "metadata_not_found" } } let meta = $meta_load.stdout # Check TTL let now = (date now | format date "%Y-%m-%dT%H:%M:%SZ") if $now > $meta.expires_at { return { valid: false, reason: "ttl_expired" } } # Check source file mtime for src_file in $meta.source_files { let current_mtime = (do { if ($src_file | path exists) { $src_file | stat | get modified | into int } else { -1 } } | complete | get stdout) let cached_mtime = ($meta.source_mtimes | get --optional $src_file | default (-1)) if $current_mtime != $cached_mtime { return { valid: false, reason: "sops_file_modified" } } } { valid: true, reason: "validation_passed" } } # Clear SOPS cache (security cleanup) export def clear-sops-cache [ --pattern: string = "*" # Optional: clear specific pattern ] { cache-clear-type "sops" } # Rotate SOPS cache (refresh permissions and TTL) export def rotate-sops-cache [ file_path: string decrypted_content: string ] { # Clear old cache let old_cache_key = (compute-sops-hash $file_path) let home = ($env.HOME? | default "~" | path expand) let old_cache_file = ($home | path join ".provisioning" "cache" "config" "sops" $old_cache_key) do { rm -f $old_cache_file rm -f $"($old_cache_file).meta" } | complete | ignore # Write new cache with refreshed TTL cache-sops-decrypt $file_path $decrypted_content } # Get SOPS cache statistics export def get-sops-cache-stats [] { let home = ($env.HOME? | default "~" | path expand) let base = ($home | path join ".provisioning" "cache" "config" "sops") if not ($base | path exists) { return { total_entries: 0, total_size_mb: 0, permission_errors: 0 } } mut stats = { total_entries: 0, total_size_mb: 0, permission_errors: 0 } for meta_file in (glob $"($base)/**/*.meta") { let cache_file = ($meta_file | str substring 0..-6) if ($cache_file | path exists) { # Check permissions let perm_check = (validate-sops-permissions $cache_file) if not $perm_check.valid { $stats.permission_errors += 1 } let size_result = (do { $cache_file | stat | get size } | complete) if $size_result.exit_code == 0 { let size_mb = ($size_result.stdout / 1048576) $stats.total_entries += 1 $stats.total_size_mb += $size_mb } } } $stats } # Enforce permissions on all SOPS cache files (security audit) export def enforce-sops-permissions [] { let home = ($env.HOME? | default "~" | path expand) let base = ($home | path join ".provisioning" "cache" "config" "sops") if not ($base | path exists) { return } # Enforce directory permissions set-sops-dir-permissions $base # Enforce file permissions for cache_file in (glob $"($base)/*" --no-dir) { if ($cache_file | path exists) { set-sops-file-permissions $cache_file } } }