ghostbit-cli 1.4.0__tar.gz → 1.5.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.
- {ghostbit_cli-1.4.0 → ghostbit_cli-1.5.0}/PKG-INFO +1 -1
- {ghostbit_cli-1.4.0 → ghostbit_cli-1.5.0}/__init__.py +13 -2
- {ghostbit_cli-1.4.0 → ghostbit_cli-1.5.0}/_crypto.py +47 -0
- {ghostbit_cli-1.4.0 → ghostbit_cli-1.5.0}/ghostbit_cli.egg-info/PKG-INFO +1 -1
- {ghostbit_cli-1.4.0 → ghostbit_cli-1.5.0}/pyproject.toml +1 -1
- {ghostbit_cli-1.4.0 → ghostbit_cli-1.5.0}/README.md +0 -0
- {ghostbit_cli-1.4.0 → ghostbit_cli-1.5.0}/_api.py +0 -0
- {ghostbit_cli-1.4.0 → ghostbit_cli-1.5.0}/_completion.py +0 -0
- {ghostbit_cli-1.4.0 → ghostbit_cli-1.5.0}/_config.py +0 -0
- {ghostbit_cli-1.4.0 → ghostbit_cli-1.5.0}/_history.py +0 -0
- {ghostbit_cli-1.4.0 → ghostbit_cli-1.5.0}/ghostbit_cli.egg-info/SOURCES.txt +0 -0
- {ghostbit_cli-1.4.0 → ghostbit_cli-1.5.0}/ghostbit_cli.egg-info/dependency_links.txt +0 -0
- {ghostbit_cli-1.4.0 → ghostbit_cli-1.5.0}/ghostbit_cli.egg-info/entry_points.txt +0 -0
- {ghostbit_cli-1.4.0 → ghostbit_cli-1.5.0}/ghostbit_cli.egg-info/requires.txt +0 -0
- {ghostbit_cli-1.4.0 → ghostbit_cli-1.5.0}/ghostbit_cli.egg-info/top_level.txt +0 -0
- {ghostbit_cli-1.4.0 → ghostbit_cli-1.5.0}/setup.cfg +0 -0
- {ghostbit_cli-1.4.0 → ghostbit_cli-1.5.0}/setup.py +0 -0
|
@@ -29,6 +29,7 @@ from ._crypto import (
|
|
|
29
29
|
decrypt,
|
|
30
30
|
decrypt_bytes,
|
|
31
31
|
derive_key,
|
|
32
|
+
derive_key_for,
|
|
32
33
|
encrypt,
|
|
33
34
|
gen_key,
|
|
34
35
|
gen_salt,
|
|
@@ -110,10 +111,12 @@ def cmd_paste(args) -> None:
|
|
|
110
111
|
|
|
111
112
|
if password:
|
|
112
113
|
kdf_salt = gen_salt()
|
|
113
|
-
|
|
114
|
+
kdf_choice = args.kdf
|
|
115
|
+
key = derive_key_for(kdf_choice, password, kdf_salt)
|
|
114
116
|
else:
|
|
115
117
|
key = gen_key()
|
|
116
118
|
kdf_salt = None
|
|
119
|
+
kdf_choice = "pbkdf2-sha256" # ignored when has_password is false
|
|
117
120
|
|
|
118
121
|
if args.compress:
|
|
119
122
|
import gzip
|
|
@@ -132,6 +135,7 @@ def cmd_paste(args) -> None:
|
|
|
132
135
|
"burn": args.burn,
|
|
133
136
|
"max_views": args.max_views,
|
|
134
137
|
"compressed": args.compress,
|
|
138
|
+
"kdf": kdf_choice,
|
|
135
139
|
}
|
|
136
140
|
|
|
137
141
|
result = api_create(server, payload)
|
|
@@ -270,7 +274,10 @@ def cmd_view(args) -> None:
|
|
|
270
274
|
if not kdf_salt:
|
|
271
275
|
print("Error: no KDF salt — paste is not password-protected.", file=sys.stderr)
|
|
272
276
|
sys.exit(1)
|
|
273
|
-
|
|
277
|
+
# Server tells us which KDF was used; default to legacy PBKDF2 for
|
|
278
|
+
# pastes written before the field existed.
|
|
279
|
+
kdf_choice = data.get("kdf") or "pbkdf2-sha256"
|
|
280
|
+
key = derive_key_for(kdf_choice, password, kdf_salt)
|
|
274
281
|
else:
|
|
275
282
|
# Restore base64 padding stripped for URL safety.
|
|
276
283
|
padded = key_b64url + "=" * (-len(key_b64url) % 4)
|
|
@@ -518,6 +525,10 @@ current server: {current_server}
|
|
|
518
525
|
"--compress", "-z", action="store_true",
|
|
519
526
|
help="Gzip the plaintext locally BEFORE encryption (60–80%% smaller for text).",
|
|
520
527
|
) # fmt: skip
|
|
528
|
+
parser.add_argument(
|
|
529
|
+
"--kdf", choices=("pbkdf2-sha256", "argon2id"), default="pbkdf2-sha256",
|
|
530
|
+
help="Key derivation function (used only with --password). Default: pbkdf2-sha256.",
|
|
531
|
+
) # fmt: skip
|
|
521
532
|
parser.add_argument(
|
|
522
533
|
"--max-views", "-m", type=int, default=None, metavar="N",
|
|
523
534
|
help="Delete after N views.",
|
|
@@ -21,8 +21,24 @@ try:
|
|
|
21
21
|
except ImportError:
|
|
22
22
|
_CRYPTO_OK = False
|
|
23
23
|
|
|
24
|
+
try:
|
|
25
|
+
from argon2.low_level import Type as _Argon2Type
|
|
26
|
+
from argon2.low_level import hash_secret_raw as _argon2_hash
|
|
27
|
+
|
|
28
|
+
_ARGON2_OK = True
|
|
29
|
+
except ImportError:
|
|
30
|
+
_ARGON2_OK = False
|
|
31
|
+
|
|
24
32
|
_PBKDF2_ITERATIONS = 600_000
|
|
25
33
|
|
|
34
|
+
# OWASP 2023+ minimum for Argon2id in interactive contexts (browser/CLI).
|
|
35
|
+
# m = memory in KiB; t = passes; p = parallelism; hash length in bytes.
|
|
36
|
+
# Anything stronger meaningfully impacts page-load time on a phone — bump
|
|
37
|
+
# these together with the browser WASM lib's defaults when it lands.
|
|
38
|
+
_ARGON2_MEMORY_KIB = 19_456 # 19 MiB
|
|
39
|
+
_ARGON2_TIME_COST = 2
|
|
40
|
+
_ARGON2_PARALLELISM = 1
|
|
41
|
+
|
|
26
42
|
|
|
27
43
|
def require_crypto() -> None:
|
|
28
44
|
"""Abort with an actionable message if `cryptography` is missing."""
|
|
@@ -62,6 +78,7 @@ def decrypt(ciphertext_b64: str, nonce_b64: str, key: bytes) -> str:
|
|
|
62
78
|
|
|
63
79
|
|
|
64
80
|
def derive_key(password: str, salt_b64: str) -> bytes:
|
|
81
|
+
"""PBKDF2-SHA256 at 600k iterations — the historical default."""
|
|
65
82
|
salt = base64.b64decode(salt_b64)
|
|
66
83
|
kdf = PBKDF2HMAC(
|
|
67
84
|
algorithm=_hashes.SHA256(), length=32, salt=salt, iterations=_PBKDF2_ITERATIONS
|
|
@@ -69,5 +86,35 @@ def derive_key(password: str, salt_b64: str) -> bytes:
|
|
|
69
86
|
return kdf.derive(password.encode())
|
|
70
87
|
|
|
71
88
|
|
|
89
|
+
def derive_key_argon2id(password: str, salt_b64: str) -> bytes:
|
|
90
|
+
"""Argon2id with OWASP minimum params (m=19 MiB, t=2, p=1)."""
|
|
91
|
+
if not _ARGON2_OK:
|
|
92
|
+
print(
|
|
93
|
+
"Error: 'argon2-cffi' package required for --kdf argon2id. "
|
|
94
|
+
"Run: pip install argon2-cffi",
|
|
95
|
+
file=sys.stderr,
|
|
96
|
+
)
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
salt = base64.b64decode(salt_b64)
|
|
99
|
+
return _argon2_hash(
|
|
100
|
+
secret=password.encode(),
|
|
101
|
+
salt=salt,
|
|
102
|
+
time_cost=_ARGON2_TIME_COST,
|
|
103
|
+
memory_cost=_ARGON2_MEMORY_KIB,
|
|
104
|
+
parallelism=_ARGON2_PARALLELISM,
|
|
105
|
+
hash_len=32,
|
|
106
|
+
type=_Argon2Type.ID,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def derive_key_for(kdf: str, password: str, salt_b64: str) -> bytes:
|
|
111
|
+
"""Dispatch to the right KDF based on the protocol's kdf field."""
|
|
112
|
+
if kdf == "pbkdf2-sha256":
|
|
113
|
+
return derive_key(password, salt_b64)
|
|
114
|
+
if kdf == "argon2id":
|
|
115
|
+
return derive_key_argon2id(password, salt_b64)
|
|
116
|
+
raise ValueError(f"unsupported kdf {kdf!r}")
|
|
117
|
+
|
|
118
|
+
|
|
72
119
|
def key_to_fragment(key: bytes) -> str:
|
|
73
120
|
return base64.urlsafe_b64encode(key).rstrip(b"=").decode()
|
|
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
|