ensec-cli 1.0.0__tar.gz → 1.0.1__tar.gz
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.
- ensec_cli-1.0.1/PKG-INFO +28 -0
- ensec_cli-1.0.1/README.md +17 -0
- ensec_cli-1.0.1/ensec_cli/__init__.py +1 -0
- ensec_cli-1.0.1/ensec_cli/cli.py +367 -0
- ensec_cli-1.0.1/ensec_cli/esdcompress.py +526 -0
- ensec_cli-1.0.1/ensec_cli/passchk.py +11 -0
- ensec_cli-1.0.1/ensec_cli/rsa_encryptor.py +445 -0
- ensec_cli-1.0.1/ensec_cli/rsa_signer.py +187 -0
- ensec_cli-1.0.1/ensec_cli/utils.py +217 -0
- ensec_cli-1.0.1/ensec_cli/wavencode.py +48 -0
- ensec_cli-1.0.1/ensec_cli.egg-info/PKG-INFO +28 -0
- ensec_cli-1.0.1/ensec_cli.egg-info/SOURCES.txt +17 -0
- ensec_cli-1.0.1/ensec_cli.egg-info/top_level.txt +1 -0
- {ensec_cli-1.0.0 → ensec_cli-1.0.1}/pyproject.toml +4 -1
- ensec_cli-1.0.0/PKG-INFO +0 -9
- ensec_cli-1.0.0/README.md +0 -78
- ensec_cli-1.0.0/ensec_cli.egg-info/PKG-INFO +0 -9
- ensec_cli-1.0.0/ensec_cli.egg-info/SOURCES.txt +0 -9
- ensec_cli-1.0.0/ensec_cli.egg-info/top_level.txt +0 -1
- {ensec_cli-1.0.0 → ensec_cli-1.0.1}/LICENSE +0 -0
- {ensec_cli-1.0.0 → ensec_cli-1.0.1}/ensec_cli.egg-info/dependency_links.txt +0 -0
- {ensec_cli-1.0.0 → ensec_cli-1.0.1}/ensec_cli.egg-info/entry_points.txt +0 -0
- {ensec_cli-1.0.0 → ensec_cli-1.0.1}/ensec_cli.egg-info/requires.txt +0 -0
- {ensec_cli-1.0.0 → ensec_cli-1.0.1}/setup.cfg +0 -0
ensec_cli-1.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ensec-cli
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: EncryptSecureDEC Open Source CLI
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: pycryptodome
|
|
9
|
+
Requires-Dist: cryptography
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
# ENSEC-CLI
|
|
13
|
+
|
|
14
|
+
EncryptSecureDEC Open Source CLI
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
pip install ensec-cli
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
ensec encrypt test.txt
|
|
23
|
+
|
|
24
|
+
ensec decrypt test.txt.vdec
|
|
25
|
+
|
|
26
|
+
ensec sign test.txt
|
|
27
|
+
|
|
28
|
+
ensec verify-sign test.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.1"
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# Copyright (c) 2025 Innovation Craft Inc. All Rights Reserved.
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import getpass
|
|
6
|
+
import sys
|
|
7
|
+
import lzma
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
import datetime
|
|
11
|
+
from Crypto.Cipher import AES
|
|
12
|
+
from Crypto.Random import get_random_bytes
|
|
13
|
+
from Crypto.Protocol.KDF import PBKDF2
|
|
14
|
+
from . import rsa_signer
|
|
15
|
+
from . import wavencode
|
|
16
|
+
from . import rsa_encryptor
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# OKならそのまま続行
|
|
21
|
+
|
|
22
|
+
BLOCKCHAIN_HEADER = b'BLOCKCHAIN_DATA_START\n'
|
|
23
|
+
def _format_bytes(num: int) -> str:
|
|
24
|
+
units = ["B", "KB", "MB", "GB", "TB", "PB"]
|
|
25
|
+
n = float(num)
|
|
26
|
+
for u in units:
|
|
27
|
+
if n < 1024.0 or u == units[-1]:
|
|
28
|
+
return f"{n:.2f} {u}" if u != "B" else f"{int(n)} {u}"
|
|
29
|
+
n /= 1024.0
|
|
30
|
+
|
|
31
|
+
def _dir_size_bytes(root: str) -> int:
|
|
32
|
+
total = 0
|
|
33
|
+
# シンボリックリンクは辿らない(無限ループ回避)
|
|
34
|
+
stack = [root]
|
|
35
|
+
while stack:
|
|
36
|
+
d = stack.pop()
|
|
37
|
+
try:
|
|
38
|
+
with os.scandir(d) as it:
|
|
39
|
+
for entry in it:
|
|
40
|
+
try:
|
|
41
|
+
if entry.is_symlink():
|
|
42
|
+
continue
|
|
43
|
+
if entry.is_file(follow_symlinks=False):
|
|
44
|
+
total += entry.stat(follow_symlinks=False).st_size
|
|
45
|
+
elif entry.is_dir(follow_symlinks=False):
|
|
46
|
+
stack.append(entry.path)
|
|
47
|
+
except (PermissionError, FileNotFoundError):
|
|
48
|
+
# 権限/競合で読めないものはスキップ
|
|
49
|
+
continue
|
|
50
|
+
except (PermissionError, FileNotFoundError, NotADirectoryError):
|
|
51
|
+
continue
|
|
52
|
+
return total
|
|
53
|
+
|
|
54
|
+
class Block:
|
|
55
|
+
def __init__(self, data, previous_hash, operation_type, file_hash, user, memo):
|
|
56
|
+
self.timestamp = datetime.datetime.now(datetime.timezone.utc)
|
|
57
|
+
self.data = data
|
|
58
|
+
self.previous_hash = previous_hash
|
|
59
|
+
self.operation_type = operation_type
|
|
60
|
+
self.file_hash = file_hash
|
|
61
|
+
self.user = user
|
|
62
|
+
self.memo = memo
|
|
63
|
+
self.hash = self.calculate_hash()
|
|
64
|
+
|
|
65
|
+
def calculate_hash(self):
|
|
66
|
+
sha = hashlib.sha256()
|
|
67
|
+
sha.update(
|
|
68
|
+
str(self.timestamp).encode('utf-8') +
|
|
69
|
+
str(self.data).encode('utf-8') +
|
|
70
|
+
str(self.previous_hash).encode('utf-8') +
|
|
71
|
+
str(self.operation_type).encode('utf-8') +
|
|
72
|
+
str(self.file_hash).encode('utf-8') +
|
|
73
|
+
str(self.user).encode('utf-8') +
|
|
74
|
+
str(self.memo).encode('utf-8')
|
|
75
|
+
)
|
|
76
|
+
return sha.hexdigest()
|
|
77
|
+
|
|
78
|
+
def to_dict(self):
|
|
79
|
+
return {
|
|
80
|
+
'timestamp': str(self.timestamp),
|
|
81
|
+
'data': self.data,
|
|
82
|
+
'previous_hash': self.previous_hash,
|
|
83
|
+
'operation_type': self.operation_type,
|
|
84
|
+
'file_hash': self.file_hash,
|
|
85
|
+
'user': self.user,
|
|
86
|
+
'memo': self.memo,
|
|
87
|
+
'hash': self.hash
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
class Blockchain:
|
|
91
|
+
def __init__(self):
|
|
92
|
+
self.chain = []
|
|
93
|
+
|
|
94
|
+
def add_block(self, new_block):
|
|
95
|
+
if len(self.chain) == 0:
|
|
96
|
+
new_block.previous_hash = "0"
|
|
97
|
+
else:
|
|
98
|
+
new_block.previous_hash = self.chain[-1].hash
|
|
99
|
+
new_block.hash = new_block.calculate_hash()
|
|
100
|
+
self.chain.append(new_block)
|
|
101
|
+
|
|
102
|
+
def to_json(self):
|
|
103
|
+
return json.dumps([block.to_dict() for block in self.chain], indent=2)
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def from_json(data):
|
|
107
|
+
chain_data = json.loads(data)
|
|
108
|
+
blockchain = Blockchain()
|
|
109
|
+
for block_data in chain_data:
|
|
110
|
+
block = Block(
|
|
111
|
+
data=block_data['data'],
|
|
112
|
+
previous_hash=block_data['previous_hash'],
|
|
113
|
+
operation_type=block_data['operation_type'],
|
|
114
|
+
file_hash=block_data['file_hash'],
|
|
115
|
+
user=block_data['user'],
|
|
116
|
+
memo=block_data['memo']
|
|
117
|
+
)
|
|
118
|
+
block.timestamp = datetime.datetime.strptime(block_data['timestamp'], '%Y-%m-%d %H:%M:%S.%f%z')
|
|
119
|
+
block.hash = block_data['hash']
|
|
120
|
+
blockchain.chain.append(block)
|
|
121
|
+
return blockchain
|
|
122
|
+
|
|
123
|
+
def is_chain_valid(self):
|
|
124
|
+
for i in range(1, len(self.chain)):
|
|
125
|
+
current = self.chain[i]
|
|
126
|
+
previous = self.chain[i - 1]
|
|
127
|
+
if current.previous_hash != previous.hash:
|
|
128
|
+
return False
|
|
129
|
+
if current.calculate_hash() != current.hash:
|
|
130
|
+
return False
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
deletemode = {
|
|
134
|
+
"mode": False
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
def delete_pre_file(file_path):
|
|
138
|
+
path_o = Path(file_path)
|
|
139
|
+
abs_path = path_o.resolve()
|
|
140
|
+
if abs_path.exists():
|
|
141
|
+
abs_path.unlink()
|
|
142
|
+
|
|
143
|
+
def cli_encrypt(file_path, password, memo):
|
|
144
|
+
from . import passchk
|
|
145
|
+
with open(file_path, 'rb') as f:
|
|
146
|
+
plaintext = f.read()
|
|
147
|
+
salt = get_random_bytes(16)
|
|
148
|
+
|
|
149
|
+
if passchk.passchk(memo, password):
|
|
150
|
+
print(" Warning: The note contains a password.\n For security reasons, it is recommended not to include passwords in notes.")
|
|
151
|
+
a = input(" Do you want to continue (y or n) >> ")
|
|
152
|
+
if a.lower() != "y":
|
|
153
|
+
print(" Canceled the encryption.")
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
key = PBKDF2(password, salt, dkLen=32, count=100_000)
|
|
157
|
+
nonce = get_random_bytes(12)
|
|
158
|
+
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
|
|
159
|
+
ciphertext, tag = cipher.encrypt_and_digest(plaintext)
|
|
160
|
+
file_hash = hashlib.sha256(ciphertext).hexdigest()
|
|
161
|
+
username = getpass.getuser()
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
with lzma.open(file_path + ".vdec", 'rb') as f:
|
|
165
|
+
data = f.read()
|
|
166
|
+
split_index = data.index(BLOCKCHAIN_HEADER)
|
|
167
|
+
chain_json = data[split_index + len(BLOCKCHAIN_HEADER):].decode('utf-8')
|
|
168
|
+
blockchain = Blockchain.from_json(chain_json)
|
|
169
|
+
except:
|
|
170
|
+
blockchain = Blockchain()
|
|
171
|
+
block = Block(file_hash, blockchain.chain[-1].hash if blockchain.chain else "0", "Encrypt", file_hash, username, memo)
|
|
172
|
+
blockchain.add_block(block)
|
|
173
|
+
|
|
174
|
+
encrypted_data = salt + nonce + ciphertext + tag
|
|
175
|
+
blockchain_data = BLOCKCHAIN_HEADER + blockchain.to_json().encode('utf-8')
|
|
176
|
+
|
|
177
|
+
out_path = file_path + ".vdec"
|
|
178
|
+
with lzma.open(out_path, 'wb') as f:
|
|
179
|
+
f.write(encrypted_data)
|
|
180
|
+
f.write(blockchain_data)
|
|
181
|
+
if deletemode["mode"] == True:
|
|
182
|
+
delete_pre_file(file_path)
|
|
183
|
+
print(f"✅ Encryption completed: {out_path}")
|
|
184
|
+
|
|
185
|
+
def cli_decrypt(file_path, password, memo):
|
|
186
|
+
if file_path.endswith(".wav"):
|
|
187
|
+
with open(file_path, 'rb') as f:
|
|
188
|
+
wav_bytes = f.read()
|
|
189
|
+
data = wavencode.wav_bytes_to_binary(wav_bytes)
|
|
190
|
+
else:
|
|
191
|
+
with lzma.open(file_path, 'rb') as f:
|
|
192
|
+
data = f.read()
|
|
193
|
+
|
|
194
|
+
split_index = data.index(BLOCKCHAIN_HEADER)
|
|
195
|
+
crypto_data = data[:split_index]
|
|
196
|
+
chain_json = data[split_index + len(BLOCKCHAIN_HEADER):].decode('utf-8')
|
|
197
|
+
blockchain = Blockchain.from_json(chain_json)
|
|
198
|
+
|
|
199
|
+
salt = crypto_data[:16]
|
|
200
|
+
nonce = crypto_data[16:28]
|
|
201
|
+
tag = crypto_data[-16:]
|
|
202
|
+
ciphertext = crypto_data[28:-16]
|
|
203
|
+
|
|
204
|
+
key = PBKDF2(password, salt, dkLen=32, count=100_000)
|
|
205
|
+
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
|
|
206
|
+
try:
|
|
207
|
+
plaintext = cipher.decrypt_and_verify(ciphertext, tag)
|
|
208
|
+
except ValueError:
|
|
209
|
+
print("❌ Error: Decryption failed. The password may be incorrect or the file may be tampered with.")
|
|
210
|
+
sys.exit(1)
|
|
211
|
+
|
|
212
|
+
output_file = file_path.replace(".vdec.wav", "_decrypted").replace(".vdec", "_decrypted")
|
|
213
|
+
with open(output_file, 'wb') as f:
|
|
214
|
+
f.write(plaintext)
|
|
215
|
+
|
|
216
|
+
username = getpass.getuser()
|
|
217
|
+
file_hash = hashlib.sha256(ciphertext).hexdigest()
|
|
218
|
+
block = Block(file_hash, blockchain.chain[-1].hash if blockchain.chain else "0", "Decrypt", file_hash, username, memo)
|
|
219
|
+
blockchain.add_block(block)
|
|
220
|
+
|
|
221
|
+
if not file_path.endswith(".wav"):
|
|
222
|
+
with lzma.open(file_path, 'wb') as f:
|
|
223
|
+
f.write(salt + nonce + ciphertext + tag)
|
|
224
|
+
f.write(BLOCKCHAIN_HEADER)
|
|
225
|
+
f.write(blockchain.to_json().encode('utf-8'))
|
|
226
|
+
|
|
227
|
+
print(f"✅ Decryption completed: {output_file}")
|
|
228
|
+
|
|
229
|
+
def cli_verify_chain(file_path):
|
|
230
|
+
if file_path.endswith(".wav"):
|
|
231
|
+
with open(file_path, 'rb') as f:
|
|
232
|
+
data = wavencode.wav_bytes_to_binary(f.read())
|
|
233
|
+
else:
|
|
234
|
+
with lzma.open(file_path, 'rb') as f:
|
|
235
|
+
data = f.read()
|
|
236
|
+
split_index = data.index(BLOCKCHAIN_HEADER)
|
|
237
|
+
chain_json = data[split_index + len(BLOCKCHAIN_HEADER):].decode('utf-8')
|
|
238
|
+
blockchain = Blockchain.from_json(chain_json)
|
|
239
|
+
if blockchain.is_chain_valid():
|
|
240
|
+
print("✅ Blockchain is consistent")
|
|
241
|
+
else:
|
|
242
|
+
print("❌ Blockchain has inconsistencies")
|
|
243
|
+
|
|
244
|
+
# --- RSA key check at startup ---
|
|
245
|
+
rsa_encryptor.ensure_rsa_keys()
|
|
246
|
+
from .utils import decrypt_folder_cli,encrypt_folder
|
|
247
|
+
|
|
248
|
+
def main():
|
|
249
|
+
parser = argparse.ArgumentParser(description="EncryptSecureDEC CLI")
|
|
250
|
+
parser.add_argument("mode",choices=["encrypt","decrypt","verify-chain","sign",
|
|
251
|
+
"verify-sign","key-protect-on","key-protect-off"])
|
|
252
|
+
parser.add_argument("file", help="Target file path")
|
|
253
|
+
parser.add_argument("--memo", default="", help="Operation memo")
|
|
254
|
+
parser.add_argument("--password", help="Password for encryption/decryption (prompted if omitted)")
|
|
255
|
+
parser.add_argument("--delete", action="store_true",help="Delete the plaintext file after encrypting the file.")
|
|
256
|
+
parser.add_argument("--rsa", action="store_true",help="Encrypt / Decrypt RSA Mode")
|
|
257
|
+
parser.add_argument("--dir", action="store_true",help="Encrypt Dir mode")
|
|
258
|
+
parser.add_argument("--pubkey", help="Path to public key file (only required in RSA encrypt mode)")
|
|
259
|
+
args = parser.parse_args()
|
|
260
|
+
|
|
261
|
+
# --- validate RSA/pubkey usage ---
|
|
262
|
+
# --- validate RSA/pubkey usage ---
|
|
263
|
+
if args.rsa:
|
|
264
|
+
if args.mode == "decrypt" and args.pubkey:
|
|
265
|
+
parser.error("--pubkey must not be specified in decrypt mode (private key is used automatically)")
|
|
266
|
+
file_path = Path(args.file)
|
|
267
|
+
ext = file_path.suffix
|
|
268
|
+
|
|
269
|
+
# --- RSA Key Protection Management ---
|
|
270
|
+
if args.mode == "key-protect-on":
|
|
271
|
+
result = rsa_encryptor.migrate_private_key_encrypt_inplace()
|
|
272
|
+
if result == 0:
|
|
273
|
+
print("🔐 Private key protection ENABLED")
|
|
274
|
+
sys.exit(result)
|
|
275
|
+
|
|
276
|
+
if args.mode == "key-protect-off":
|
|
277
|
+
result = rsa_encryptor.migrate_private_key_decrypt_inplace()
|
|
278
|
+
if result == 0:
|
|
279
|
+
print("🔓 Private key protection DISABLED")
|
|
280
|
+
sys.exit(result)
|
|
281
|
+
|
|
282
|
+
if not args.rsa and ext != ".rdec" and args.mode != "sign" and args.mode != "verify-sign" and args.dir!=True and ext!=".esdc":
|
|
283
|
+
password = args.password or getpass.getpass("🔑 Enter password: ")
|
|
284
|
+
|
|
285
|
+
# Check if file exists
|
|
286
|
+
if not os.path.isfile(args.file) and args.dir!=True:
|
|
287
|
+
print(f"❌ Error: File not found - {args.file}")
|
|
288
|
+
sys.exit(1)
|
|
289
|
+
|
|
290
|
+
# For decrypt mode, check extension
|
|
291
|
+
if args.mode == "decrypt" and not (args.file.endswith(".vdec") or args.file.endswith(".rdec") or args.file.endswith(".esdc")):
|
|
292
|
+
print(f"❌ Error: The file for decryption must have a '.vdec' or '.rdec' extension.")
|
|
293
|
+
sys.exit(1)
|
|
294
|
+
|
|
295
|
+
if args.delete:
|
|
296
|
+
deletemode["mode"] = True
|
|
297
|
+
|
|
298
|
+
if args.mode=="decrypt" and os.path.splitext(args.file)[1].lower()==".esdc":
|
|
299
|
+
if os.path.splitext(args.file)[1].lower()==".esdc":
|
|
300
|
+
if os.path.exists(args.file)==False:
|
|
301
|
+
print("Decryption failed: Failed to decrypt\n file not found")
|
|
302
|
+
sys.exit(1)
|
|
303
|
+
p = file_path
|
|
304
|
+
|
|
305
|
+
# 「そのパスが属するディレクトリ」を取りたい場合
|
|
306
|
+
parent_dir = p.parent
|
|
307
|
+
# ルート判定(E:\ や C:\ など)
|
|
308
|
+
is_root = parent_dir == Path(parent_dir.anchor)
|
|
309
|
+
|
|
310
|
+
if is_root:
|
|
311
|
+
# ルート直下に作業用フォルダを作る(例: E:\_esdwork)
|
|
312
|
+
work_dir = parent_dir / "_esdwork"
|
|
313
|
+
else:
|
|
314
|
+
work_dir = parent_dir / "_esdwork" # どのみちサブフォルダに逃がすと安全
|
|
315
|
+
work_dir.mkdir(parents=True, exist_ok=True)
|
|
316
|
+
|
|
317
|
+
res=decrypt_folder_cli(args.file,work_dir)
|
|
318
|
+
if res==1:
|
|
319
|
+
print("Decryption successful: Decryption was successful\n")
|
|
320
|
+
else:
|
|
321
|
+
print(f"Decryption failed: Failed to decrypt\n {res}")
|
|
322
|
+
sys.exit()
|
|
323
|
+
if args.mode=="encrypt" and args.dir==True:
|
|
324
|
+
if os.path.exists(args.file)==False:
|
|
325
|
+
print("Encryption failed: Failed to encrypt\n Dir not Found")
|
|
326
|
+
sys.exit(1)
|
|
327
|
+
# ルート判定(E:\ や C:\ など)
|
|
328
|
+
folder_path = Path(args.file).resolve()
|
|
329
|
+
parent_dir = folder_path.parent
|
|
330
|
+
base_name = folder_path.name
|
|
331
|
+
|
|
332
|
+
size_bytes = _dir_size_bytes(str(folder_path))
|
|
333
|
+
print(f"📦 Folder size: {_format_bytes(size_bytes)} ({size_bytes:,} bytes)")
|
|
334
|
+
|
|
335
|
+
out_file = str(parent_dir / f"{base_name}.esdc")
|
|
336
|
+
res=encrypt_folder(args.file,out_file)
|
|
337
|
+
if res==1:
|
|
338
|
+
print("Encryption successful: Encryption was successful\n")
|
|
339
|
+
else:
|
|
340
|
+
print(f"Encryption failed: Failed to encrypt\n {res}")
|
|
341
|
+
sys.exit(0)
|
|
342
|
+
|
|
343
|
+
# --- RSA Mode ---
|
|
344
|
+
if args.rsa and args.mode == "encrypt":
|
|
345
|
+
pubkey_path = args.pubkey if args.pubkey else str(rsa_encryptor.RSA_PUB_PATH)
|
|
346
|
+
rsa_encryptor.encrypt_file_with_dialog(args.file, pubkey_path)
|
|
347
|
+
sys.exit()
|
|
348
|
+
elif args.rsa and args.mode == "decrypt":
|
|
349
|
+
rsa_encryptor.decrypt_file_with_dialog(args.file)
|
|
350
|
+
sys.exit()
|
|
351
|
+
|
|
352
|
+
# --- Password Mode ---
|
|
353
|
+
if args.mode == "encrypt":
|
|
354
|
+
cli_encrypt(args.file, password, args.memo)
|
|
355
|
+
elif args.mode == "decrypt":
|
|
356
|
+
cli_decrypt(args.file, password, args.memo)
|
|
357
|
+
elif args.mode == "verify-chain":
|
|
358
|
+
cli_verify_chain(args.file)
|
|
359
|
+
elif args.mode == "sign":
|
|
360
|
+
rsa_signer.sign_file(args.file)
|
|
361
|
+
elif args.mode == "verify-sign":
|
|
362
|
+
rsa_signer.verify_file_signature(args.file)
|
|
363
|
+
else:
|
|
364
|
+
print("❌ Unknown mode")
|
|
365
|
+
|
|
366
|
+
if __name__ == "__main__":
|
|
367
|
+
main()
|