feat(16-03): add GetRawCredentials with 4-source fallback, wrap GetCredentials

- GetRawCredentials resolves credentials: device transit, device legacy, profile transit, profile legacy
- Cache key includes source (device/profile) to prevent cross-source poisoning
- GetCredentials is now a backward-compatible wrapper calling GetRawCredentials + ParseRouterOSCredentials
- Add DecryptRaw to device package for raw byte decryption without JSON parsing
- Invalidate clears both parsed and raw cache entries
- All existing callers (PollDevice, CmdResponder, TunnelResponder, BackupResponder, SSHRelay) unchanged

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jason Staack
2026-03-21 18:28:56 -05:00
parent b3dbd1e6b9
commit 89d904505d
2 changed files with 160 additions and 83 deletions

View File

@@ -14,6 +14,38 @@ type credentialsJSON struct {
Password string `json:"password"`
}
// DecryptRaw decrypts AES-256-GCM encrypted data and returns the raw plaintext bytes.
// Used by GetRawCredentials to obtain credential JSON before type-specific parsing.
// The ciphertext layout is the same as described in DecryptCredentials.
func DecryptRaw(ciphertext []byte, key []byte) ([]byte, error) {
if len(key) != 32 {
return nil, fmt.Errorf("encryption key must be 32 bytes, got %d", len(key))
}
if len(ciphertext) < 12+16 {
return nil, fmt.Errorf("ciphertext too short: need at least 28 bytes (12 nonce + 16 tag), got %d", len(ciphertext))
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, fmt.Errorf("creating AES cipher: %w", err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("creating GCM cipher: %w", err)
}
nonce := ciphertext[:12]
encryptedData := ciphertext[12:]
plaintext, err := gcm.Open(nil, nonce, encryptedData, nil)
if err != nil {
return nil, fmt.Errorf("decrypting credentials (wrong key or tampered data): %w", err)
}
return plaintext, nil
}
// DecryptCredentials decrypts AES-256-GCM encrypted credentials and returns the
// username and password stored within.
//