ensec-cli 2.0.0__tar.gz → 2.2.0__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: 2.0.0
3
+ Version: 2.2.0
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__ = "2.2.0"
@@ -0,0 +1,275 @@
1
+ import os
2
+ import json
3
+ import struct
4
+ import configparser
5
+ import hashlib
6
+
7
+
8
+ # ======================================================
9
+ # SHA256
10
+ # ======================================================
11
+ def calc_sha256(data: bytes) -> str:
12
+ return hashlib.sha256(data).hexdigest()
13
+
14
+
15
+ # ======================================================
16
+ # 設定ファイルパス生成
17
+ # ======================================================
18
+ from pathlib import Path
19
+ import os
20
+
21
+ APP_NAME = "ENSEC"
22
+
23
+ def _default_db_path() -> Path:
24
+ if os.name == "nt":
25
+ home = Path.home()
26
+ base = home / ".local" / "share" / APP_NAME
27
+ else:
28
+ xdg_data_home = Path(
29
+ os.getenv(
30
+ "XDG_DATA_HOME",
31
+ Path.home() / ".local" / "share"
32
+ )
33
+ )
34
+ base = xdg_data_home / APP_NAME
35
+
36
+ base.mkdir(parents=True, exist_ok=True)
37
+
38
+ return base / "split_config.ini"
39
+
40
+ def init_split_config():
41
+ """
42
+ split_config.ini が存在しない場合だけ、
43
+ デフォルト(基本無効)の設定ファイルを生成する。
44
+ 既存ファイルがある場合は絶対に上書きしない。
45
+ """
46
+ config_path = _default_db_path()
47
+
48
+ if os.path.exists(config_path):
49
+ return
50
+
51
+ config = configparser.ConfigParser()
52
+
53
+ config["Split"] = {
54
+ "enabled": "false", # 基本無効
55
+ "mode": "half",
56
+ "parts": "2",
57
+ "delmode": "false"
58
+ }
59
+
60
+ with open(config_path, "w", encoding="utf-8") as f:
61
+ config.write(f)
62
+
63
+
64
+
65
+ def _load_split_settings() -> dict:
66
+
67
+ settings = {
68
+ "mode": "count",
69
+ "parts": 4
70
+ }
71
+
72
+ if settings["parts"] < 1:
73
+ settings["parts"] = 1
74
+
75
+ return settings
76
+
77
+
78
+ # ======================================================
79
+ # chunk_size 自動判定
80
+ # ======================================================
81
+ def auto_chunk_size(file_size):
82
+ MB = 1024 * 1024
83
+ if file_size < 10 * MB:
84
+ return None
85
+ elif file_size < 100 * MB:
86
+ return 5 * MB
87
+ elif file_size < 500 * MB:
88
+ return 10 * MB
89
+ elif file_size < 1024 * MB:
90
+ return 20 * MB
91
+ else:
92
+ return 50 * MB
93
+
94
+
95
+ HEADER_SIZE_LEN = 4
96
+
97
+
98
+ # ======================================================
99
+ # ヘッダー付きパート書き込み(署名付き)
100
+ # ======================================================
101
+ def _write_part(path, part_index, total_parts, original_name, data):
102
+ header = {
103
+ "original_name": original_name,
104
+ "part_index": part_index,
105
+ "total_parts": total_parts,
106
+ "chunk_size": len(data),
107
+ "sha256": calc_sha256(data)
108
+ }
109
+
110
+ header_raw = json.dumps(header).encode("utf-8")
111
+ header_size = struct.pack(">I", len(header_raw))
112
+
113
+ with open(path, "wb") as f:
114
+ f.write(header_size)
115
+ f.write(header_raw)
116
+ f.write(data)
117
+
118
+
119
+ # ======================================================
120
+ # 分割(完全自動制御)
121
+ # ======================================================
122
+ def split_file_with_header(file_path):
123
+ sett = _load_split_settings()
124
+
125
+ #if not enable:
126
+ # return None
127
+
128
+ mode = sett["mode"]
129
+ parts = sett["parts"]
130
+
131
+ total_size = os.path.getsize(file_path)
132
+ original_name = os.path.basename(file_path)
133
+
134
+ if mode == "half":
135
+ total_parts = 2
136
+ half = total_size // 2
137
+ chunk_sizes = [half, total_size - half]
138
+
139
+ else:
140
+ total_parts = parts
141
+ base = total_size // parts
142
+ remainder = total_size % parts
143
+ chunk_sizes = [base] * parts
144
+ chunk_sizes[-1] += remainder
145
+
146
+ out_paths = []
147
+
148
+ with open(file_path, "rb") as fin:
149
+ for i in range(total_parts):
150
+ data = fin.read(chunk_sizes[i])
151
+ part_path = f"{file_path}{i}"
152
+
153
+ _write_part(
154
+ path=part_path,
155
+ part_index=i,
156
+ total_parts=total_parts,
157
+ original_name=original_name,
158
+ data=data
159
+ )
160
+
161
+ out_paths.append(part_path)
162
+
163
+ return out_paths
164
+
165
+
166
+ def is_delmode_enabled() -> bool:
167
+ config_path = _default_db_path()
168
+
169
+ if not os.path.exists(config_path):
170
+ return False
171
+
172
+ config = configparser.ConfigParser()
173
+ config.read(config_path, encoding="utf-8")
174
+
175
+ return config.getboolean("Split", "delmode", fallback=False)
176
+
177
+
178
+ # ======================================================
179
+ # 拡張子取得
180
+ # ======================================================
181
+ def get_file_extension(file_path: str) -> str:
182
+ if not isinstance(file_path, str) or not file_path.strip():
183
+ raise ValueError("ファイルパスは非空の文字列で指定してください。")
184
+ _, ext = os.path.splitext(file_path)
185
+ return ext
186
+
187
+
188
+ # ======================================================
189
+ # 結合(part0 → 自動判定)
190
+ # ======================================================
191
+ import re
192
+ def merge_from_part0(part0_path, output_path=None):
193
+ part0_path = os.path.abspath(part0_path)
194
+
195
+ folder = os.path.dirname(part0_path)
196
+ base = os.path.basename(part0_path)
197
+ prefix = base[:-1]
198
+
199
+ pattern = re.compile(rf"^{re.escape(prefix)}\d+$")
200
+
201
+ parts = [
202
+ os.path.join(folder, fn)
203
+ for fn in os.listdir(folder)
204
+ if pattern.match(fn)
205
+ ]
206
+
207
+ return merge_files_with_header(parts, output_path)
208
+
209
+
210
+ # ======================================================
211
+ # 完全検証付き結合処理(署名付き)
212
+ # ======================================================
213
+ def merge_files_with_header(parts, output_path=None):
214
+ part_info = []
215
+
216
+ for path in parts:
217
+ try:
218
+ with open(path, "rb") as f:
219
+ raw = f.read(4)
220
+ if len(raw) < 4:
221
+ continue
222
+
223
+ header_size = struct.unpack(">I", raw)[0]
224
+ header_raw = f.read(header_size)
225
+ header = json.loads(header_raw.decode("utf-8"))
226
+
227
+ required = ("original_name", "part_index", "total_parts", "sha256")
228
+ if not all(k in header for k in required):
229
+ raise ValueError(f"{path} のヘッダーが不完全です")
230
+
231
+ part_info.append((header["part_index"], path, header))
232
+
233
+ except Exception as e:
234
+ raise ValueError(f"{path} の読み取り失敗: {e}")
235
+
236
+ if not part_info:
237
+ raise ValueError("適切なパートが見つかりません")
238
+
239
+ part_info.sort(key=lambda x: x[0])
240
+
241
+ names = {h["original_name"] for _,_,h in part_info}
242
+ if len(names) != 1:
243
+ raise ValueError("original_name が一致しません → 別ファイルのパートが混入")
244
+
245
+ original_name = part_info[0][2]["original_name"]
246
+
247
+ if ".." in original_name or "/" in original_name or "\\" in original_name:
248
+ raise ValueError("original_name に危険な文字列があります")
249
+
250
+ total_parts = part_info[0][2]["total_parts"]
251
+ if total_parts != len(part_info):
252
+ raise ValueError("total_parts と実パート数が一致しません")
253
+
254
+ indexes = [idx for idx,_,_ in part_info]
255
+ if sorted(indexes) != list(range(total_parts)):
256
+ raise ValueError("part_index が不正です(重複・欠落・範囲外)")
257
+
258
+ if output_path is None:
259
+ folder = os.path.dirname(parts[0])
260
+ output_path = os.path.join(folder, original_name)
261
+
262
+ with open(output_path, "wb") as out:
263
+ for idx, part_path, header in part_info:
264
+ with open(part_path, "rb") as f:
265
+ hsz = struct.unpack(">I", f.read(4))[0]
266
+ f.read(hsz)
267
+ payload = f.read()
268
+
269
+ # --- 署名検証 ---
270
+ if calc_sha256(payload) != header["sha256"]:
271
+ raise ValueError(f"{part_path} は改ざんされています(ハッシュ不一致)")
272
+
273
+ out.write(payload)
274
+
275
+ return output_path
@@ -18,8 +18,10 @@ from pathlib import Path
18
18
  from .conf_load import load_config
