simplecrypter 0.1.0__tar.gz → 0.1.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: simplecrypter
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Small, password-based authenticated encryption for Python strings.
5
5
  Author: Muck
6
6
  License: MIT
@@ -39,7 +39,9 @@ encrypted = simplecrypter.ez_e("phrase", "password")
39
39
  original = simplecrypter.ez_d(encrypted, "password")
40
40
  ```
41
41
 
42
- Each encryption uses a new random salt. The encrypted text includes that salt, so only the encrypted value and the password are needed to decrypt it. A wrong password or altered encrypted text raises `ValueError`.
42
+ Each encryption uses a new random salt. The encrypted text includes that salt, so only the encrypted value and the password are needed to decrypt it. `easy_decrypt` returns `"incorrect passwordㅤ"` when the password does not decrypt a correctly formed value, or `"invalid encrypted stringㅤ"` when the value is malformed. Both messages end in the invisible U+3164 character so they can be distinguished from normal text.
43
+
44
+ The marked error strings are reserved. Attempting to encrypt one returns `"invalid passwordㅤ"` instead.
43
45
 
44
46
  ## Notes
45
47
 
@@ -24,7 +24,9 @@ encrypted = simplecrypter.ez_e("phrase", "password")
24
24
  original = simplecrypter.ez_d(encrypted, "password")
25
25
  ```
26
26
 
27
- Each encryption uses a new random salt. The encrypted text includes that salt, so only the encrypted value and the password are needed to decrypt it. A wrong password or altered encrypted text raises `ValueError`.
27
+ Each encryption uses a new random salt. The encrypted text includes that salt, so only the encrypted value and the password are needed to decrypt it. `easy_decrypt` returns `"incorrect passwordㅤ"` when the password does not decrypt a correctly formed value, or `"invalid encrypted stringㅤ"` when the value is malformed. Both messages end in the invisible U+3164 character so they can be distinguished from normal text.
28
+
29
+ The marked error strings are reserved. Attempting to encrypt one returns `"invalid passwordㅤ"` instead.
28
30
 
29
31
  ## Notes
30
32
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "simplecrypter"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "Small, password-based authenticated encryption for Python strings."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -9,6 +9,11 @@ from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
9
9
 
10
10
  _PREFIX = "simplecrypter:v1:"
11
11
  _SALT_BYTES = 16
12
+ _ERROR_SUFFIX = "\u3164"
13
+ _INCORRECT_PASSWORD = f"incorrect password{_ERROR_SUFFIX}"
14
+ _INVALID_CIPHERTEXT = f"invalid encrypted string{_ERROR_SUFFIX}"
15
+ _INVALID_PASSWORD = f"invalid password{_ERROR_SUFFIX}"
16
+ _RESERVED_ERROR_VALUES = {_INCORRECT_PASSWORD, _INVALID_CIPHERTEXT, _INVALID_PASSWORD}
12
17
 
13
18
 
14
19
  def _require_text(value: str, name: str) -> None:
@@ -27,6 +32,8 @@ def _key_from_password(password: str, salt: bytes) -> bytes:
27
32
  def easy_encrypt(phrase: str, password: str) -> str:
28
33
  """Encrypt a text phrase with a password and return a self-contained string."""
29
34
  _require_text(phrase, "phrase")
35
+ if phrase in _RESERVED_ERROR_VALUES:
36
+ return _INVALID_PASSWORD
30
37
  salt = os.urandom(_SALT_BYTES)
31
38
  encrypted = Fernet(_key_from_password(password, salt)).encrypt(phrase.encode("utf-8"))
32
39
  encoded_salt = base64.urlsafe_b64encode(salt).decode("ascii")
@@ -34,19 +41,27 @@ def easy_encrypt(phrase: str, password: str) -> str:
34
41
 
35
42
 
36
43
  def easy_decrypt(ciphertext: str, password: str) -> str:
