ensec-cli 1.0.0__tar.gz → 1.0.2__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.
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: ensec-cli
3
+ Version: 1.0.2
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,17 @@
1
+ # ENSEC-CLI
2
+
3
+ EncryptSecureDEC Open Source CLI
4
+
5
+ ## Install
6
+
7
+ pip install ensec-cli
8
+
9
+ ## Usage
10
+
11
+ ensec encrypt test.txt
12
+
13
+ ensec decrypt test.txt.vdec
14
+
15
+ ensec sign test.txt
16
+
17
+ ensec verify-sign test.txt
@@ -0,0 +1 @@
1
+ __version__ = "1.0.2"
@@ -0,0 +1,374 @@
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
+ 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 main():
256
+ parser = argparse.ArgumentParser(description="EncryptSecureDEC CLI")
257
+ parser.add_argument("mode",choices=["encrypt","decrypt","verify-chain","sign",
258
+ "verify-sign","key-protect-on","key-protect-off"])
259
+ parser.add_argument("file", help="Target file path")
260
+ parser.add_argument("--memo", default="", help="Operation memo")
261
+ parser.add_argument("--password", help="Password for encryption/decryption (prompted if omitted)")
262
+ parser.add_argument("--delete", action="store_true",help="Delete the plaintext file after encrypting the file.")
263
+ parser.add_argument("--rsa", action="store_true",help="Encrypt / Decrypt RSA Mode")
264
+ parser.add_argument("--dir", action="store_true",help="Encrypt Dir mode")
265
+ parser.add_argument("--pubkey", help="Path to public key file (only required in RSA encrypt mode)")
266
+ args = parser.parse_args()
267
+
268
+ # --- validate RSA/pubkey usage ---
269
+ # --- validate RSA/pubkey usage ---
270
+ if args.rsa:
271
+ if args.mode == "decrypt" and args.pubkey:
272
+ parser.error("--pubkey must not be specified in decrypt mode (private key is used automatically)")
273
+ file_path = Path(args.file)
274
+ ext = file_path.suffix
275
+
276
+ # --- RSA Key Protection Management ---
277
+ if args.mode == "key-protect-on":
278
+ result = rsa_encryptor.migrate_private_key_encrypt_inplace()
279
+ if result == 0:
280
+ print("🔐 Private key protection ENABLED")
281
+ sys.exit(result)
282
+
283
+ if args.mode == "key-protect-off":
284
+ result = rsa_encryptor.migrate_private_key_decrypt_inplace()
285
+ if result == 0:
286
+ print("🔓 Private key protection DISABLED")
287
+ sys.exit(result)
288
+
289
+ if not args.rsa and ext != ".rdec" and args.mode != "sign" and args.mode != "verify-sign" and args.dir!=True and ext!=".esdc":
290
+ password = args.password or getpass.getpass("🔑 Enter password: ")
291
+
292
+ # Check if file exists
293
+ if not os.path.isfile(args.file) and args.dir!=True:
294
+ print(f"❌ Error: File not found - {args.file}")
295
+ sys.exit(1)
296
+
297
+ # For decrypt mode, check extension
298
+ if args.mode == "decrypt" and not (args.file.endswith(".vdec") or args.file.endswith(".rdec") or args.file.endswith(".esdc")):
299
+ print(f"❌ Error: The file for decryption must have a '.vdec' or '.rdec' extension.")
300
+ sys.exit(1)
301
+
302
+ if args.delete:
303
+ deletemode["mode"] = True
304
+
305
+ if args.mode=="decrypt" and os.path.splitext(args.file)[1].lower()==".esdc":
306
+ if os.path.splitext(args.file)[1].lower()==".esdc":
307
+ if os.path.exists(args.file)==False:
308
+ print("Decryption failed: Failed to decrypt\n file not found")
309
+ sys.exit(1)
310
+ p = file_path
311
+
312
+ # 「そのパスが属するディレクトリ」を取りたい場合
313
+ parent_dir = p.parent
314
+ # ルート判定(E:\ や C:\ など)
315
+ is_root = parent_dir == Path(parent_dir.anchor)
316
+
317
+ if is_root:
318
+ # ルート直下に作業用フォルダを作る(例: E:\_esdwork)
319
+ work_dir = parent_dir / "_esdwork"
320
+ else:
321
+ work_dir = parent_dir / "_esdwork" # どのみちサブフォルダに逃がすと安全
322
+ work_dir.mkdir(parents=True, exist_ok=True)
323
+
324
+ res=decrypt_folder_cli(args.file,work_dir)
325
+ if res==1:
326
+ print("Decryption successful: Decryption was successful\n")
327
+ else:
328
+ print(f"Decryption failed: Failed to decrypt\n {res}")
329
+ sys.exit()
330
+ if args.mode=="encrypt" and args.dir==True:
331
+ if os.path.exists(args.file)==False:
332
+ print("Encryption failed: Failed to encrypt\n Dir not Found")
333
+ sys.exit(1)
334
+ # ルート判定(E:\ や C:\ など)
335
+ folder_path = Path(args.file).resolve()
336
+ parent_dir = folder_path.parent
337
+ base_name = folder_path.name
338
+
339
+ size_bytes = _dir_size_bytes(str(folder_path))
340
+ print(f"📦 Folder size: {_format_bytes(size_bytes)} ({size_bytes:,} bytes)")
341
+
342
+ out_file = str(parent_dir / f"{base_name}.esdc")
343
+ res=encrypt_folder(args.file,out_file)
344
+ if res==1:
345
+ print("Encryption successful: Encryption was successful\n")
346
+ else:
347
+ print(f"Encryption failed: Failed to encrypt\n {res}")
348
+ sys.exit(0)
349
+
350
+ # --- RSA Mode ---
351
+ if args.rsa and args.mode == "encrypt":
352
+ pubkey_path = args.pubkey if args.pubkey else str(rsa_encryptor.RSA_PUB_PATH)
353
+ rsa_encryptor.encrypt_file_with_dialog(args.file, pubkey_path)
354
+ sys.exit()
355
+ elif args.rsa and args.mode == "decrypt":
356
+ rsa_encryptor.decrypt_file_with_dialog(args.file)
357
+ sys.exit()
358
+
359
+ # --- Password Mode ---
360
+ if args.mode == "encrypt":
361
+ cli_encrypt(args.file, password, args.memo)
362
+ elif args.mode == "decrypt":
363
+ cli_decrypt(args.file, password, args.memo)
364
+ elif args.mode == "verify-chain":
365
+ cli_verify_chain(args.file)
366
+ elif args.mode == "sign":
367
+ rsa_signer.sign_file(args.file)
368
+ elif args.mode == "verify-sign":
369
+ rsa_signer.verify_file_signature(args.file)
370
+ else:
371
+ print("❌ Unknown mode")
372
+
373
+ if __name__ == "__main__":
374
+ main()
@@ -0,0 +1,49 @@
1
+ import configparser
2
+ import os
3
+
4
+ CONFIG_FILE = "config.ini"
5
+
6
+ def load_config():
7
+ """
8
+ config.ini を読み込み、
9
+ 復号化後ファイル名に _decrypted を付けるかどうかを返す。
10
+
11
+ 戻り値:
12
+ bool
13
+ """
14
+
15
+ # デフォルト設定
16
+ default_config = {
17
+ "add_decrypted_suffix": "true"
18
+ }
19
+
20
+ config = configparser.ConfigParser()
21
+
22
+ # configファイルが存在しない場合は自動生成
23
+ if not os.path.exists(CONFIG_FILE):
24
+ config["Settings"] = default_config
25
+
26
+ with open(CONFIG_FILE, "w", encoding="utf-8") as f:
27
+ config.write(f)
28
+
29
+ print(f"[INFO] {CONFIG_FILE} を作成しました")
30
+
31
+ # 読み込み
32
+ config.read(CONFIG_FILE, encoding="utf-8")
33
+
34
+ # bool型として取得
35
+ add_suffix = config.getboolean(
36
+ "Settings",
37
+ "add_decrypted_suffix",
38
+ fallback=True
39
+ )
40
+
41
+ return add_suffix
42
+
43
+
44
+ # 使用例
45
+ if __name__ == "__main__":
46
+ if load_config():
47
+ print("復号化後ファイル名に _decrypted を付けます")
48
+ else:
49
+ print("復号化後ファイル名に _decrypted を付けません")