feat(v1.1.7-secrets): secrets SDK + table + admin API + dashboard

Encrypted per-app secrets, reachable from scripts as
secrets::{get,set,delete,list}(name) and managed from the dashboard
Secrets tab. Values are AES-256-GCM-sealed with the process master key
(picloud_shared::crypto) before they touch Postgres; the repo only ever
sees ciphertext + nonce. JSON round-trip preserves Rhai types.

- migration 0023_secrets.sql (PRIMARY KEY (app_id, name)).
- SecretsService trait (picloud-shared) + SecretsServiceImpl + repo
  (manager-core), wired into the Services bundle and Rhai engine.
- Capability::AppSecretsRead/Write (→ script:read / script:write); no
  new Scope variants (seven-scope commitment).
- Admin API GET/POST/DELETE /apps/{id}/secrets (list returns names +
  updated_at, never values).
- build_app now takes a MasterKey, sourced from PICLOUD_SECRET_KEY in
  main.rs; test callers pass a fixed test key.
- 64 KB value cap (PICLOUD_SECRET_MAX_VALUE_BYTES); no ServiceEvent
  emission (secret writes don't fire triggers, by design).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-04 21:37:17 +02:00
parent dc2e4fa01f
commit 2d11090d1a
28 changed files with 1959 additions and 35 deletions

View File

@@ -92,7 +92,11 @@ pub fn encrypt(plaintext: &[u8], key: &[u8; KEY_LEN]) -> EncryptResult {
/// Returns [`CryptoError::InvalidNonce`] if `nonce` is the wrong length,
/// or [`CryptoError::Decrypt`] if authentication fails for any reason
/// (wrong key, corruption, tampering).
pub fn decrypt(ciphertext: &[u8], nonce: &[u8], key: &[u8; KEY_LEN]) -> Result<Vec<u8>, CryptoError> {
pub fn decrypt(
ciphertext: &[u8],
nonce: &[u8],
key: &[u8; KEY_LEN],
) -> Result<Vec<u8>, CryptoError> {
if nonce.len() != NONCE_LEN {
return Err(CryptoError::InvalidNonce(nonce.len()));
}
@@ -238,7 +242,7 @@ mod tests {
fn test_key() -> [u8; KEY_LEN] {
let mut k = [0u8; KEY_LEN];
for (i, b) in k.iter_mut().enumerate() {
*b = i as u8;
*b = u8::try_from(i).unwrap_or(0);
}
k
}