Skip to content

Commit 459d235

Browse files
committed
Add encrypted secret manager and ${secrets.NAME} placeholders
A Fernet-encrypted JSON vault under ~/.je_auto_control/secrets stores named secrets behind a passphrase-derived key (PBKDF2-HMAC-SHA256, 600k iterations). Action scripts reference entries via ${secrets.NAME} in interpolation, keeping plaintext out of the variable scope and out of script JSON. AC_secret_init / unlock / lock / set / remove / list / status drive it from headless code, plus a Secrets GUI tab.
1 parent 7cd90ff commit 459d235

12 files changed

Lines changed: 754 additions & 1 deletion

File tree

je_auto_control/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,11 @@
131131
from je_auto_control.utils.profiler import (
132132
ActionProfiler, ActionStats, default_profiler,
133133
)
134+
# Secrets (headless)
135+
from je_auto_control.utils.secrets import (
136+
SecretManager, SecretStoreError, SecretStoreLocked,
137+
default_secret_manager, default_secret_store_path,
138+
)
134139
# Run history (headless)
135140
from je_auto_control.utils.run_history.history_store import (
136141
HistoryStore, RunRecord, default_history_store,
@@ -308,6 +313,9 @@ def start_autocontrol_gui(*args, **kwargs):
308313
"PixelColorTrigger", "FilePathTrigger",
309314
# Profiler
310315
"ActionProfiler", "ActionStats", "default_profiler",
316+
# Secret manager
317+
"SecretManager", "SecretStoreError", "SecretStoreLocked",
318+
"default_secret_manager", "default_secret_store_path",
311319
# Run history
312320
"HistoryStore", "RunRecord", "default_history_store",
313321
# Accessibility

je_auto_control/gui/language_wrapper/english.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"tab_vlm": "AI Locator",
3636
"tab_ocr_reader": "OCR Reader",
3737
"tab_variables": "Variables",
38+
"tab_secrets": "Secrets",
3839
"tab_llm_planner": "LLM Planner",
3940
"tab_remote_desktop": "Remote Desktop",
4041
"tab_rest_api": "REST API",
@@ -664,6 +665,30 @@
664665
"rh_preview_empty": "Select a run to preview.",
665666
"rh_preview_no_artifact": "No screenshot for this run.",
666667

668+
# Secrets tab
669+
"secret_unlock_group": "Vault",
670+
"secret_manage_group": "Secrets",
671+
"secret_passphrase_label": "Passphrase:",
672+
"secret_passphrase_placeholder": "vault passphrase",
673+
"secret_init": "Create vault",
674+
"secret_init_done": "Vault created and unlocked.",
675+
"secret_unlock": "Unlock",
676+
"secret_lock": "Lock",
677+
"secret_add": "Add secret",
678+
"secret_remove": "Remove",
679+
"secret_change_passphrase": "Change passphrase",
680+
"secret_change_done": "Vault re-encrypted with the new passphrase.",
681+
"secret_status_uninitialized": "Vault not created yet.",
682+
"secret_status_unlocked": "Vault is unlocked.",
683+
"secret_status_locked": "Vault is locked.",
684+
"secret_passphrase_required": "Enter a passphrase first.",
685+
"secret_wrong_passphrase": "Wrong passphrase.",
686+
"secret_unlock_first": "Unlock the vault before managing secrets.",
687+
"secret_name_prompt": "Secret name (use as ${secrets.NAME}):",
688+
"secret_value_prompt": "Secret value:",
689+
"secret_old_passphrase_prompt": "Current passphrase:",
690+
"secret_new_passphrase_prompt": "New passphrase:",
691+
667692
# Profiler tab
668693
"prof_enable": "Enable profiler",
669694
"prof_disable": "Disable profiler",

je_auto_control/gui/language_wrapper/japanese.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"tab_report": "レポート",
3030
"tab_run_history": "実行履歴",
3131
"tab_profiler": "プロファイラ",
32+
"tab_secrets": "シークレット",
3233
"tab_accessibility": "アクセシビリティ",
3334
"tab_vlm": "AI ロケーター",
3435
"tab_ocr_reader": "OCR リーダー",
@@ -662,6 +663,30 @@
662663
"rh_preview_empty": "実行を選択するとプレビューが表示されます。",
663664
"rh_preview_no_artifact": "この実行のスクリーンショットはありません。",
664665

666+
# Secrets tab
667+
"secret_unlock_group": "ボールト",
668+
"secret_manage_group": "シークレット",
669+
"secret_passphrase_label": "パスフレーズ:",
670+
"secret_passphrase_placeholder": "ボールトパスフレーズ",
671+
"secret_init": "ボールトを作成",
672+
"secret_init_done": "ボールトを作成し、解錠しました。",
673+
"secret_unlock": "解錠",
674+
"secret_lock": "ロック",
675+
"secret_add": "シークレットを追加",
676+
"secret_remove": "削除",
677+
"secret_change_passphrase": "パスフレーズ変更",
678+
"secret_change_done": "新しいパスフレーズで再暗号化しました。",
679+
"secret_status_uninitialized": "ボールト未作成。",
680+
"secret_status_unlocked": "ボールト解錠中。",
681+
"secret_status_locked": "ボールトはロック中。",
682+
"secret_passphrase_required": "パスフレーズを入力してください。",
683+
"secret_wrong_passphrase": "パスフレーズが違います。",
684+
"secret_unlock_first": "先にボールトを解錠してください。",
685+
"secret_name_prompt": "シークレット名 (${secrets.NAME} で参照):",
686+
"secret_value_prompt": "シークレット値:",
687+
"secret_old_passphrase_prompt": "現在のパスフレーズ:",
688+
"secret_new_passphrase_prompt": "新しいパスフレーズ:",
689+
665690
# Profiler tab
666691
"prof_enable": "プロファイラを有効化",
667692
"prof_disable": "プロファイラを無効化",

je_auto_control/gui/language_wrapper/simplified_chinese.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"tab_report": "报告生成",
2222
"tab_run_history": "执行记录",
2323
"tab_profiler": "性能分析",
24+
"tab_secrets": "密钥管理",
2425
"tab_accessibility": "无障碍树",
2526
"tab_vlm": "AI 定位",
2627
"tab_ocr_reader": "OCR 读取",
@@ -652,6 +653,30 @@
652653
"rh_preview_empty": "请选择一条记录预览。",
653654
"rh_preview_no_artifact": "此次执行没有截图。",
654655

656+
# Secrets tab
657+
"secret_unlock_group": "密钥库",
658+
"secret_manage_group": "密钥",
659+
"secret_passphrase_label": "通行码:",
660+
"secret_passphrase_placeholder": "密钥库通行码",
661+
"secret_init": "创建密钥库",
662+
"secret_init_done": "密钥库已创建并解锁。",
663+
"secret_unlock": "解锁",
664+
"secret_lock": "锁定",
665+
"secret_add": "添加密钥",
666+
"secret_remove": "删除",
667+
"secret_change_passphrase": "修改通行码",
668+
"secret_change_done": "密钥库已使用新通行码重新加密。",
669+
"secret_status_uninitialized": "尚未创建密钥库。",
670+
"secret_status_unlocked": "密钥库已解锁。",
671+
"secret_status_locked": "密钥库已锁定。",
672+
"secret_passphrase_required": "请先输入通行码。",
673+
"secret_wrong_passphrase": "通行码错误。",
674+
"secret_unlock_first": "请先解锁密钥库再管理密钥。",
675+
"secret_name_prompt": "密钥名称(脚本以 ${secrets.NAME} 引用):",
676+
"secret_value_prompt": "密钥内容:",
677+
"secret_old_passphrase_prompt": "当前通行码:",
678+
"secret_new_passphrase_prompt": "新通行码:",
679+
655680
# Profiler tab
656681
"prof_enable": "启用性能分析",
657682
"prof_disable": "停用性能分析",

je_auto_control/gui/language_wrapper/traditional_chinese.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"tab_vlm": "AI 定位",
2727
"tab_ocr_reader": "OCR 讀取",
2828
"tab_variables": "執行期變數",
29+
"tab_secrets": "密鑰管理",
2930
"tab_llm_planner": "LLM 腳本規劃",
3031
"tab_remote_desktop": "遠端桌面",
3132
"tab_rest_api": "REST API",
@@ -653,6 +654,30 @@
653654
"rh_preview_empty": "請選擇一筆紀錄以預覽。",
654655
"rh_preview_no_artifact": "此次執行沒有截圖。",
655656

657+
# Secrets tab
658+
"secret_unlock_group": "密鑰庫",
659+
"secret_manage_group": "密鑰",
660+
"secret_passphrase_label": "通行碼:",
661+
"secret_passphrase_placeholder": "密鑰庫通行碼",
662+
"secret_init": "建立密鑰庫",
663+
"secret_init_done": "密鑰庫已建立並解鎖。",
664+
"secret_unlock": "解鎖",
665+
"secret_lock": "鎖定",
666+
"secret_add": "新增密鑰",
667+
"secret_remove": "移除",
668+
"secret_change_passphrase": "變更通行碼",
669+
"secret_change_done": "密鑰庫已用新通行碼重新加密。",
670+
"secret_status_uninitialized": "尚未建立密鑰庫。",
671+
"secret_status_unlocked": "密鑰庫已解鎖。",
672+
"secret_status_locked": "密鑰庫已鎖定。",
673+
"secret_passphrase_required": "請先輸入通行碼。",
674+
"secret_wrong_passphrase": "通行碼錯誤。",
675+
"secret_unlock_first": "請先解鎖密鑰庫再管理密鑰。",
676+
"secret_name_prompt": "密鑰名稱(在腳本以 ${secrets.NAME} 引用):",
677+
"secret_value_prompt": "密鑰內容:",
678+
"secret_old_passphrase_prompt": "目前通行碼:",
679+
"secret_new_passphrase_prompt": "新通行碼:",
680+
656681
# Profiler tab
657682
"prof_enable": "啟用效能分析",
658683
"prof_disable": "停用效能分析",

je_auto_control/gui/main_widget.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from je_auto_control.gui.ocr_tab import OCRReaderTab
2121
from je_auto_control.gui.plugins_tab import PluginsTab
2222
from je_auto_control.gui.profiler_tab import ProfilerTab
23+
from je_auto_control.gui.secrets_tab import SecretsTab
2324
from je_auto_control.gui.admin_console_tab import AdminConsoleTab
2425
from je_auto_control.gui.audit_log_tab import AuditLogTab
2526
from je_auto_control.gui.diagnostics_tab import DiagnosticsTab
@@ -109,6 +110,8 @@ def __init__(self, parent=None):
109110
category="editing")
110111
self._add_tab("variables", "tab_variables", VariablesTab(),
111112
category="editing")
113+
self._add_tab("secrets", "tab_secrets", SecretsTab(),
114+
category="editing")
112115
self._add_tab("vlm", "tab_vlm", VLMTab(),
113116
category="detection")
114117
self._add_tab("ocr_reader", "tab_ocr_reader", OCRReaderTab(),

je_auto_control/gui/secrets_tab.py

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
"""Secrets tab: unlock the vault and manage ${secrets.NAME} entries."""
2+
from typing import Optional
3+
4+
from PySide6.QtWidgets import (
5+
QAbstractItemView, QGroupBox, QHBoxLayout, QInputDialog, QLabel,
6+
QLineEdit, QListWidget, QListWidgetItem, QMessageBox, QPushButton,
7+
QVBoxLayout, QWidget,
8+
)
9+
10+
from je_auto_control.gui._i18n_helpers import TranslatableMixin
11+
from je_auto_control.gui.language_wrapper.multi_language_wrapper import (
12+
language_wrapper,
13+
)
14+
from je_auto_control.utils.secrets import (
15+
SecretStoreError, SecretStoreLocked, default_secret_manager,
16+
)
17+
18+
19+
def _t(key: str) -> str:
20+
return language_wrapper.translate(key, key)
21+
22+
23+
class SecretsTab(TranslatableMixin, QWidget):
24+
"""Manage the encrypted secret vault used by ``${secrets.NAME}``."""
25+
26+
def __init__(self, parent: Optional[QWidget] = None) -> None:
27+
super().__init__(parent)
28+
self._tr_init()
29+
self._status_label = QLabel()
30+
self._passphrase = QLineEdit()
31+
self._passphrase.setEchoMode(QLineEdit.Password)
32+
self._list = QListWidget()
33+
self._list.setEditTriggers(QAbstractItemView.NoEditTriggers)
34+
self._build_layout()
35+
self._refresh_status()
36+
37+
def retranslate(self) -> None:
38+
TranslatableMixin.retranslate(self)
39+
self._refresh_status()
40+
41+
def _build_layout(self) -> None:
42+
root = QVBoxLayout(self)
43+
unlock_box = self._tr(QGroupBox(), "secret_unlock_group")
44+
unlock_layout = QHBoxLayout(unlock_box)
45+
unlock_layout.addWidget(self._tr(QLabel(), "secret_passphrase_label"))
46+
self._passphrase.setPlaceholderText(_t("secret_passphrase_placeholder"))
47+
unlock_layout.addWidget(self._passphrase)
48+
init_btn = self._tr(QPushButton(), "secret_init")
49+
init_btn.clicked.connect(self._on_init)
50+
unlock_layout.addWidget(init_btn)
51+
unlock_btn = self._tr(QPushButton(), "secret_unlock")
52+
unlock_btn.clicked.connect(self._on_unlock)
53+
unlock_layout.addWidget(unlock_btn)
54+
lock_btn = self._tr(QPushButton(), "secret_lock")
55+
lock_btn.clicked.connect(self._on_lock)
56+
unlock_layout.addWidget(lock_btn)
57+
root.addWidget(unlock_box)
58+
59+
manage_box = self._tr(QGroupBox(), "secret_manage_group")
60+
manage_layout = QVBoxLayout(manage_box)
61+
manage_layout.addWidget(self._list)
62+
button_row = QHBoxLayout()
63+
add_btn = self._tr(QPushButton(), "secret_add")
64+
add_btn.clicked.connect(self._on_add)
65+
button_row.addWidget(add_btn)
66+
remove_btn = self._tr(QPushButton(), "secret_remove")
67+
remove_btn.clicked.connect(self._on_remove)
68+
button_row.addWidget(remove_btn)
69+
change_btn = self._tr(QPushButton(), "secret_change_passphrase")
70+
change_btn.clicked.connect(self._on_change_passphrase)
71+
button_row.addWidget(change_btn)
72+
button_row.addStretch()
73+
manage_layout.addLayout(button_row)
74+
root.addWidget(manage_box, stretch=1)
75+
76+
root.addWidget(self._status_label)
77+
78+
def _refresh_status(self) -> None:
79+
manager = default_secret_manager
80+
if not manager.is_initialized:
81+
self._status_label.setText(_t("secret_status_uninitialized"))
82+
elif manager.is_unlocked:
83+
self._status_label.setText(_t("secret_status_unlocked"))
84+
else:
85+
self._status_label.setText(_t("secret_status_locked"))
86+
self._refresh_list()
87+
88+
def _refresh_list(self) -> None:
89+
self._list.clear()
90+
if not default_secret_manager.is_unlocked:
91+
return
92+
try:
93+
names = default_secret_manager.list_names()
94+
except SecretStoreError:
95+
names = []
96+
for name in names:
97+
self._list.addItem(QListWidgetItem(name))
98+
99+
def _consume_passphrase(self) -> str:
100+
text = self._passphrase.text()
101+
self._passphrase.clear()
102+
return text
103+
104+
def _on_init(self) -> None:
105+
passphrase = self._consume_passphrase()
106+
if not passphrase:
107+
QMessageBox.warning(self, _t("secret_init"),
108+
_t("secret_passphrase_required"))
109+
return
110+
try:
111+
default_secret_manager.initialize(passphrase)
112+
except SecretStoreError as error:
113+
QMessageBox.warning(self, _t("secret_init"), str(error))
114+
return
115+
QMessageBox.information(self, _t("secret_init"),
116+
_t("secret_init_done"))
117+
self._refresh_status()
118+
119+
def _on_unlock(self) -> None:
120+
passphrase = self._consume_passphrase()
121+
if not passphrase:
122+
return
123+
try:
124+
ok = default_secret_manager.unlock(passphrase)
125+
except SecretStoreError as error:
126+
QMessageBox.warning(self, _t("secret_unlock"), str(error))
127+
return
128+
if not ok:
129+
QMessageBox.warning(self, _t("secret_unlock"),
130+
_t("secret_wrong_passphrase"))
131+
self._refresh_status()
132+
133+
def _on_lock(self) -> None:
134+
default_secret_manager.lock()
135+
self._refresh_status()
136+
137+
def _on_add(self) -> None:
138+
if not default_secret_manager.is_unlocked:
139+
QMessageBox.information(self, _t("secret_add"),
140+
_t("secret_unlock_first"))
141+
return
142+
name, ok = QInputDialog.getText(
143+
self, _t("secret_add"), _t("secret_name_prompt"),
144+
)
145+
if not ok or not name.strip():
146+
return
147+
value, ok = QInputDialog.getText(
148+
self, _t("secret_add"), _t("secret_value_prompt"),
149+
QLineEdit.Password,
150+
)
151+
if not ok:
152+
return
153+
try:
154+
default_secret_manager.set(name.strip(), value)
155+
except (SecretStoreError, SecretStoreLocked, ValueError) as error:
156+
QMessageBox.warning(self, _t("secret_add"), str(error))
157+
return
158+
self._refresh_list()
159+
160+
def _on_remove(self) -> None:
161+
item = self._list.currentItem()
162+
if item is None:
163+
return
164+
try:
165+
default_secret_manager.remove(item.text())
166+
except (SecretStoreError, SecretStoreLocked) as error:
167+
QMessageBox.warning(self, _t("secret_remove"), str(error))
168+
return
169+
self._refresh_list()
170+
171+
def _on_change_passphrase(self) -> None:
172+
old, ok = QInputDialog.getText(
173+
self, _t("secret_change_passphrase"),
174+
_t("secret_old_passphrase_prompt"), QLineEdit.Password,
175+
)
176+
if not ok:
177+
return
178+
new, ok = QInputDialog.getText(
179+
self, _t("secret_change_passphrase"),
180+
_t("secret_new_passphrase_prompt"), QLineEdit.Password,
181+
)
182+
if not ok or not new:
183+
return
184+
try:
185+
default_secret_manager.change_passphrase(old, new)
186+
except (SecretStoreError, ValueError) as error:
187+
QMessageBox.warning(self, _t("secret_change_passphrase"),
188+
str(error))
189+
return
190+
QMessageBox.information(
191+
self, _t("secret_change_passphrase"),
192+
_t("secret_change_done"),
193+
)
194+
self._refresh_status()

0 commit comments

Comments
 (0)