Derive a Fernet instance from `SECRET_KEY` for session-body confidentiality. Layer 1 of the on-disk session protection. The HMAC header (added in PR 1) protects integrity of the on-disk bytes; this Fernet layer additionally protects confidentiality so that a leak of `sessions/`
(secret)
| 79 | |
| 80 | |
| 81 | def _derive_session_fernet(secret): |
| 82 | """Derive a Fernet instance from `SECRET_KEY` for session-body |
| 83 | confidentiality. |
| 84 | |
| 85 | Layer 1 of the on-disk session protection. The HMAC header (added in |
| 86 | PR 1) protects integrity of the on-disk bytes; this Fernet layer |
| 87 | additionally protects confidentiality so that a leak of `sessions/` |
| 88 | files alone (without the SECRET_KEY in `pgadmin4.db`) does not |
| 89 | expose OAuth tokens, cloud credentials, MFA OTPs, `pass_enc_key` |
| 90 | (the KEK that decrypts saved server passwords), or any other |
| 91 | sensitive material the application places in the session. |
| 92 | |
| 93 | Caveat (real, not theoretical): SECRET_KEY currently lives in |
| 94 | `pgadmin4.db` in the same DATA_DIR. A leak that includes BOTH |
| 95 | `sessions/` and `pgadmin4.db` recovers the derived Fernet key and |
| 96 | can decrypt session bodies. Closing this gap requires moving |
| 97 | SECRET_KEY out of DATA_DIR (e.g., into the OS keychain via |
| 98 | `USE_OS_SECRET_STORAGE`); tracked as Layer 2 follow-up. |
| 99 | |
| 100 | HKDF with a fixed salt and info string is used so the derivation is |
| 101 | deterministic across processes (multiple workers must produce the |
| 102 | same key) and so a future format change can swap in a new |
| 103 | salt/info without compromising existing files. |
| 104 | """ |
| 105 | key_material = secret.encode() if isinstance(secret, str) else secret |
| 106 | derived = HKDF( |
| 107 | algorithm=hashes.SHA256(), |
| 108 | length=32, |
| 109 | salt=_SESSION_FERNET_SALT, |
| 110 | info=_SESSION_FERNET_INFO, |
| 111 | ).derive(key_material) |
| 112 | return Fernet(base64.urlsafe_b64encode(derived)) |
| 113 | |
| 114 | |
| 115 | def _open_session_file(path): |
no outgoing calls