19
19
  from . import __version__ as VERSION
20
20
  from .ecc import *
21
+ from .bin_ext import *
21
22
  # OKならそのまま続行
22
23
 
24
+ is_split_enabled=False
23
25
  conf=load_config()
24
26
 
25
27
  ecc = False
@@ -180,7 +182,7 @@ def cli_encrypt(file_path, password, memo):
180
182
  split_index = data.index(BLOCKCHAIN_HEADER)
181
183
  chain_json = data[split_index + len(BLOCKCHAIN_HEADER):].decode('utf-8')
182
184
  blockchain = Blockchain.from_json(chain_json)
183
- except:
185
+ except Exception as e:
184
186
  if os.path.exists(file_path + ".vdec") :
185
187
  print("警告:既存のブロックチェーンデータの読み込みに失敗しました。\n新規にブロックチェーンを作成します。")
186
188
  blockchain = Blockchain()
@@ -195,17 +197,29 @@ def cli_encrypt(file_path, password, memo):
195
197
  # 先にLZMA圧縮
196
198
  compressed_data = lzma.compress(combined_data)
197
199
 
198
- if ecc==True:
200
+ if ecc==True and not is_split_enabled:
199
201
  ecc_data = add_ecc(compressed_data)
200
202
  else:
201
203
  ecc_data = compressed_data
