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.
- {ensec_cli-2.0.0 → ensec_cli-2.2.0}/PKG-INFO +1 -1
- ensec_cli-2.2.0/ensec_cli/__init__.py +1 -0
- ensec_cli-2.2.0/ensec_cli/bin_ext.py +275 -0
- {ensec_cli-2.0.0 → ensec_cli-2.2.0}/ensec_cli/cli.py +42 -10
- {ensec_cli-2.0.0 → ensec_cli-2.2.0}/ensec_cli/utils.py +15 -11
- {ensec_cli-2.0.0 → ensec_cli-2.2.0}/ensec_cli.egg-info/PKG-INFO +1 -1
- {ensec_cli-2.0.0 → ensec_cli-2.2.0}/ensec_cli.egg-info/SOURCES.txt +1 -0
- {ensec_cli-2.0.0 → ensec_cli-2.2.0}/pyproject.toml +1 -1
- ensec_cli-2.0.0/ensec_cli/__init__.py +0 -1
- {ensec_cli-2.0.0 → ensec_cli-2.2.0}/LICENSE +0 -0
- {ensec_cli-2.0.0 → ensec_cli-2.2.0}/README.md +0 -0
- {ensec_cli-2.0.0 → ensec_cli-2.2.0}/ensec_cli/conf_load.py +0 -0
- {ensec_cli-2.0.0 → ensec_cli-2.2.0}/ensec_cli/ecc.py +0 -0
- {ensec_cli-2.0.0 → ensec_cli-2.2.0}/ensec_cli/esdcompress.py +0 -0
- {ensec_cli-2.0.0 → ensec_cli-2.2.0}/ensec_cli/passchk.py +0 -0
- {ensec_cli-2.0.0 → ensec_cli-2.2.0}/ensec_cli/rsa_encryptor.py +0 -0
- {ensec_cli-2.0.0 → ensec_cli-2.2.0}/ensec_cli/rsa_signer.py +0 -0
- {ensec_cli-2.0.0 → ensec_cli-2.2.0}/ensec_cli/wavencode.py +0 -0
- {ensec_cli-2.0.0 → ensec_cli-2.2.0}/ensec_cli.egg-info/dependency_links.txt +0 -0
- {ensec_cli-2.0.0 → ensec_cli-2.2.0}/ensec_cli.egg-info/entry_points.txt +0 -0
- {ensec_cli-2.0.0 → ensec_cli-2.2.0}/ensec_cli.egg-info/requires.txt +0 -0
- {ensec_cli-2.0.0 → ensec_cli-2.2.0}/ensec_cli.egg-info/top_level.txt +0 -0
- {ensec_cli-2.0.0 → ensec_cli-2.2.0}/setup.cfg +0 -0
|
@@ -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: {
|
|
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
|
|
233
|
+
if get_file_extension(file_path)!=".vdec0":
|
|
220
234
|
# 分割ファイルでない場合はECC復元とLZMA展開を試みる。失敗したら(分割ファイルでない場合)LZMA展開のみ試みる。
|
|
221
|
-
|
|
235
|
+
if ecc==True:
|
|
236
|
+
data = recover_data(data) # ECC復元とLZMA展開を試みる(成功すればdataが更新される)
|
|
222
237
|
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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 +0,0 @@
|
|
|
1
|
-
__version__ = "2.0.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|