evm-cli 2.4.0__py3-none-any.whl
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.
- evm/__init__.py +55 -0
- evm/__main__.py +9 -0
- evm/_completion.py +253 -0
- evm/_crypto.py +177 -0
- evm/_groups.py +127 -0
- evm/_history.py +141 -0
- evm/_io.py +421 -0
- evm/_json.py +52 -0
- evm/_schema.py +265 -0
- evm/_typing.py +40 -0
- evm/cli.py +961 -0
- evm/exceptions.py +150 -0
- evm/formatters.py +285 -0
- evm/manager.py +644 -0
- evm_cli-2.4.0.dist-info/METADATA +748 -0
- evm_cli-2.4.0.dist-info/RECORD +20 -0
- evm_cli-2.4.0.dist-info/WHEEL +5 -0
- evm_cli-2.4.0.dist-info/entry_points.txt +2 -0
- evm_cli-2.4.0.dist-info/licenses/LICENSE +21 -0
- evm_cli-2.4.0.dist-info/top_level.txt +1 -0
evm/__init__.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
EVM - Environment Variable Manager
|
|
3
|
+
A command-line tool for managing environment variables.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
__version__ = "2.4.0"
|
|
7
|
+
__author__ = "EVM Tool"
|
|
8
|
+
|
|
9
|
+
from .exceptions import (
|
|
10
|
+
BackupError,
|
|
11
|
+
CommandNotFoundError,
|
|
12
|
+
CorruptedStorageError,
|
|
13
|
+
DecryptionError,
|
|
14
|
+
EditorError,
|
|
15
|
+
EVMError,
|
|
16
|
+
ExportError,
|
|
17
|
+
GroupNotFoundError,
|
|
18
|
+
GroupOperationError,
|
|
19
|
+
ImportFailedError,
|
|
20
|
+
KeyAlreadyExistsError,
|
|
21
|
+
KeyNotFoundError,
|
|
22
|
+
LockTimeoutError,
|
|
23
|
+
OperationCancelledError,
|
|
24
|
+
SchemaError,
|
|
25
|
+
StorageError,
|
|
26
|
+
StoragePermissionError,
|
|
27
|
+
ValidationError,
|
|
28
|
+
)
|
|
29
|
+
from .manager import EnvironmentManager
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
'__version__',
|
|
33
|
+
'__author__',
|
|
34
|
+
# Core API
|
|
35
|
+
'EnvironmentManager',
|
|
36
|
+
# Exceptions
|
|
37
|
+
'EVMError',
|
|
38
|
+
'KeyNotFoundError',
|
|
39
|
+
'KeyAlreadyExistsError',
|
|
40
|
+
'StorageError',
|
|
41
|
+
'CorruptedStorageError',
|
|
42
|
+
'StoragePermissionError',
|
|
43
|
+
'LockTimeoutError',
|
|
44
|
+
'ExportError',
|
|
45
|
+
'ImportFailedError',
|
|
46
|
+
'CommandNotFoundError',
|
|
47
|
+
'GroupNotFoundError',
|
|
48
|
+
'GroupOperationError',
|
|
49
|
+
'BackupError',
|
|
50
|
+
'EditorError',
|
|
51
|
+
'DecryptionError',
|
|
52
|
+
'ValidationError',
|
|
53
|
+
'SchemaError',
|
|
54
|
+
'OperationCancelledError',
|
|
55
|
+
]
|
evm/__main__.py
ADDED
evm/_completion.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
EVM Shell 补全脚本生成
|
|
4
|
+
|
|
5
|
+
支持 bash, zsh, fish。
|
|
6
|
+
M3: 为 get/delete/edit/expand/validate/rename/copy 等命令
|
|
7
|
+
提供动态变量名补全(通过 evm list --json --quiet 获取 key 列表)。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def generate_bash_completion(commands: list) -> str:
|
|
12
|
+
"""生成 bash 补全脚本(含动态变量名补全)"""
|
|
13
|
+
cmds = ' '.join(commands)
|
|
14
|
+
# 需要变量名补全的命令
|
|
15
|
+
key_cmds = ' '.join([
|
|
16
|
+
'get', 'delete', 'edit', 'expand', 'validate',
|
|
17
|
+
'rename', 'copy', 'setg', 'getg', 'deleteg', 'listg',
|
|
18
|
+
])
|
|
19
|
+
return f'''# EVM bash completion
|
|
20
|
+
# L3: 仅在 evm 命令可用时注册补全
|
|
21
|
+
if ! command -v evm &>/dev/null; then
|
|
22
|
+
return 2>/dev/null || exit
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
_evm_completions() {{
|
|
26
|
+
local cur prev opts
|
|
27
|
+
COMPREPLY=()
|
|
28
|
+
cur="${{COMP_WORDS[COMP_CWORD]}}"
|
|
29
|
+
prev="${{COMP_WORDS[COMP_CWORD-1]}}"
|
|
30
|
+
|
|
31
|
+
# Top-level commands
|
|
32
|
+
local commands="{cmds}"
|
|
33
|
+
local global_opts="--help --version --verbose --env-file --json --quiet --dry-run --force"
|
|
34
|
+
local key_cmds="{key_cmds}"
|
|
35
|
+
|
|
36
|
+
# 动态获取变量名列表
|
|
37
|
+
_evm_keys() {{
|
|
38
|
+
evm list --json --quiet 2>/dev/null | \\
|
|
39
|
+
python3 -c "import sys,json; d=json.load(sys.stdin); print(' '.join(d.get('data',{{}}).keys()))" 2>/dev/null
|
|
40
|
+
}}
|
|
41
|
+
|
|
42
|
+
case "${{prev}}" in
|
|
43
|
+
evm)
|
|
44
|
+
COMPREPLY=( $(compgen -W "${{commands}} ${{global_opts}}" -- "${{cur}}") )
|
|
45
|
+
return 0
|
|
46
|
+
;;
|
|
47
|
+
--env-file)
|
|
48
|
+
COMPREPLY=( $(compgen -f -- "${{cur}}") )
|
|
49
|
+
return 0
|
|
50
|
+
;;
|
|
51
|
+
--format|-f)
|
|
52
|
+
COMPREPLY=( $(compgen -W "json env sh backup" -- "${{cur}}") )
|
|
53
|
+
return 0
|
|
54
|
+
;;
|
|
55
|
+
set)
|
|
56
|
+
COMPREPLY=( $(compgen -W "--secret --help" -- "${{cur}}") )
|
|
57
|
+
return 0
|
|
58
|
+
;;
|
|
59
|
+
get|delete|edit|expand|validate|rename|copy)
|
|
60
|
+
COMPREPLY=( $(compgen -W "$(_evm_keys) --secret --help" -- "${{cur}}") )
|
|
61
|
+
return 0
|
|
62
|
+
;;
|
|
63
|
+
setg|getg|deleteg|listg)
|
|
64
|
+
# 分组名补全(从现有 key 中提取分组前缀)
|
|
65
|
+
local groups
|
|
66
|
+
groups=$(evm groups --json --quiet 2>/dev/null | \\
|
|
67
|
+
python3 -c "import sys,json; d=json.load(sys.stdin); print(' '.join(d.get('data',{{}}).get('groups',{{}}).keys()))" 2>/dev/null)
|
|
68
|
+
COMPREPLY=( $(compgen -W "${{groups}} --help" -- "${{cur}}") )
|
|
69
|
+
return 0
|
|
70
|
+
;;
|
|
71
|
+
export)
|
|
72
|
+
COMPREPLY=( $(compgen -W "--format --output --group --help" -- "${{cur}}") )
|
|
73
|
+
return 0
|
|
74
|
+
;;
|
|
75
|
+
load)
|
|
76
|
+
COMPREPLY=( $(compgen -W "--format --replace --group --nest --help" -- "${{cur}}") )
|
|
77
|
+
return 0
|
|
78
|
+
;;
|
|
79
|
+
completion)
|
|
80
|
+
COMPREPLY=( $(compgen -W "bash zsh fish" -- "${{cur}}") )
|
|
81
|
+
return 0
|
|
82
|
+
;;
|
|
83
|
+
esac
|
|
84
|
+
|
|
85
|
+
# 如果前一个词是需要 key 补全的命令,尝试补全变量名
|
|
86
|
+
for kc in ${{key_cmds}}; do
|
|
87
|
+
if [[ "${{COMP_WORDS[1]}}" == "$kc" ]]; then
|
|
88
|
+
COMPREPLY=( $(compgen -W "$(_evm_keys)" -- "${{cur}}") )
|
|
89
|
+
return 0
|
|
90
|
+
fi
|
|
91
|
+
done
|
|
92
|
+
|
|
93
|
+
COMPREPLY=( $(compgen -W "${{commands}} ${{global_opts}}" -- "${{cur}}") )
|
|
94
|
+
}}
|
|
95
|
+
complete -F _evm_completions evm
|
|
96
|
+
'''
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def generate_zsh_completion(commands: list) -> str:
|
|
100
|
+
"""生成 zsh 补全脚本(含动态变量名补全)"""
|
|
101
|
+
return f'''#compdef evm
|
|
102
|
+
|
|
103
|
+
# L3: 仅在 evm 命令可用时注册补全
|
|
104
|
+
if (( ! $+commands[evm] )); then
|
|
105
|
+
return
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
_evm() {{
|
|
109
|
+
local -a commands
|
|
110
|
+
commands=(
|
|
111
|
+
{chr(10).join(f" '{c}:{c} command'" for c in commands)}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# 动态获取变量名列表
|
|
115
|
+
_evm_keys() {{
|
|
116
|
+
local keys
|
|
117
|
+
keys=(${{(f)"$(evm list --json --quiet 2>/dev/null | \\
|
|
118
|
+
python3 -c "import sys,json; d=json.load(sys.stdin); print('\\n'.join(d.get('data',{{}}).keys()))" 2>/dev/null)"}})
|
|
119
|
+
_describe 'variable' keys
|
|
120
|
+
}}
|
|
121
|
+
|
|
122
|
+
# 动态获取分组名列表
|
|
123
|
+
_evm_groups() {{
|
|
124
|
+
local groups
|
|
125
|
+
groups=(${{(f)"$(evm groups --json --quiet 2>/dev/null | \\
|
|
126
|
+
python3 -c "import sys,json; d=json.load(sys.stdin); print('\\n'.join(d.get('data',{{}}).get('groups',{{}}).keys()))" 2>/dev/null)"}})
|
|
127
|
+
_describe 'group' groups
|
|
128
|
+
}}
|
|
129
|
+
|
|
130
|
+
_arguments -C \\
|
|
131
|
+
'--help[Show help]' \\
|
|
132
|
+
'--version[Show version]' \\
|
|
133
|
+
'(-v --verbose)'{{-v,--verbose}}'[Show detailed info]' \\
|
|
134
|
+
'--env-file[Storage file path]:file:_files' \\
|
|
135
|
+
'--json[Output structured JSON]' \\
|
|
136
|
+
'--quiet[Suppress output]' \\
|
|
137
|
+
'--dry-run[Preview changes]' \\
|
|
138
|
+
'--force[Skip confirmation]' \\
|
|
139
|
+
'1: :->command' \\
|
|
140
|
+
'*:: :->args'
|
|
141
|
+
|
|
142
|
+
case $state in
|
|
143
|
+
command)
|
|
144
|
+
_describe 'command' commands
|
|
145
|
+
;;
|
|
146
|
+
args)
|
|
147
|
+
case $words[1] in
|
|
148
|
+
get|delete|edit|expand|validate)
|
|
149
|
+
_evm_keys
|
|
150
|
+
;;
|
|
151
|
+
rename|copy)
|
|
152
|
+
_evm_keys
|
|
153
|
+
;;
|
|
154
|
+
setg|getg|deleteg|listg)
|
|
155
|
+
_evm_groups
|
|
156
|
+
;;
|
|
157
|
+
completion)
|
|
158
|
+
_values 'shell' bash zsh fish
|
|
159
|
+
;;
|
|
160
|
+
export)
|
|
161
|
+
_arguments \\
|
|
162
|
+
'(-f --format)'{{-f,--format}}'[Format]:format:(json env sh)' \\
|
|
163
|
+
'(-o --output)'{{-o,--output}}'[Output file]:file:_files' \\
|
|
164
|
+
'(-g --group)'{{-g,--group}}'[Group name]:group'
|
|
165
|
+
;;
|
|
166
|
+
esac
|
|
167
|
+
;;
|
|
168
|
+
esac
|
|
169
|
+
}}
|
|
170
|
+
|
|
171
|
+
_evm "$@"
|
|
172
|
+
'''
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def generate_fish_completion(commands: list) -> str:
|
|
176
|
+
"""生成 fish 补全脚本(含动态变量名补全)"""
|
|
177
|
+
lines = ['# EVM fish completion', '']
|
|
178
|
+
|
|
179
|
+
# L3: 仅在 evm 命令可用时注册补全
|
|
180
|
+
lines.append('# Guard: only register if evm is in PATH')
|
|
181
|
+
lines.append('if not command -q evm')
|
|
182
|
+
lines.append(' exit')
|
|
183
|
+
lines.append('end')
|
|
184
|
+
lines.append('')
|
|
185
|
+
|
|
186
|
+
# Disable file completion by default
|
|
187
|
+
lines.append('complete -c evm -f')
|
|
188
|
+
lines.append('')
|
|
189
|
+
|
|
190
|
+
# Global options
|
|
191
|
+
lines.append('# Global options')
|
|
192
|
+
lines.append("complete -c evm -l help -d 'Show help'")
|
|
193
|
+
lines.append("complete -c evm -l version -d 'Show version'")
|
|
194
|
+
lines.append("complete -c evm -s v -l verbose -d 'Show detailed info'")
|
|
195
|
+
lines.append("complete -c evm -l env-file -d 'Storage file path' -r -F")
|
|
196
|
+
lines.append("complete -c evm -l json -d 'Output structured JSON'")
|
|
197
|
+
lines.append("complete -c evm -l quiet -s q -d 'Suppress output'")
|
|
198
|
+
lines.append("complete -c evm -l dry-run -d 'Preview changes'")
|
|
199
|
+
lines.append("complete -c evm -l force -d 'Skip confirmation'")
|
|
200
|
+
lines.append('')
|
|
201
|
+
|
|
202
|
+
# Commands
|
|
203
|
+
lines.append('# Commands')
|
|
204
|
+
for cmd in commands:
|
|
205
|
+
lines.append(f"complete -c evm -n '__fish_use_subcommand' -a '{cmd}' -d '{cmd}'")
|
|
206
|
+
lines.append('')
|
|
207
|
+
|
|
208
|
+
# Dynamic variable name completion helper
|
|
209
|
+
lines.append('# Dynamic variable name completion')
|
|
210
|
+
lines.append('function __evm_keys')
|
|
211
|
+
lines.append(' evm list --json --quiet 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); [print(k) for k in d.get(\'data\',{})]" 2>/dev/null')
|
|
212
|
+
lines.append('end')
|
|
213
|
+
lines.append('')
|
|
214
|
+
|
|
215
|
+
# Dynamic group name completion helper
|
|
216
|
+
lines.append('function __evm_groups')
|
|
217
|
+
lines.append(' evm groups --json --quiet 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); [print(k) for k in d.get(\'data\',{}).get(\'groups\',{})]" 2>/dev/null')
|
|
218
|
+
lines.append('end')
|
|
219
|
+
lines.append('')
|
|
220
|
+
|
|
221
|
+
# Sub-options
|
|
222
|
+
lines.append('# Sub-options')
|
|
223
|
+
lines.append("complete -c evm -n '__fish_seen_subcommand_from set' -l secret -d 'Encrypt value'")
|
|
224
|
+
lines.append("complete -c evm -n '__fish_seen_subcommand_from get' -l secret -d 'Decrypt value'")
|
|
225
|
+
lines.append("complete -c evm -n '__fish_seen_subcommand_from export' -s f -l format -d 'Format' -xa 'json env sh'")
|
|
226
|
+
lines.append("complete -c evm -n '__fish_seen_subcommand_from export' -s o -l output -d 'Output file' -r -F")
|
|
227
|
+
lines.append("complete -c evm -n '__fish_seen_subcommand_from load' -s f -l format -d 'Format' -xa 'json env backup'")
|
|
228
|
+
lines.append("complete -c evm -n '__fish_seen_subcommand_from load' -s r -l replace -d 'Replace mode'")
|
|
229
|
+
lines.append("complete -c evm -n '__fish_seen_subcommand_from load' -s n -l nest -d 'Nested JSON'")
|
|
230
|
+
lines.append("complete -c evm -n '__fish_seen_subcommand_from completion' -xa 'bash zsh fish'")
|
|
231
|
+
lines.append("complete -c evm -n '__fish_seen_subcommand_from history' -s n -l limit -d 'Number of entries' -x")
|
|
232
|
+
lines.append("complete -c evm -n '__fish_seen_subcommand_from schema' -xa 'set get delete validate list'")
|
|
233
|
+
lines.append('')
|
|
234
|
+
|
|
235
|
+
# Variable name completion for relevant commands
|
|
236
|
+
lines.append('# Variable name completion')
|
|
237
|
+
for cmd in ['get', 'delete', 'edit', 'expand', 'validate', 'rename', 'copy']:
|
|
238
|
+
lines.append(f"complete -c evm -n '__fish_seen_subcommand_from {cmd}' -a '(__evm_keys)'")
|
|
239
|
+
lines.append('')
|
|
240
|
+
|
|
241
|
+
# Group name completion
|
|
242
|
+
lines.append('# Group name completion')
|
|
243
|
+
for cmd in ['setg', 'getg', 'deleteg', 'listg']:
|
|
244
|
+
lines.append(f"complete -c evm -n '__fish_seen_subcommand_from {cmd}' -a '(__evm_groups)'")
|
|
245
|
+
|
|
246
|
+
return '\n'.join(lines) + '\n'
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
SHELL_GENERATORS = {
|
|
250
|
+
'bash': generate_bash_completion,
|
|
251
|
+
'zsh': generate_zsh_completion,
|
|
252
|
+
'fish': generate_fish_completion,
|
|
253
|
+
}
|
evm/_crypto.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
EVM 加密模块
|
|
4
|
+
|
|
5
|
+
提供安全的加密/解密功能:
|
|
6
|
+
- HKDF-Expand: 密钥分离(加密密钥 + MAC 密钥)
|
|
7
|
+
- HMAC-CTR: 基于 HMAC 的 CTR 模式流密码
|
|
8
|
+
- HMAC-SHA256: 认证加密(Encrypt-then-MAC)
|
|
9
|
+
|
|
10
|
+
格式: ENCv3:<salt_b64>:<iv_b64>:<mac_b64>:<ciphertext_b64>
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import base64
|
|
14
|
+
import hashlib
|
|
15
|
+
import hmac
|
|
16
|
+
import os
|
|
17
|
+
import struct
|
|
18
|
+
|
|
19
|
+
from .exceptions import DecryptionError
|
|
20
|
+
|
|
21
|
+
# SHA-256 输出长度(字节),用于 HKDF-Expand 计算
|
|
22
|
+
HKDF_HASH_LEN = 32
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def hkdf_expand(prk: bytes, info: bytes, length: int = 32) -> bytes:
|
|
26
|
+
"""HKDF-Expand (RFC 5869)
|
|
27
|
+
|
|
28
|
+
从伪随机密钥 (PRK) 派生指定长度的输出密钥材料。
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
prk: 伪随机密钥(至少 HKDF_HASH_LEN 字节)
|
|
32
|
+
info: 上下文和用途信息
|
|
33
|
+
length: 输出密钥材料长度(字节)
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
派生的密钥材料
|
|
37
|
+
"""
|
|
38
|
+
n = (length + HKDF_HASH_LEN - 1) // HKDF_HASH_LEN
|
|
39
|
+
okm = b''
|
|
40
|
+
t = b''
|
|
41
|
+
for i in range(1, n + 1):
|
|
42
|
+
t = hmac.new(
|
|
43
|
+
prk, t + info + bytes([i]), hashlib.sha256
|
|
44
|
+
).digest()
|
|
45
|
+
okm += t
|
|
46
|
+
return okm[:length]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def derive_subkeys(master_key: bytes, salt: bytes) -> tuple:
|
|
50
|
+
"""从主密钥派生独立的加密和 MAC 子密钥
|
|
51
|
+
|
|
52
|
+
使用 HKDF-Expand 和不同的 info 字符串确保密钥分离。
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
master_key: PBKDF2 输出的主密钥
|
|
56
|
+
salt: 用于 HKDF info 的随机盐
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
(enc_key, mac_key) 各 32 字节
|
|
60
|
+
"""
|
|
61
|
+
enc_key = hkdf_expand(master_key, salt + b'evm-encryption', 32)
|
|
62
|
+
mac_key = hkdf_expand(master_key, salt + b'evm-authentication', 32)
|
|
63
|
+
return enc_key, mac_key
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def hmac_ctr_keystream(key: bytes, iv: bytes, length: int) -> bytes:
|
|
67
|
+
"""生成 HMAC-CTR 模式密钥流
|
|
68
|
+
|
|
69
|
+
keystream = HMAC(key, IV || 0) || HMAC(key, IV || 1) || ...
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
key: 加密密钥
|
|
73
|
+
iv: 随机初始化向量(计数器起始值)
|
|
74
|
+
length: 需要的密钥流长度(字节)
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
指定长度的密钥流
|
|
78
|
+
"""
|
|
79
|
+
stream = b''
|
|
80
|
+
counter = 0
|
|
81
|
+
while len(stream) < length:
|
|
82
|
+
block = hmac.new(
|
|
83
|
+
key, iv + struct.pack('>I', counter), hashlib.sha256
|
|
84
|
+
).digest()
|
|
85
|
+
stream += block
|
|
86
|
+
counter += 1
|
|
87
|
+
return stream[:length]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def encrypt_v3(plaintext: str, derive_key_fn) -> str:
|
|
91
|
+
"""v3 加密: HKDF 密钥分离 + HMAC-CTR + Encrypt-then-MAC
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
plaintext: 明文
|
|
95
|
+
derive_key_fn: 密钥派生函数 (salt) -> master_key
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
ENCv3:<salt_b64>:<iv_b64>:<mac_b64>:<ciphertext_b64>
|
|
99
|
+
"""
|
|
100
|
+
salt = os.urandom(16)
|
|
101
|
+
iv = os.urandom(16)
|
|
102
|
+
master_key = derive_key_fn(salt)
|
|
103
|
+
enc_key, mac_key = derive_subkeys(master_key, salt)
|
|
104
|
+
|
|
105
|
+
data_bytes = plaintext.encode('utf-8')
|
|
106
|
+
|
|
107
|
+
# HMAC-CTR 加密
|
|
108
|
+
keystream = hmac_ctr_keystream(enc_key, iv, len(data_bytes))
|
|
109
|
+
ciphertext = bytes(a ^ b for a, b in zip(data_bytes, keystream))
|
|
110
|
+
|
|
111
|
+
# Encrypt-then-MAC: HMAC 覆盖 salt + iv + ciphertext
|
|
112
|
+
mac = hmac.new(
|
|
113
|
+
mac_key, salt + iv + ciphertext, hashlib.sha256
|
|
114
|
+
).digest()
|
|
115
|
+
|
|
116
|
+
salt_b64 = base64.b64encode(salt).decode('ascii')
|
|
117
|
+
iv_b64 = base64.b64encode(iv).decode('ascii')
|
|
118
|
+
mac_b64 = base64.b64encode(mac).decode('ascii')
|
|
119
|
+
ct_b64 = base64.b64encode(ciphertext).decode('ascii')
|
|
120
|
+
|
|
121
|
+
return f"ENCv3:{salt_b64}:{iv_b64}:{mac_b64}:{ct_b64}"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def decrypt_v3(encoded: str, derive_key_fn) -> str:
|
|
125
|
+
"""v3 解密: 验证 MAC + HMAC-CTR 解密
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
encoded: salt_b64:iv_b64:mac_b64:ciphertext_b64
|
|
129
|
+
derive_key_fn: 密钥派生函数 (salt) -> master_key
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
解密后的明文
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
DecryptionError: 格式错误或完整性校验失败
|
|
136
|
+
"""
|
|
137
|
+
parts = encoded.split(':')
|
|
138
|
+
if len(parts) != 4:
|
|
139
|
+
raise DecryptionError("Invalid v3 encrypted data format")
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
salt = base64.b64decode(parts[0])
|
|
143
|
+
iv = base64.b64decode(parts[1])
|
|
144
|
+
stored_mac = base64.b64decode(parts[2])
|
|
145
|
+
ciphertext = base64.b64decode(parts[3])
|
|
146
|
+
except Exception as e:
|
|
147
|
+
raise DecryptionError(f"Failed to decode v3 data: {e}")
|
|
148
|
+
|
|
149
|
+
master_key = derive_key_fn(salt)
|
|
150
|
+
enc_key, mac_key = derive_subkeys(master_key, salt)
|
|
151
|
+
|
|
152
|
+
# 验证 MAC(常量时间比较)
|
|
153
|
+
computed_mac = hmac.new(
|
|
154
|
+
mac_key, salt + iv + ciphertext, hashlib.sha256
|
|
155
|
+
).digest()
|
|
156
|
+
if not hmac.compare_digest(stored_mac, computed_mac):
|
|
157
|
+
raise DecryptionError(
|
|
158
|
+
"Data integrity check failed — data may be corrupted or tampered"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# HMAC-CTR 解密
|
|
162
|
+
keystream = hmac_ctr_keystream(enc_key, iv, len(ciphertext))
|
|
163
|
+
plaintext = bytes(a ^ b for a, b in zip(ciphertext, keystream))
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
return plaintext.decode('utf-8')
|
|
167
|
+
except UnicodeDecodeError as e:
|
|
168
|
+
raise DecryptionError(f"Decrypted data is not valid UTF-8: {e}")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
__all__ = [
|
|
172
|
+
'hkdf_expand',
|
|
173
|
+
'derive_subkeys',
|
|
174
|
+
'hmac_ctr_keystream',
|
|
175
|
+
'encrypt_v3',
|
|
176
|
+
'decrypt_v3',
|
|
177
|
+
]
|
evm/_groups.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
EVM 分组操作 Mixin
|
|
4
|
+
|
|
5
|
+
从 manager.py 中提取的分组管理功能。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
from ._typing import EnvironmentManagerProtocol
|
|
10
|
+
from .exceptions import (
|
|
11
|
+
GroupNotFoundError,
|
|
12
|
+
GroupOperationError,
|
|
13
|
+
KeyAlreadyExistsError,
|
|
14
|
+
KeyNotFoundError,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GroupMixin(EnvironmentManagerProtocol):
|
|
19
|
+
"""分组操作 mixin — 提供 setg/getg/deleteg/groups 等功能"""
|
|
20
|
+
|
|
21
|
+
def set_grouped(
|
|
22
|
+
self, group: str, key: str, value: str, dry_run: bool = False
|
|
23
|
+
) -> str:
|
|
24
|
+
"""设置分组变量"""
|
|
25
|
+
full_key = f"{group}:{key}" if group else key
|
|
26
|
+
if dry_run:
|
|
27
|
+
return f"[DRY-RUN] Would set: [{group}]{key} = {value}"
|
|
28
|
+
self._env_vars[full_key] = value
|
|
29
|
+
self._save_env_vars()
|
|
30
|
+
return f"Set: [{group}]{key} = {value}"
|
|
31
|
+
|
|
32
|
+
def get_grouped(self, group: str, key: str) -> str:
|
|
33
|
+
"""获取分组变量
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
KeyNotFoundError: 变量不存在
|
|
37
|
+
"""
|
|
38
|
+
full_key = f"{group}:{key}" if group else key
|
|
39
|
+
value = self._env_vars.get(full_key)
|
|
40
|
+
if value is None and group:
|
|
41
|
+
value = self._env_vars.get(key)
|
|
42
|
+
if value is None:
|
|
43
|
+
raise KeyNotFoundError(full_key)
|
|
44
|
+
return value
|
|
45
|
+
|
|
46
|
+
def delete_grouped(
|
|
47
|
+
self, group: str, key: str, dry_run: bool = False
|
|
48
|
+
) -> str:
|
|
49
|
+
"""删除分组变量
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
KeyNotFoundError: 变量不存在
|
|
53
|
+
"""
|
|
54
|
+
full_key = f"{group}:{key}" if group else key
|
|
55
|
+
if full_key not in self._env_vars:
|
|
56
|
+
raise KeyNotFoundError(full_key)
|
|
57
|
+
if dry_run:
|
|
58
|
+
return f"[DRY-RUN] Would delete: [{group}]{key}"
|
|
59
|
+
del self._env_vars[full_key]
|
|
60
|
+
self._save_env_vars()
|
|
61
|
+
return f"Deleted: [{group}]{key}"
|
|
62
|
+
|
|
63
|
+
def list_groups(self) -> dict[str, int]:
|
|
64
|
+
"""列出所有分组
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
{group_name: variable_count} 字典
|
|
68
|
+
"""
|
|
69
|
+
groups: dict[str, int] = {}
|
|
70
|
+
for key in self._env_vars:
|
|
71
|
+
if ':' in key:
|
|
72
|
+
group = key.split(':', 1)[0]
|
|
73
|
+
groups[group] = groups.get(group, 0) + 1
|
|
74
|
+
return groups
|
|
75
|
+
|
|
76
|
+
def delete_group(self, group: str, dry_run: bool = False) -> str:
|
|
77
|
+
"""删除整个分组
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
GroupOperationError: 尝试删除 default 组
|
|
81
|
+
GroupNotFoundError: 分组不存在
|
|
82
|
+
"""
|
|
83
|
+
if group == 'default':
|
|
84
|
+
raise GroupOperationError(
|
|
85
|
+
"Cannot delete default namespace. Use 'clear' to remove all variables."
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
prefix = f"{group}:"
|
|
89
|
+
to_delete = [k for k in self._env_vars if k.startswith(prefix)]
|
|
90
|
+
|
|
91
|
+
if not to_delete:
|
|
92
|
+
raise GroupNotFoundError(group)
|
|
93
|
+
|
|
94
|
+
if dry_run:
|
|
95
|
+
return (
|
|
96
|
+
f"[DRY-RUN] Would delete group '{group}' "
|
|
97
|
+
f"and {len(to_delete)} variables"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
for key in to_delete:
|
|
101
|
+
del self._env_vars[key]
|
|
102
|
+
self._save_env_vars()
|
|
103
|
+
return f"Deleted group '{group}' and all its variables ({len(to_delete)} total)"
|
|
104
|
+
|
|
105
|
+
def move_to_group(
|
|
106
|
+
self, key: str, new_group: str, dry_run: bool = False
|
|
107
|
+
) -> str:
|
|
108
|
+
"""移动变量到另一个分组
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
KeyNotFoundError: 变量不存在
|
|
112
|
+
KeyAlreadyExistsError: 目标分组中已有同名 key
|
|
113
|
+
"""
|
|
114
|
+
if key not in self._env_vars:
|
|
115
|
+
raise KeyNotFoundError(key)
|
|
116
|
+
|
|
117
|
+
new_key = f"{new_group}:{key}"
|
|
118
|
+
if new_key in self._env_vars and new_key != key:
|
|
119
|
+
raise KeyAlreadyExistsError(new_key)
|
|
120
|
+
|
|
121
|
+
if dry_run:
|
|
122
|
+
return f"[DRY-RUN] Would move: {key} -> {new_key}"
|
|
123
|
+
|
|
124
|
+
value = self._env_vars.pop(key)
|
|
125
|
+
self._env_vars[new_key] = value
|
|
126
|
+
self._save_env_vars()
|
|
127
|
+
return f"Moved: {key} -> {new_key}"
|