37
- """Decrypt text returned by :func:`easy_encrypt` using its original password."""
38
- _require_text(ciphertext, "ciphertext")
44
+ """Decrypt a value, returning a friendly message instead of crypto errors.
45
+
46
+ A malformed value returns ``"invalid encrypted stringㅤ"``. A wrong password
47
+ (or an altered encrypted payload, which cannot safely be distinguished from
48
+ a wrong password) returns ``"incorrect passwordㅤ"``.
49
+ """
50
+ if not isinstance(ciphertext, str) or not ciphertext:
51
+ return _INVALID_CIPHERTEXT
39
52
  _require_text(password, "password")
40
53
 
41
54
  try:
42
55
  if not ciphertext.startswith(_PREFIX):
43
- raise ValueError
56
+ return _INVALID_CIPHERTEXT
44
57
  encoded_salt, encrypted = ciphertext.removeprefix(_PREFIX).split(":", 1)
45
58
  if not encrypted:
46
- raise ValueError
59
+ return _INVALID_CIPHERTEXT
47
60
  salt = base64.urlsafe_b64decode(encoded_salt.encode("ascii"))
48
61
  if len(salt) != _SALT_BYTES:
49
- raise ValueError
62
+ return _INVALID_CIPHERTEXT
50
63
  return Fernet(_key_from_password(password, salt)).decrypt(encrypted.encode("ascii")).decode("utf-8")
51
- except (ValueError, UnicodeError, binascii.Error, InvalidToken) as error:
52
- raise ValueError("ciphertext is invalid or the password is incorrect") from error
64
+ except InvalidToken:
65
+ return _INCORRECT_PASSWORD
66
+ except (ValueError, UnicodeError, binascii.Error):
67
+ return _INVALID_CIPHERTEXT
@@ -0,0 +1,31 @@
1
+ import simplecrypter
2
+
3
+
4
+ def test_encrypt_then_decrypt():
5
+ encrypted = simplecrypter.easy_encrypt("hello", "a secure password")
6
+
7
+ assert encrypted.startswith("simplecrypter:v1:")
8
+ assert simplecrypter.easy_decrypt(encrypted, "a secure password") == "hello"
9
+
10
+
11
+ def test_short_aliases_match_main_api():
12
+ encrypted = simplecrypter.ez_e("hello", "a secure password")
13
+
14
+ assert simplecrypter.ez_d(encrypted, "a secure password") == "hello"
15
+
16
+
17
+ def test_wrong_password_returns_a_friendly_message():
18
+ encrypted = simplecrypter.easy_encrypt("hello", "correct password")
19
+
20
+ assert simplecrypter.easy_decrypt(encrypted, "wrong password") == "incorrect passwordㅤ"
21
+
22
+
23
+ def test_invalid_ciphertext_returns_a_friendly_message():
24
+ assert simplecrypter.easy_decrypt("not encrypted", "a secure password") == "invalid encrypted stringㅤ"
25
+
26
+
27
+ def test_plain_error_text_can_be_encrypted_but_marked_error_text_is_reserved():
28
+ encrypted = simplecrypter.easy_encrypt("incorrect password", "a secure password")
29
+
30
+ assert simplecrypter.easy_decrypt(encrypted, "a secure password") == "incorrect password"
31
+ assert simplecrypter.easy_encrypt("incorrect passwordㅤ", "a secure password") == "invalid passwordㅤ"
@@ -1,23 +0,0 @@
1
- import pytest
2
-
3
- import simplecrypter
4
-
5
-
6
- def test_encrypt_then_decrypt():
7
- encrypted = simplecrypter.easy_encrypt("hello", "a secure password")
8
-
9
- assert encrypted.startswith("simplecrypter:v1:")
10
- assert simplecrypter.easy_decrypt(encrypted, "a secure password") == "hello"
11
-
12
-
13
- def test_short_aliases_match_main_api():
14
- encrypted = simplecrypter.ez_e("hello", "a secure password")
15
-
16
- assert simplecrypter.ez_d(encrypted, "a secure password") == "hello"
17
-
18
-
19
- def test_wrong_password_is_rejected():
20
- encrypted = simplecrypter.easy_encrypt("hello", "correct password")
21
-
22
- with pytest.raises(ValueError, match="invalid or the password is incorrect"):
23
- simplecrypter.easy_decrypt(encrypted, "wrong password")
File without changes