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