Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions asknews_sdk/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from urllib.parse import quote, urlencode

from anyio import Path as AsyncPath
from cryptography.fernet import Fernet
from cryptography.fernet import Fernet, InvalidToken
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
Expand Down Expand Up @@ -290,7 +290,10 @@ def _load_token() -> Optional[TokenInfo]:
return None

data = file_path.read_bytes()
salt, token_info = data.split(b"::", 1)
# The salt is raw random bytes and can itself contain the delimiter.
if data[16:18] != b"::":
raise InvalidToken
salt, token_info = data[:16], data[18:]

key = _derive_encryption_key((client_id + client_secret), salt)
serialized_token = _decrypt_with_key(key, token_info)
Expand Down Expand Up @@ -335,7 +338,10 @@ async def _load_token() -> Optional[TokenInfo]:
return None

data = await file_path.read_bytes()
salt, token_info = data.split(b"::", 1)
# The salt is raw random bytes and can itself contain the delimiter.
if data[16:18] != b"::":
raise InvalidToken
salt, token_info = data[:16], data[18:]

key = _derive_encryption_key((client_id + client_secret), salt)
serialized_token = _decrypt_with_key(key, token_info)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ dependencies = [
"httpx>=0.27.2,<0.29.0",
"certifi>=2026.10.19",
"asgiref>=3.7.2",
"cryptography>=40.0.0,<46.0.7",
"cryptography>=40.0.0,<49.0.1",
"crontab>=1.0.1",
"pydantic[email]>=2.10.4",
]
Expand Down
32 changes: 32 additions & 0 deletions tests/test_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -596,3 +596,35 @@ async def test_load_save_token_disk_async(tmp_path):

assert token_info == loaded_token_info


@pytest.mark.parametrize("salt", [b"a" * 16, b"a" * 15 + b":", b"::" + b"a" * 14])
@pytest.mark.parametrize("async_save", [False, True])
@pytest.mark.parametrize("async_load", [False, True])
async def test_token_disk_salt_delimiters(tmp_path, monkeypatch, salt, async_save, async_load):
# Raw salts can contain or end with the delimiter; their length is fixed.
monkeypatch.setattr("asknews_sdk.security.os.urandom", lambda size: salt)
path = tmp_path / "token"
token = {"access_token": "token", "scope": "news", "token_type": "Bearer", "expires_in": 3600}
with pytest.warns(SecurityWarning):
save = (_save_token_disk_async if async_save else _save_token_disk)(path, "id", "secret")
if async_save:
await save(token)
else:
save(token)
assert path.read_bytes()[:18] == salt + b"::"
load = (_load_token_disk_async if async_load else _load_token_disk)(path, "id", "secret")
loaded = await load() if async_load else load()
assert loaded == token


@pytest.mark.parametrize("data", [b"", b"short::token", b"a" * 16 + b"!!token"])
@pytest.mark.parametrize("async_load", [False, True])
async def test_token_disk_rejects_invalid_header(tmp_path, data, async_load):
path = tmp_path / "token"
path.write_bytes(data)
load = (_load_token_disk_async if async_load else _load_token_disk)(path, "id", "secret")
with pytest.raises(InvalidToken):
if async_load:
await load()
else:
load()
Loading
Loading