204
+
202
205
  out_path = file_path + ".vdec"
203
206
  with open(out_path, 'wb') as f:
204
207
  f.write(ecc_data)
205
-
208
+
209
+ if is_split_enabled:
210
+ a = split_file_with_header(out_path)
211
+ if a is not None:
212
+ display_path = a[0]
213
+ else:
214
+ display_path = out_path
215
+ else:
216
+ display_path = out_path
217
+
218
+ if is_split_enabled:
219
+ delete_pre_file(out_path)
206
220
  if deletemode["mode"] == True:
207
221
  delete_pre_file(file_path)
208
- print(f"✅ Encryption completed: {out_path}")
222
+ print(f"✅ Encryption completed: {display_path}")
209
223
 
210
224
  def cli_decrypt(file_path, password, memo):
211
225
  if file_path.endswith(".wav"):
@@ -216,12 +230,26 @@ def cli_decrypt(file_path, password, memo):
216
230
  try:
217
231
  with open(file_path, 'rb') as f:
218
232
  data = f.read()
219
- if ecc==True:
233
+ if get_file_extension(file_path)!=".vdec0":
220
234
  # 分割ファイルでない場合はECC復元とLZMA展開を試みる。失敗したら(分割ファイルでない場合)LZMA展開のみ試みる。
221
- data = recover_data(data) # ECC復元とLZMA展開を試みる(成功すればdataが更新される)
235
+ if ecc==True:
236
+ data = recover_data(data) # ECC復元とLZMA展開を試みる(成功すればdataが更新される)
222
237
 
223
- if data is None:
224
- raise ValueError("データの復元に失敗")
238
+ if data is None:
239
+ raise ValueError("データの復元に失敗")
240
+ else:
241
+ data = lzma.decompress(data)
242
+ elif get_file_extension(file_path)==".vdec0":
243
+ try:
244
+ catc = merge_from_part0(file_path)
245
+ if catc!=None:
246
+ file_path=catc
247
+ with open(file_path, 'rb') as f:
248
+ data = f.read()
249
+ data = lzma.decompress(data)
250
+ except Exception as e:
251
+ print(f"エラー:次のエラーが発生しました:\n{e}")
252
+ return None
225
253
  else:
226
254
  data = lzma.decompress(data)
