ensec-cli 1.0.11__tar.gz → 2.1.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-1.0.11 → ensec_cli-2.1.0}/PKG-INFO +2 -1
- ensec_cli-2.1.0/ensec_cli/__init__.py +1 -0
- ensec_cli-2.1.0/ensec_cli/bin_ext.py +275 -0
- {ensec_cli-1.0.11 → ensec_cli-2.1.0}/ensec_cli/cli.py +113 -17
- ensec_cli-2.1.0/ensec_cli/ecc.py +29 -0
- {ensec_cli-1.0.11 → ensec_cli-2.1.0}/ensec_cli.egg-info/PKG-INFO +2 -1
- {ensec_cli-1.0.11 → ensec_cli-2.1.0}/ensec_cli.egg-info/SOURCES.txt +2 -0
- {ensec_cli-1.0.11 → ensec_cli-2.1.0}/ensec_cli.egg-info/requires.txt +1 -0
- {ensec_cli-1.0.11 → ensec_cli-2.1.0}/pyproject.toml +3 -2
- ensec_cli-1.0.11/ensec_cli/__init__.py +0 -1
- {ensec_cli-1.0.11 → ensec_cli-2.1.0}/LICENSE +0 -0
- {ensec_cli-1.0.11 → ensec_cli-2.1.0}/README.md +0 -0
- {ensec_cli-1.0.11 → ensec_cli-2.1.0}/ensec_cli/conf_load.py +0 -0
- {ensec_cli-1.0.11 → ensec_cli-2.1.0}/ensec_cli/esdcompress.py +0 -0
- {ensec_cli-1.0.11 → ensec_cli-2.1.0}/ensec_cli/passchk.py +0 -0
- {ensec_cli-1.0.11 → ensec_cli-2.1.0}/ensec_cli/rsa_encryptor.py +0 -0
- {ensec_cli-1.0.11 → ensec_cli-2.1.0}/ensec_cli/rsa_signer.py +0 -0
- {ensec_cli-1.0.11 → ensec_cli-2.1.0}/ensec_cli/utils.py +0 -0
- {ensec_cli-1.0.11 → ensec_cli-2.1.0}/ensec_cli/wavencode.py +0 -0
- {ensec_cli-1.0.11 → ensec_cli-2.1.0}/ensec_cli.egg-info/dependency_links.txt +0 -0
- {ensec_cli-1.0.11 → ensec_cli-2.1.0}/ensec_cli.egg-info/entry_points.txt +0 -0
- {ensec_cli-1.0.11 → ensec_cli-2.1.0}/ensec_cli.egg-info/top_level.txt +0 -0
- {ensec_cli-1.0.11 → ensec_cli-2.1.0}/setup.cfg +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ensec-cli
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: EncryptSecureDEC Open Source CLI
|
|
5
5
|
Requires-Python: >=3.9
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
7
|
License-File: LICENSE
|
|
8
8
|
Requires-Dist: pycryptodome
|
|
9
9
|
Requires-Dist: cryptography
|
|
10
|
+
Requires-Dist: reedsolo
|
|
10
11
|
Dynamic: license-file
|
|
11
12
|
|
|
12
13
|
# ENSEC-CLI
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.1.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
|
|
@@ -17,11 +17,15 @@ from . import rsa_encryptor
|
|
|
17
17
|
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
|
|
|
27
|
+
ecc = False
|
|
28
|
+
|
|
25
29
|
BLOCKCHAIN_HEADER = b'BLOCKCHAIN_DATA_START\n'
|
|
26
30
|
def _format_bytes(num: int) -> str:
|
|
27
31
|
units = ["B", "KB", "MB", "GB", "TB", "PB"]
|
|
@@ -164,26 +168,58 @@ def cli_encrypt(file_path, password, memo):
|
|
|
164
168
|
username = getpass.getuser()
|
|
165
169
|
|
|
166
170
|
try:
|
|
167
|
-
with
|
|
168
|
-
|
|
171
|
+
with open(file_path + ".vdec", 'rb') as f:
|
|
172
|
+
raw_data = f.read()
|
|
173
|
+
if ecc==True:
|
|
174
|
+
try:
|
|
175
|
+
data = recover_data(raw_data)
|
|
176
|
+
if data is None:
|
|
177
|
+
data = lzma.decompress(raw_data)
|
|
178
|
+
except(ValueError, lzma.LZMAError):
|
|
179
|
+
data = lzma.decompress(raw_data)
|
|
180
|
+
else:
|
|
181
|
+
data=lzma.decompress(raw_data)
|
|
169
182
|
split_index = data.index(BLOCKCHAIN_HEADER)
|
|
170
183
|
chain_json = data[split_index + len(BLOCKCHAIN_HEADER):].decode('utf-8')
|
|
171
184
|
blockchain = Blockchain.from_json(chain_json)
|
|
172
|
-
except:
|
|
185
|
+
except Exception as e:
|
|
186
|
+
if os.path.exists(file_path + ".vdec") :
|
|
187
|
+
print("警告:既存のブロックチェーンデータの読み込みに失敗しました。\n新規にブロックチェーンを作成します。")
|
|
173
188
|
blockchain = Blockchain()
|
|
189
|
+
|
|
174
190
|
block = Block(file_hash, blockchain.chain[-1].hash if blockchain.chain else "0", "Encrypt", file_hash, username, memo)
|
|
175
191
|
blockchain.add_block(block)
|
|
176
192
|
|
|
177
193
|
encrypted_data = salt + nonce + ciphertext + tag
|
|
178
194
|
blockchain_data = BLOCKCHAIN_HEADER + blockchain.to_json().encode('utf-8')
|
|
179
195
|
|
|
196
|
+
combined_data = encrypted_data + blockchain_data
|
|
197
|
+
# 先にLZMA圧縮
|
|
198
|
+
compressed_data = lzma.compress(combined_data)
|
|
199
|
+
|
|
200
|
+
if ecc==True and not is_split_enabled:
|
|
201
|
+
ecc_data = add_ecc(compressed_data)
|
|
202
|
+
else:
|
|
203
|
+
ecc_data = compressed_data
|
|
204
|
+
|
|
180
205
|
out_path = file_path + ".vdec"
|
|
181
|
-
with
|
|
182
|
-
f.write(
|
|
183
|
-
|
|
206
|
+
with open(out_path, 'wb') as f:
|
|
207
|
+
f.write(ecc_data)
|
|
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)
|
|
184
220
|
if deletemode["mode"] == True:
|
|
185
221
|
delete_pre_file(file_path)
|
|
186
|
-
print(f"✅ Encryption completed: {
|
|
222
|
+
print(f"✅ Encryption completed: {display_path}")
|
|
187
223
|
|
|
188
224
|
def cli_decrypt(file_path, password, memo):
|
|
189
225
|
if file_path.endswith(".wav"):
|
|
@@ -191,8 +227,41 @@ def cli_decrypt(file_path, password, memo):
|
|
|
191
227
|
wav_bytes = f.read()
|
|
192
228
|
data = wavencode.wav_bytes_to_binary(wav_bytes)
|
|
193
229
|
else:
|
|
194
|
-
|
|
195
|
-
|
|
230
|
+
try:
|
|
231
|
+
with open(file_path, 'rb') as f:
|
|
232
|
+
data = f.read()
|
|
233
|
+
if get_file_extension(file_path)!=".vdec0":
|
|
234
|
+
# 分割ファイルでない場合はECC復元とLZMA展開を試みる。失敗したら(分割ファイルでない場合)LZMA展開のみ試みる。
|
|
235
|
+
if ecc==True:
|
|
236
|
+
data = recover_data(data) # ECC復元とLZMA展開を試みる(成功すればdataが更新される)
|
|
237
|
+
|
|
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
|
|
253
|
+
else:
|
|
254
|
+
data = lzma.decompress(data)
|
|
255
|
+
except ValueError as e:
|
|
256
|
+
print("エラー:ファイルの読み込みに失敗しました")
|
|
257
|
+
print("ECCでも復元できないくらいにファイルが破損している可能性があります")
|
|
258
|
+
return
|
|
259
|
+
except EOFError:
|
|
260
|
+
print("エラー:圧縮データが途中で途切れています\n(ファイルが破損している可能性があります)\n分割データの場合必要なパーツが足りません")
|
|
261
|
+
return
|
|
262
|
+
except lzma.LZMAError:
|
|
263
|
+
print("エラー:LZMA展開に失敗しました")
|
|
264
|
+
return
|
|
196
265
|
|
|
197
266
|
split_index = data.index(BLOCKCHAIN_HEADER)
|
|
198
267
|
crypto_data = data[:split_index]
|
|
@@ -225,12 +294,24 @@ def cli_decrypt(file_path, password, memo):
|
|
|
225
294
|
file_hash = hashlib.sha256(ciphertext).hexdigest()
|
|
226
295
|
block = Block(file_hash, blockchain.chain[-1].hash if blockchain.chain else "0", "Decrypt", file_hash, username, memo)
|
|
227
296
|
blockchain.add_block(block)
|
|
297
|
+
|
|
298
|
+
# 暗号バイト列
|
|
299
|
+
encrypted_data = salt + nonce + ciphertext + tag
|
|
300
|
+
blockchain_data = BLOCKCHAIN_HEADER + blockchain.to_json().encode('utf-8')
|
|
301
|
+
|
|
302
|
+
combined_data = encrypted_data + blockchain_data
|
|
303
|
+
|
|
304
|
+
compressed_data = lzma.compress(combined_data)
|
|
305
|
+
|
|
306
|
+
if ecc==True:
|
|
307
|
+
ecc_data = add_ecc(compressed_data)
|
|
308
|
+
else:
|
|
309
|
+
ecc_data = compressed_data
|
|
228
310
|
|
|
229
311
|
if not file_path.endswith(".wav"):
|
|
230
|
-
with
|
|
231
|
-
f.write(
|
|
232
|
-
|
|
233
|
-
f.write(blockchain.to_json().encode('utf-8'))
|
|
312
|
+
with open(file_path, 'wb') as f:
|
|
313
|
+
f.write(ecc_data)
|
|
314
|
+
|
|
234
315
|
|
|
235
316
|
print(f"✅ Decryption completed: {output_file}")
|
|
236
317
|
|
|
@@ -239,8 +320,15 @@ def cli_verify_chain(file_path):
|
|
|
239
320
|
with open(file_path, 'rb') as f:
|
|
240
321
|
data = wavencode.wav_bytes_to_binary(f.read())
|
|
241
322
|
else:
|
|
242
|
-
with
|
|
323
|
+
with open(file_path, 'rb') as f:
|
|
243
324
|
data = f.read()
|
|
325
|
+
if ecc == True:
|
|
326
|
+
data = recover_data(data)
|
|
327
|
+
if data is None:
|
|
328
|
+
print("❌ Error: データの復元に失敗しました")
|
|
329
|
+
return
|
|
330
|
+
else:
|
|
331
|
+
data = lzma.decompress(data)
|
|
244
332
|
split_index = data.index(BLOCKCHAIN_HEADER)
|
|
245
333
|
chain_json = data[split_index + len(BLOCKCHAIN_HEADER):].decode('utf-8')
|
|
246
334
|
blockchain = Blockchain.from_json(chain_json)
|
|
@@ -275,7 +363,9 @@ def password_from_keyfile(keyfile_path: str) -> str:
|
|
|
275
363
|
sys.exit(1)
|
|
276
364
|
|
|
277
365
|
return hashlib.sha256(data).hexdigest()
|
|
366
|
+
|
|
278
367
|
def main():
|
|
368
|
+
global ecc
|
|
279
369
|
parser = argparse.ArgumentParser(description="EncryptSecureDEC CLI")
|
|
280
370
|
parser.add_argument("mode",choices=["encrypt","decrypt","verify-chain","sign",
|
|
281
371
|
"verify-sign","key-protect-on","key-protect-off"])
|
|
@@ -288,8 +378,14 @@ def main():
|
|
|
288
378
|
parser.add_argument("--pubkey", help="Path to public key file (only required in RSA encrypt mode)")
|
|
289
379
|
parser.add_argument("--keyfile",help="Use a file as the password source for encryption/decryption")
|
|
290
380
|
parser.add_argument("--version",action="version",version=f"ENSEC_CLI {VERSION}")
|
|
381
|
+
parser.add_argument("--ecc", action="store_true",help="Enable ECC protection")
|
|
382
|
+
parser.add_argument("--split",action="store_true",help="Split encrypted file")
|
|
291
383
|
args = parser.parse_args()
|
|
292
|
-
|
|
384
|
+
if args.split:
|
|
385
|
+
global is_split_enabled
|
|
386
|
+
is_split_enabled=True
|
|
387
|
+
if args.ecc:
|
|
388
|
+
ecc=True
|
|
293
389
|
# --- validate RSA/pubkey usage ---
|
|
294
390
|
# --- validate RSA/pubkey usage ---
|
|
295
391
|
if args.rsa:
|
|
@@ -327,7 +423,7 @@ def main():
|
|
|
327
423
|
sys.exit(1)
|
|
328
424
|
|
|
329
425
|
# 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")):
|
|
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")):
|
|
331
427
|
print(f"❌ Error: The file for decryption must have a '.vdec' or '.rdec' extension.")
|
|
332
428
|
sys.exit(1)
|
|
333
429
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from reedsolo import RSCodec, ReedSolomonError
|
|
2
|
+
import lzma
|
|
3
|
+
ECC_BYTES = 32 # 32byteのECC。最大で約16byte分の破損を訂正可能
|
|
4
|
+
rs = RSCodec(ECC_BYTES)
|
|
5
|
+
|
|
6
|
+
def add_ecc(data: bytes) -> bytes:
|
|
7
|
+
"""暗号化済みデータにECCを付与"""
|
|
8
|
+
return bytes(rs.encode(data))
|
|
9
|
+
|
|
10
|
+
def recover_ecc(encoded: bytes) -> bytes:
|
|
11
|
+
"""ECC付きデータを復元"""
|
|
12
|
+
try:
|
|
13
|
+
decoded = rs.decode(encoded)
|
|
14
|
+
return bytes(decoded[0]) # 元データ
|
|
15
|
+
except ReedSolomonError as e:
|
|
16
|
+
raise ValueError("ECCで復元できないほど破損しています") from e
|
|
17
|
+
|
|
18
|
+
def recover_data(data):
|
|
19
|
+
try:
|
|
20
|
+
compressed_data = recover_ecc(data)
|
|
21
|
+
except ValueError as e:
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
data = lzma.decompress(compressed_data)
|
|
26
|
+
except lzma.LZMAError:
|
|
27
|
+
print("エラーLZMA圧縮データの展開に失敗しました")
|
|
28
|
+
return None
|
|
29
|
+
return data
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ensec-cli
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: EncryptSecureDEC Open Source CLI
|
|
5
5
|
Requires-Python: >=3.9
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
7
|
License-File: LICENSE
|
|
8
8
|
Requires-Dist: pycryptodome
|
|
9
9
|
Requires-Dist: cryptography
|
|
10
|
+
Requires-Dist: reedsolo
|
|
10
11
|
Dynamic: license-file
|
|
11
12
|
|
|
12
13
|
# ENSEC-CLI
|
|
@@ -4,14 +4,15 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ensec-cli"
|
|
7
|
-
version = "1.0
|
|
7
|
+
version = "2.1.0"
|
|
8
8
|
description = "EncryptSecureDEC Open Source CLI"
|
|
9
9
|
requires-python = ">=3.9"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
|
|
12
12
|
dependencies = [
|
|
13
13
|
"pycryptodome",
|
|
14
|
-
"cryptography"
|
|
14
|
+
"cryptography",
|
|
15
|
+
"reedsolo"
|
|
15
16
|
]
|
|
16
17
|
[tool.setuptools]
|
|
17
18
|
packages = ["ensec_cli"]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.0.11"
|
|
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
|