create-workframe 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -201
- package/NOTICE +12 -12
- package/README.md +8 -92
- package/SECURITY.md +40 -40
- package/bin/workframe.js +329 -329
- package/docs/workspace-instructions/WORKFRAME_ONBOARDING.md +1 -1
- package/docs/workspace-instructions/WORKFRAME_ROUTING.md +8 -8
- package/package.json +3 -6
- package/profiles/architect/AGENTS.md +29 -29
- package/profiles/architect/SOUL.md +2 -2
- package/profiles/architect/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/designer/AGENTS.md +26 -26
- package/profiles/designer/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/dev/AGENTS.md +28 -28
- package/profiles/dev/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/docs/AGENTS.md +27 -27
- package/profiles/docs/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/research/AGENTS.md +26 -26
- package/profiles/research/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/visionary/AGENTS.md +25 -25
- package/profiles/visionary/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/workframe-agent/AGENTS.md +37 -37
- package/profiles/workframe-agent/skills/devops/botfather/SKILL.md +85 -85
- package/profiles/workframe-agent/skills/devops/kanban-handoff-pattern/SKILL.md +58 -58
- package/profiles/workframe-agent/skills/devops/workframe-cohort/SKILL.md +54 -54
- package/rules/workspace-README.md +5 -5
- package/scripts/apply-update-hermes.sh +17 -17
- package/scripts/apply-update-workframe.sh +77 -77
- package/scripts/bootstrap-workspace-link.sh +8 -8
- package/scripts/bundle-workframe-ui.mjs +3 -3
- package/scripts/compose-docker-host.sh +37 -37
- package/scripts/ensure-compose-host-paths.mjs +51 -51
- package/scripts/fix-zk-encryption-key.sh +35 -35
- package/scripts/lib/install-identity.mjs +212 -212
- package/scripts/restart-gateway-hermes.sh +12 -12
- package/scripts/set-compose-public-url.mjs +92 -92
- package/scripts/setup-stack-secrets.sh +50 -50
- package/scripts/sync-canonical-to-package.mjs +8 -7
- package/scripts/verify-public-deploy.sh +105 -105
- package/shared/WORKFRAME_AGENT_LIBRARY.md +17 -17
- package/shared/WORKFRAME_AGENT_OPERATIONS.md +15 -15
- package/shared/WORKFRAME_AGENT_PACKS.json +18 -18
- package/shared/WORKFRAME_AGENT_PACKS.yaml +8 -8
- package/shared/WORKFRAME_SKILL_CURATION.md +4 -4
- package/workframe-api/README.md +28 -28
- package/workframe-api/action_proxy.py +131 -131
- package/workframe-api/auth_rate_limit.py +49 -49
- package/workframe-api/credential_vault.py +445 -445
- package/workframe-api/data/avatar-catalog.json +41 -41
- package/workframe-api/email_sender.py +220 -220
- package/workframe-api/google_auth.py +90 -90
- package/workframe-api/install_api.py +359 -359
- package/workframe-api/internal_proxy_auth.py +150 -150
- package/workframe-api/llm_proxy.py +277 -277
- package/workframe-api/oidc_jwt.py +108 -108
- package/workframe-api/package.json +12 -13
- package/workframe-api/public/assets/index-DPXu_lGn.css +1 -1
- package/workframe-api/public/assets/index-DYnLrCZZ.js +8 -8
- package/workframe-api/requirements.txt +2 -2
- package/workframe-api/site_meta.py +271 -271
- package/workframe-api/stack_config.py +427 -427
- package/workframe-api/time-bind-chat.py +99 -99
- package/workframe-api/turn_credentials.py +226 -226
- package/workframe-api/updates.py +417 -417
- package/workframe-api/vault_kek.py +159 -159
- package/workframe-api/zk_auth.py +633 -633
- package/workframe-supervisor/Dockerfile +11 -11
- package/workframe-supervisor/server.py +787 -787
- package/workframe-ui/docker/nginx.conf +85 -85
- package/workframe-ui/public/assets/{arc-CBDYvkAF.js → arc-COAT3laO.js} +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-DUyH3hWG.js +1 -0
- package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-XnBRKeW0.js → architectureDiagram-3BPJPVTR-BFjWV24l.js} +1 -1
- package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-VYHUfVhd.js → blockDiagram-GPEHLZMM-DSQLPfrj.js} +1 -1
- package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-BTjUcJpm.js → c4Diagram-AAUBKEIU-DKEHv1t2.js} +1 -1
- package/workframe-ui/public/assets/channel-g7r_RGaY.js +1 -0
- package/workframe-ui/public/assets/{chunk-2J33WTMH-w7uu7R-b.js → chunk-2J33WTMH-DHZg-DUi.js} +1 -1
- package/workframe-ui/public/assets/{chunk-3OPIFGDE-Cb9LtnDX.js → chunk-3OPIFGDE-BB-OYTfp.js} +1 -1
- package/workframe-ui/public/assets/{chunk-4BX2VUAB-DiQ-qCwH.js → chunk-4BX2VUAB-C93q0YIm.js} +1 -1
- package/workframe-ui/public/assets/{chunk-55IACEB6-C-mLFr7z.js → chunk-55IACEB6-MAYniqik.js} +1 -1
- package/workframe-ui/public/assets/{chunk-5ZQYHXKU-DOesfiCI.js → chunk-5ZQYHXKU-ChgN6YJs.js} +1 -1
- package/workframe-ui/public/assets/{chunk-727SXJPM-BJ3oBZuz.js → chunk-727SXJPM-B_FYwdAv.js} +1 -1
- package/workframe-ui/public/assets/{chunk-AQP2D5EJ-CCA6xpGs.js → chunk-AQP2D5EJ-1_Hw_h1A.js} +1 -1
- package/workframe-ui/public/assets/{chunk-BSJP7CBP-a0cMNFb2.js → chunk-BSJP7CBP-CFiDQ1Rv.js} +1 -1
- package/workframe-ui/public/assets/{chunk-CSCIHK7Q-kuqN8EIY.js → chunk-CSCIHK7Q-DZ9UMTlB.js} +1 -1
- package/workframe-ui/public/assets/{chunk-FMBD7UC4-DyPgYHCg.js → chunk-FMBD7UC4-DlMlyFgw.js} +1 -1
- package/workframe-ui/public/assets/{chunk-KSCS5N6A-CdUuvR0V.js → chunk-KSCS5N6A-DHXtQ_Hf.js} +1 -1
- package/workframe-ui/public/assets/{chunk-L5ZTLDWV-Dq9NoWmK.js → chunk-L5ZTLDWV-CuQzg-QG.js} +1 -1
- package/workframe-ui/public/assets/{chunk-LZXEDZCA-p74rddlO.js → chunk-LZXEDZCA-BHzjzCGg.js} +2 -2
- package/workframe-ui/public/assets/{chunk-ND2GUHAM-DBD2u1Gz.js → chunk-ND2GUHAM-DHXx05n2.js} +1 -1
- package/workframe-ui/public/assets/{chunk-NZK2D7GU-BeIeYFnd.js → chunk-NZK2D7GU-CV5pmDM_.js} +1 -1
- package/workframe-ui/public/assets/{chunk-O5CBEL6O-ClHc56ib.js → chunk-O5CBEL6O-6tkCHxsV.js} +1 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-C5UQehWY.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-DhWllrI8.js +1 -0
- package/workframe-ui/public/assets/{chunk-XPW4576I-EFr8R_1p.js → chunk-XPW4576I-BClwIiCp.js} +1 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BBM_8T8E.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BBM_8T8E.js +1 -0
- package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-C7aPBODd.js → cose-bilkent-S5V4N54A-DOrGV6DQ.js} +1 -1
- package/workframe-ui/public/assets/{dagre-BM42HDAG-BdU1Rv-H.js → dagre-BM42HDAG-DXTPvJkX.js} +1 -1
- package/workframe-ui/public/assets/{diagram-2AECGRRQ-DWowSo85.js → diagram-2AECGRRQ-xX_v-pbf.js} +1 -1
- package/workframe-ui/public/assets/{diagram-5GNKFQAL-MnxBbceO.js → diagram-5GNKFQAL-Cd2pXbBe.js} +1 -1
- package/workframe-ui/public/assets/{diagram-KO2AKTUF-DQaLRXFf.js → diagram-KO2AKTUF-Df3XvUtk.js} +1 -1
- package/workframe-ui/public/assets/{diagram-LMA3HP47-CQaBud9k.js → diagram-LMA3HP47-CsijIPaD.js} +1 -1
- package/workframe-ui/public/assets/{diagram-OG6HWLK6-D8bAXbY9.js → diagram-OG6HWLK6-aq5fmfHd.js} +1 -1
- package/workframe-ui/public/assets/{dist-DGpTLHr_.js → dist-D1c0mkbB.js} +1 -1
- package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-1E-xSvBK.js → erDiagram-TEJ5UH35-DnFysVRY.js} +1 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-Ci8mdb44.js +1 -0
- package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-CgOVD5hu.js → flowDiagram-I6XJVG4X-C6Ebi3su.js} +1 -1
- package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-JFYAIauo.js → ganttDiagram-6RSMTGT7-BQXQtUpa.js} +1 -1
- package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-B9REenIl.js → gitGraph-WXDBUCRP-Dt0zIs_M.js} +1 -1
- package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-BQ7NcMSn.js → gitGraphDiagram-PVQCEYII-BF8gHzRn.js} +1 -1
- package/workframe-ui/public/assets/index-DpoUZAxh.css +1 -0
- package/workframe-ui/public/assets/{index-Dnw6vjqb.js → index-lRpzpNPT.js} +2 -2
- package/workframe-ui/public/assets/{info-J43DQDTF-CL6-eTjH.js → info-J43DQDTF-CSmszQJT.js} +1 -1
- package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-LJTODW4W.js → infoDiagram-5YYISTIA-CVTKGW6p.js} +1 -1
- package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-bchrQVuo.js → ishikawaDiagram-YF4QCWOH-Z8pT09Lv.js} +1 -1
- package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-DkrvYuxP.js → journeyDiagram-JHISSGLW-r3wD68_T.js} +1 -1
- package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-DFRbj0IG.js → kanban-definition-UN3LZRKU-Il8VglqN.js} +1 -1
- package/workframe-ui/public/assets/{line-Vd48P7-O.js → line-oyjpfz2A.js} +1 -1
- package/workframe-ui/public/assets/{linear-Ckizh2G7.js → linear-Cf7p5tVp.js} +1 -1
- package/workframe-ui/public/assets/{mermaid-parser.core-Bkimsnqj.js → mermaid-parser.core-YmbZ-AfY.js} +2 -2
- package/workframe-ui/public/assets/{mermaid.core-x0TvVuPo.js → mermaid.core-BFdCAqCo.js} +3 -3
- package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-6ykAFPEz.js → mindmap-definition-RKZ34NQL-Cy2iCtEl.js} +1 -1
- package/workframe-ui/public/assets/{packet-YPE3B663-Dw3xgMDt.js → packet-YPE3B663-DwOBZL6K.js} +1 -1
- package/workframe-ui/public/assets/{pie-LRSECV5Y-DATysawG.js → pie-LRSECV5Y-04PPhnKK.js} +1 -1
- package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-SJKD1S0S.js → pieDiagram-4H26LBE5-LxIpgHqi.js} +1 -1
- package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-BrYDZX8q.js → quadrantDiagram-W4KKPZXB-0nBYfYm4.js} +1 -1
- package/workframe-ui/public/assets/{radar-GUYGQ44K-BmWYPCds.js → radar-GUYGQ44K-D2-vBqps.js} +1 -1
- package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-DwL9Mc8e.js → requirementDiagram-4Y6WPE33-DbuU0nlu.js} +1 -1
- package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-DYIFsL8h.js → sankeyDiagram-5OEKKPKP-B2hQ6B2x.js} +1 -1
- package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-0-FPkFk8.js → sequenceDiagram-3UESZ5HK-BBrU30e1.js} +1 -1
- package/workframe-ui/public/assets/{src-B_od6b6h.js → src-BJEDmV70.js} +1 -1
- package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BQCiBk6u.js → stateDiagram-AJRCARHV-7FGO4kkH.js} +1 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-DLTSizMg.js +1 -0
- package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-DS3tFcXj.js → timeline-definition-PNZ67QCA-ptDm4rCN.js} +1 -1
- package/workframe-ui/public/assets/{treeView-BLDUP644-DSyUCKLY.js → treeView-BLDUP644-CS6Z-0q8.js} +1 -1
- package/workframe-ui/public/assets/{treemap-LRROVOQU-CEZaNh5Y.js → treemap-LRROVOQU-DqV4Y2VA.js} +1 -1
- package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-CD-Vc9NF.js → vennDiagram-CIIHVFJN-C0UrZJYt.js} +1 -1
- package/workframe-ui/public/assets/{wardley-L42UT6IY-Drq5w1Mc.js → wardley-L42UT6IY-bNDN3_Sa.js} +1 -1
- package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DouXDJoF.js → wardleyDiagram-YWT4CUSO-jWiJsefM.js} +1 -1
- package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-DDf_Lol5.js → xychartDiagram-2RQKCTM6-Dsh_fLCy.js} +1 -1
- package/workframe-ui/public/favicon.svg +7 -7
- package/workframe-ui/public/index.html +50 -50
- package/workframe-ui/public/workframe-config.json +3 -3
- package/scripts/security_audit.py +0 -156
- package/scripts/test-scaffold.mjs +0 -390
- package/workframe-api/tests/__init__.py +0 -0
- package/workframe-api/tests/db_setup.py +0 -13
- package/workframe-api/tests/test_admin_updates_gated.py +0 -30
- package/workframe-api/tests/test_agent_dm_bootstrap.py +0 -196
- package/workframe-api/tests/test_agent_profile_sync.py +0 -76
- package/workframe-api/tests/test_auth_email.py +0 -222
- package/workframe-api/tests/test_auth_hole_fix_selfcheck.py +0 -99
- package/workframe-api/tests/test_auth_rate_limit.py +0 -19
- package/workframe-api/tests/test_avatar_resolve.py +0 -77
- package/workframe-api/tests/test_child_soul_template.py +0 -71
- package/workframe-api/tests/test_credential_canary.py +0 -135
- package/workframe-api/tests/test_credential_isolation.py +0 -448
- package/workframe-api/tests/test_credential_resolution.py +0 -206
- package/workframe-api/tests/test_device_oauth.py +0 -108
- package/workframe-api/tests/test_doctor_repair.py +0 -103
- package/workframe-api/tests/test_ensure_profile_api.py +0 -77
- package/workframe-api/tests/test_gateway_compose_security.py +0 -136
- package/workframe-api/tests/test_install_secure_host.py +0 -39
- package/workframe-api/tests/test_internal_proxy_auth.py +0 -125
- package/workframe-api/tests/test_invite_runtime_bootstrap.py +0 -72
- package/workframe-api/tests/test_kanban_delegation.py +0 -185
- package/workframe-api/tests/test_llm_proxy.py +0 -155
- package/workframe-api/tests/test_login_access_policy.py +0 -183
- package/workframe-api/tests/test_mvp_model_bootstrap.py +0 -75
- package/workframe-api/tests/test_onboarding_bootstrap.py +0 -248
- package/workframe-api/tests/test_platform_auth.py +0 -47
- package/workframe-api/tests/test_profile_config_path.py +0 -56
- package/workframe-api/tests/test_profile_config_yaml_repair.py +0 -63
- package/workframe-api/tests/test_profile_create.py +0 -72
- package/workframe-api/tests/test_profile_identity_overlay.py +0 -61
- package/workframe-api/tests/test_profile_install_health.py +0 -45
- package/workframe-api/tests/test_profile_secret_policy.py +0 -57
- package/workframe-api/tests/test_profile_workspace_cwd.py +0 -34
- package/workframe-api/tests/test_provider_bootstrap.py +0 -75
- package/workframe-api/tests/test_provider_connect.py +0 -54
- package/workframe-api/tests/test_room_crud.py +0 -192
- package/workframe-api/tests/test_room_tenancy.py +0 -701
- package/workframe-api/tests/test_runtime_identity_backfill.py +0 -34
- package/workframe-api/tests/test_site_meta.py +0 -81
- package/workframe-api/tests/test_soul_stub.py +0 -42
- package/workframe-api/tests/test_space_member_sync.py +0 -99
- package/workframe-api/tests/test_stripe_stack_config.py +0 -37
- package/workframe-api/tests/test_supervisor_lifecycle.py +0 -52
- package/workframe-api/tests/test_turn_credential_vault.py +0 -125
- package/workframe-api/tests/test_updates.py +0 -176
- package/workframe-api/tests/test_user_cohort.py +0 -113
- package/workframe-api/tests/test_vault_envelope.py +0 -110
- package/workframe-api/tests/test_workspace_members.py +0 -183
- package/workframe-api/tests/test_workspace_messaging_sync.py +0 -125
- package/workframe-api/tests/test_workspace_provider_list.py +0 -57
- package/workframe-supervisor/tests/test_exec_guard.py +0 -42
- package/workframe-supervisor/tests/test_server_import.py +0 -21
- package/workframe-ui/public/assets/architecture-7EHR7CIX-CtbQKTuT.js +0 -1
- package/workframe-ui/public/assets/channel-Dy4Z4-jn.js +0 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-CtBEchFK.js +0 -1
- package/workframe-ui/public/assets/chunk-WU5MYG2G-B9pBtriN.js +0 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BMAEA8jI.js +0 -1
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BMAEA8jI.js +0 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-D75cstNT.js +0 -1
- package/workframe-ui/public/assets/index-DpAGxump.css +0 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-B89jAMFF.js +0 -1
|
@@ -1,159 +1,159 @@
|
|
|
1
|
-
"""Vault master key (KEK) — in-memory only; separate from ZK_AUTH_ENCRYPTION_KEY."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import base64
|
|
6
|
-
import hashlib
|
|
7
|
-
import json
|
|
8
|
-
import os
|
|
9
|
-
import secrets
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
from typing import Any
|
|
12
|
-
|
|
13
|
-
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
14
|
-
|
|
15
|
-
DATA_DIR = Path(os.environ.get("WORKFRAME_API_DATA_DIR", "/app/data"))
|
|
16
|
-
VAULT_KEK_FILE = DATA_DIR / ".vault_kek"
|
|
17
|
-
META_TABLE = "vault_meta"
|
|
18
|
-
|
|
19
|
-
_KEK: bytes | None = None
|
|
20
|
-
_PBKDF2_ROUNDS = 200_000
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def _b64e(raw: bytes) -> str:
|
|
24
|
-
return base64.b64encode(raw).decode("ascii")
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def _b64d(value: str) -> bytes:
|
|
28
|
-
raw = base64.b64decode(str(value or "").strip())
|
|
29
|
-
if len(raw) != 32:
|
|
30
|
-
raise ValueError("vault KEK must be 32 bytes")
|
|
31
|
-
return raw
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _derive_wrap_key(passphrase: str, salt: bytes) -> bytes:
|
|
35
|
-
phrase = str(passphrase or "")
|
|
36
|
-
if len(phrase) < 12:
|
|
37
|
-
raise ValueError("passphrase must be at least 12 characters")
|
|
38
|
-
return hashlib.pbkdf2_hmac("sha256", phrase.encode("utf-8"), salt, _PBKDF2_ROUNDS, dklen=32)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def _aes_gcm_encrypt(key: bytes, plaintext: bytes) -> dict[str, str]:
|
|
42
|
-
iv = os.urandom(12)
|
|
43
|
-
ct = AESGCM(key).encrypt(iv, plaintext, None)
|
|
44
|
-
return {
|
|
45
|
-
"v": 1,
|
|
46
|
-
"alg": "AES-256-GCM",
|
|
47
|
-
"iv": _b64e(iv),
|
|
48
|
-
"tag": _b64e(ct[-16:]),
|
|
49
|
-
"ciphertext": _b64e(ct[:-16]),
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def _aes_gcm_decrypt(key: bytes, payload: dict[str, Any]) -> bytes:
|
|
54
|
-
if int(payload.get("v") or 0) != 1 or str(payload.get("alg") or "") != "AES-256-GCM":
|
|
55
|
-
raise ValueError("unsupported wrap payload")
|
|
56
|
-
iv = base64.b64decode(str(payload["iv"]))
|
|
57
|
-
tag = base64.b64decode(str(payload["tag"]))
|
|
58
|
-
ct = base64.b64decode(str(payload["ciphertext"]))
|
|
59
|
-
return AESGCM(key).decrypt(iv, ct + tag, None)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def kek_in_memory() -> bool:
|
|
63
|
-
return _KEK is not None
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def get_kek() -> bytes:
|
|
67
|
-
if _KEK is None:
|
|
68
|
-
raise RuntimeError("vault_sealed")
|
|
69
|
-
return _KEK
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def set_kek(raw: bytes) -> None:
|
|
73
|
-
global _KEK
|
|
74
|
-
if len(raw) != 32:
|
|
75
|
-
raise ValueError("KEK must be 32 bytes")
|
|
76
|
-
_KEK = bytes(raw)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def clear_kek() -> None:
|
|
80
|
-
global _KEK
|
|
81
|
-
_KEK = None
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def load_kek_from_env() -> bool:
|
|
85
|
-
raw = os.environ.get("WORKFRAME_VAULT_KEK", "").strip()
|
|
86
|
-
if not raw:
|
|
87
|
-
return False
|
|
88
|
-
set_kek(_b64d(raw))
|
|
89
|
-
return True
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def load_kek_from_file() -> bool:
|
|
93
|
-
if not VAULT_KEK_FILE.is_file():
|
|
94
|
-
return False
|
|
95
|
-
try:
|
|
96
|
-
text = VAULT_KEK_FILE.read_text(encoding="utf-8").strip()
|
|
97
|
-
payload = json.loads(text) if text.startswith("{") else {"key": text}
|
|
98
|
-
key_b64 = str(payload.get("key") or text).strip()
|
|
99
|
-
set_kek(_b64d(key_b64))
|
|
100
|
-
return True
|
|
101
|
-
except Exception:
|
|
102
|
-
return False
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def persist_kek_file() -> None:
|
|
106
|
-
"""Persist in-memory KEK for single-tenant bootstrap (chmod 600)."""
|
|
107
|
-
if _KEK is None:
|
|
108
|
-
return
|
|
109
|
-
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
110
|
-
VAULT_KEK_FILE.write_text(
|
|
111
|
-
json.dumps({"v": 1, "key": _b64e(_KEK)}),
|
|
112
|
-
encoding="utf-8",
|
|
113
|
-
)
|
|
114
|
-
try:
|
|
115
|
-
os.chmod(VAULT_KEK_FILE, 0o600)
|
|
116
|
-
except OSError:
|
|
117
|
-
pass
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def generate_and_persist_kek() -> bytes:
|
|
121
|
-
set_kek(os.urandom(32))
|
|
122
|
-
persist_kek_file()
|
|
123
|
-
return get_kek()
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def wrap_kek_for_passphrase(passphrase: str) -> tuple[str, str]:
|
|
127
|
-
"""Return (salt_b64, wrapped_kek_json) for vault_meta."""
|
|
128
|
-
if _KEK is None:
|
|
129
|
-
raise RuntimeError("vault_sealed")
|
|
130
|
-
salt = os.urandom(16)
|
|
131
|
-
wrap_key = _derive_wrap_key(passphrase, salt)
|
|
132
|
-
wrapped = _aes_gcm_encrypt(wrap_key, _KEK)
|
|
133
|
-
return _b64e(salt), json.dumps(wrapped)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def unwrap_kek_from_passphrase(passphrase: str, salt_b64: str, wrapped_json: str) -> bytes:
|
|
137
|
-
salt = base64.b64decode(str(salt_b64 or "").strip())
|
|
138
|
-
wrap_key = _derive_wrap_key(passphrase, salt)
|
|
139
|
-
payload = json.loads(wrapped_json)
|
|
140
|
-
return _aes_gcm_decrypt(wrap_key, payload)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def unseal_for_tests() -> None:
|
|
144
|
-
"""Tests: deterministic KEK without touching disk."""
|
|
145
|
-
seed = os.environ.get("WORKFRAME_VAULT_KEK", "").strip()
|
|
146
|
-
if seed:
|
|
147
|
-
set_kek(_b64d(seed))
|
|
148
|
-
return
|
|
149
|
-
set_kek(hashlib.sha256(b"workframe-test-vault-kek").digest())
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if __name__ == "__main__":
|
|
153
|
-
generate_and_persist_kek()
|
|
154
|
-
assert kek_in_memory()
|
|
155
|
-
salt, wrapped = wrap_kek_for_passphrase("test-passphrase-12")
|
|
156
|
-
clear_kek()
|
|
157
|
-
set_kek(unwrap_kek_from_passphrase("test-passphrase-12", salt, wrapped))
|
|
158
|
-
assert kek_in_memory()
|
|
159
|
-
print("vault_kek ok")
|
|
1
|
+
"""Vault master key (KEK) — in-memory only; separate from ZK_AUTH_ENCRYPTION_KEY."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import secrets
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
14
|
+
|
|
15
|
+
DATA_DIR = Path(os.environ.get("WORKFRAME_API_DATA_DIR", "/app/data"))
|
|
16
|
+
VAULT_KEK_FILE = DATA_DIR / ".vault_kek"
|
|
17
|
+
META_TABLE = "vault_meta"
|
|
18
|
+
|
|
19
|
+
_KEK: bytes | None = None
|
|
20
|
+
_PBKDF2_ROUNDS = 200_000
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _b64e(raw: bytes) -> str:
|
|
24
|
+
return base64.b64encode(raw).decode("ascii")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _b64d(value: str) -> bytes:
|
|
28
|
+
raw = base64.b64decode(str(value or "").strip())
|
|
29
|
+
if len(raw) != 32:
|
|
30
|
+
raise ValueError("vault KEK must be 32 bytes")
|
|
31
|
+
return raw
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _derive_wrap_key(passphrase: str, salt: bytes) -> bytes:
|
|
35
|
+
phrase = str(passphrase or "")
|
|
36
|
+
if len(phrase) < 12:
|
|
37
|
+
raise ValueError("passphrase must be at least 12 characters")
|
|
38
|
+
return hashlib.pbkdf2_hmac("sha256", phrase.encode("utf-8"), salt, _PBKDF2_ROUNDS, dklen=32)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _aes_gcm_encrypt(key: bytes, plaintext: bytes) -> dict[str, str]:
|
|
42
|
+
iv = os.urandom(12)
|
|
43
|
+
ct = AESGCM(key).encrypt(iv, plaintext, None)
|
|
44
|
+
return {
|
|
45
|
+
"v": 1,
|
|
46
|
+
"alg": "AES-256-GCM",
|
|
47
|
+
"iv": _b64e(iv),
|
|
48
|
+
"tag": _b64e(ct[-16:]),
|
|
49
|
+
"ciphertext": _b64e(ct[:-16]),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _aes_gcm_decrypt(key: bytes, payload: dict[str, Any]) -> bytes:
|
|
54
|
+
if int(payload.get("v") or 0) != 1 or str(payload.get("alg") or "") != "AES-256-GCM":
|
|
55
|
+
raise ValueError("unsupported wrap payload")
|
|
56
|
+
iv = base64.b64decode(str(payload["iv"]))
|
|
57
|
+
tag = base64.b64decode(str(payload["tag"]))
|
|
58
|
+
ct = base64.b64decode(str(payload["ciphertext"]))
|
|
59
|
+
return AESGCM(key).decrypt(iv, ct + tag, None)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def kek_in_memory() -> bool:
|
|
63
|
+
return _KEK is not None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_kek() -> bytes:
|
|
67
|
+
if _KEK is None:
|
|
68
|
+
raise RuntimeError("vault_sealed")
|
|
69
|
+
return _KEK
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def set_kek(raw: bytes) -> None:
|
|
73
|
+
global _KEK
|
|
74
|
+
if len(raw) != 32:
|
|
75
|
+
raise ValueError("KEK must be 32 bytes")
|
|
76
|
+
_KEK = bytes(raw)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def clear_kek() -> None:
|
|
80
|
+
global _KEK
|
|
81
|
+
_KEK = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def load_kek_from_env() -> bool:
|
|
85
|
+
raw = os.environ.get("WORKFRAME_VAULT_KEK", "").strip()
|
|
86
|
+
if not raw:
|
|
87
|
+
return False
|
|
88
|
+
set_kek(_b64d(raw))
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def load_kek_from_file() -> bool:
|
|
93
|
+
if not VAULT_KEK_FILE.is_file():
|
|
94
|
+
return False
|
|
95
|
+
try:
|
|
96
|
+
text = VAULT_KEK_FILE.read_text(encoding="utf-8").strip()
|
|
97
|
+
payload = json.loads(text) if text.startswith("{") else {"key": text}
|
|
98
|
+
key_b64 = str(payload.get("key") or text).strip()
|
|
99
|
+
set_kek(_b64d(key_b64))
|
|
100
|
+
return True
|
|
101
|
+
except Exception:
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def persist_kek_file() -> None:
|
|
106
|
+
"""Persist in-memory KEK for single-tenant bootstrap (chmod 600)."""
|
|
107
|
+
if _KEK is None:
|
|
108
|
+
return
|
|
109
|
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
VAULT_KEK_FILE.write_text(
|
|
111
|
+
json.dumps({"v": 1, "key": _b64e(_KEK)}),
|
|
112
|
+
encoding="utf-8",
|
|
113
|
+
)
|
|
114
|
+
try:
|
|
115
|
+
os.chmod(VAULT_KEK_FILE, 0o600)
|
|
116
|
+
except OSError:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def generate_and_persist_kek() -> bytes:
|
|
121
|
+
set_kek(os.urandom(32))
|
|
122
|
+
persist_kek_file()
|
|
123
|
+
return get_kek()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def wrap_kek_for_passphrase(passphrase: str) -> tuple[str, str]:
|
|
127
|
+
"""Return (salt_b64, wrapped_kek_json) for vault_meta."""
|
|
128
|
+
if _KEK is None:
|
|
129
|
+
raise RuntimeError("vault_sealed")
|
|
130
|
+
salt = os.urandom(16)
|
|
131
|
+
wrap_key = _derive_wrap_key(passphrase, salt)
|
|
132
|
+
wrapped = _aes_gcm_encrypt(wrap_key, _KEK)
|
|
133
|
+
return _b64e(salt), json.dumps(wrapped)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def unwrap_kek_from_passphrase(passphrase: str, salt_b64: str, wrapped_json: str) -> bytes:
|
|
137
|
+
salt = base64.b64decode(str(salt_b64 or "").strip())
|
|
138
|
+
wrap_key = _derive_wrap_key(passphrase, salt)
|
|
139
|
+
payload = json.loads(wrapped_json)
|
|
140
|
+
return _aes_gcm_decrypt(wrap_key, payload)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def unseal_for_tests() -> None:
|
|
144
|
+
"""Tests: deterministic KEK without touching disk."""
|
|
145
|
+
seed = os.environ.get("WORKFRAME_VAULT_KEK", "").strip()
|
|
146
|
+
if seed:
|
|
147
|
+
set_kek(_b64d(seed))
|
|
148
|
+
return
|
|
149
|
+
set_kek(hashlib.sha256(b"workframe-test-vault-kek").digest())
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
generate_and_persist_kek()
|
|
154
|
+
assert kek_in_memory()
|
|
155
|
+
salt, wrapped = wrap_kek_for_passphrase("test-passphrase-12")
|
|
156
|
+
clear_kek()
|
|
157
|
+
set_kek(unwrap_kek_from_passphrase("test-passphrase-12", salt, wrapped))
|
|
158
|
+
assert kek_in_memory()
|
|
159
|
+
print("vault_kek ok")
|