227
255
  except ValueError as e:
@@ -335,6 +363,7 @@ def password_from_keyfile(keyfile_path: str) -> str:
335
363
  sys.exit(1)
336
364
 
337
365
  return hashlib.sha256(data).hexdigest()
366
+
338
367
  def main():
339
368
  global ecc
340
369
  parser = argparse.ArgumentParser(description="EncryptSecureDEC CLI")
@@ -350,8 +379,11 @@ def main():
350
379
  parser.add_argument("--keyfile",help="Use a file as the password source for encryption/decryption")
351
380
  parser.add_argument("--version",action="version",version=f"ENSEC_CLI {VERSION}")
352
381
  parser.add_argument("--ecc", action="store_true",help="Enable ECC protection")
382
+ parser.add_argument("--split",action="store_true",help="Split encrypted file")
353
383
  args = parser.parse_args()
354
-
384
+ if args.split:
385
+ global is_split_enabled
386
+ is_split_enabled=True
355
387
  if args.ecc:
356
388
  ecc=True
357
389
  # --- validate RSA/pubkey usage ---
@@ -391,7 +423,7 @@ def main():
391
423
  sys.exit(1)
392
424
 
393
425
  # For decrypt mode, check extension
394
- if args.mode == "decrypt" and not (args.file.endswith(".vdec") or args.file.endswith(".rdec") or args.file.endswith(".esdc")):
426
+ if args.mode == "decrypt" and not (args.file.endswith(".vdec") or args.file.endswith(".rdec") or args.file.endswith(".esdc") or args.file.endswith(".vdec0")):
395
427
  print(f"❌ Error: The file for decryption must have a '.vdec' or '.rdec' extension.")
396
428
  sys.exit(1)
397
429
 
@@ -5,20 +5,24 @@ from Crypto.PublicKey import RSA
5
5
  #import tkinter as tk
6
6
  #from tkinter import ttk, messagebox
7
7
 
8
- def _default_app_dir() -> str:
9
- base = os.getenv("APPDATA") or os.path.expanduser("~")
10
- folder = os.path.join(base, "EncryptSecureDEC")
11
- os.makedirs(folder, exist_ok=True)
12
- return folder
8
+ from pathlib import Path
9
+ import os
10
+ import hashlib
11
+ from Crypto.PublicKey import RSA
12
+
13
+ APP_NAME = "ENSEC"
14
+
15
+ def _home_for_xdg():
16
+ return Path.home()
13
17
 
14
- default_keys=_default_app_dir()+"\\key"
18
+ HOME = _home_for_xdg()
19
+ XDG_DATA_HOME = Path(os.getenv("XDG_DATA_HOME", HOME / ".local" / "share"))
20
+ DATA_DIR = XDG_DATA_HOME / APP_NAME
15
21
 
16
22
  def _trusted_dir() -> str:
17
- # default_keys = ".../EncryptSecureDEC/Key" を想定
18
- app_dir = os.path.dirname(default_keys) # ".../EncryptSecureDEC"
19
- d = os.path.join(app_dir, "TrustedKeys")
20
- os.makedirs(d, exist_ok=True)
21
- return d
23
+ d = DATA_DIR / "TrustedKeys"
24
+ d.mkdir(parents=True, exist_ok=True)
25
+ return str(d)
22
26
 
23
27
  def _sha256_fp(pem_bytes: bytes) -> str:
24
28
  h = hashlib.sha256(pem_bytes).hexdigest()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ensec-cli
3
- Version: 2.0.0
3
+ Version: 2.2.0
4
4
  Summary: EncryptSecureDEC Open Source CLI
5
5
  Requires-Python: >=3.9
6
6
  Description-Content-Type: text/markdown
@@ -2,6 +2,7 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  ensec_cli/__init__.py
5
+ ensec_cli/bin_ext.py
5
6
  ensec_cli/cli.py
6
7
  ensec_cli/conf_load.py
7
8
  ensec_cli/ecc.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ensec-cli"
7
- version = "2.0.0"
7
+ version = "2.2.0"
8
8
  description = "EncryptSecureDEC Open Source CLI"
9
9
  requires-python = ">=3.9"
10
10
  readme = "README.md"
@@ -1 +0,0 @@
1
- __version__ = "2.0.0"
File without changes
File without changes
File without changes
File without changes