Skip to content

Commit 6e7f100

Browse files
committed
Harden PQC key serialization
1 parent 4d0682f commit 6e7f100

1 file changed

Lines changed: 42 additions & 36 deletions

File tree

Lines changed: 42 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,74 @@
11
from __future__ import annotations
22

33
import base64
4+
import binascii
5+
import os
6+
import tempfile
47
from pathlib import Path
58

69

710
def bytes_to_base64(data: bytes) -> str:
8-
"""
9-
Encode bytes to a Base64 string.
10-
"""
11-
return base64.b64encode(data).decode("utf-8")
11+
"""Encode bytes to a Base64 string."""
12+
return base64.b64encode(data).decode("ascii")
1213

1314

1415
def base64_to_bytes(data: str) -> bytes:
15-
"""
16-
Decode a Base64 string to bytes.
17-
"""
18-
return base64.b64decode(data.encode("utf-8"))
16+
"""Strictly decode a Base64 string to bytes."""
17+
try:
18+
return base64.b64decode(data.encode("ascii"), validate=True)
19+
except (UnicodeEncodeError, binascii.Error) as exc:
20+
raise ValueError("Invalid Base64 data") from exc
1921

2022

21-
def save_binary_file(path: str | Path, data: bytes) -> None:
22-
"""
23-
Save raw binary data to disk.
24-
"""
23+
def _atomic_write(path: str | Path, data: bytes) -> None:
2524
file_path = Path(path)
2625
file_path.parent.mkdir(parents=True, exist_ok=True)
27-
file_path.write_bytes(data)
26+
descriptor, temporary_name = tempfile.mkstemp(
27+
prefix=f".{file_path.name}.",
28+
dir=file_path.parent,
29+
)
30+
temporary_path = Path(temporary_name)
31+
try:
32+
os.fchmod(descriptor, 0o600)
33+
with os.fdopen(descriptor, "wb") as stream:
34+
stream.write(data)
35+
stream.flush()
36+
os.fsync(stream.fileno())
37+
os.replace(temporary_path, file_path)
38+
try:
39+
file_path.chmod(0o600)
40+
except OSError:
41+
pass
42+
except Exception:
43+
try:
44+
os.close(descriptor)
45+
except OSError:
46+
pass
47+
temporary_path.unlink(missing_ok=True)
48+
raise
49+
50+
51+
def save_binary_file(path: str | Path, data: bytes) -> None:
52+
"""Atomically save binary key material with owner-only permissions."""
53+
_atomic_write(path, data)
2854

2955

3056
def load_binary_file(path: str | Path) -> bytes:
31-
"""
32-
Load raw binary data from disk.
33-
"""
3457
return Path(path).read_bytes()
3558

3659

3760
def save_base64_file(path: str | Path, data: bytes) -> None:
38-
"""
39-
Save binary data as Base64 text.
40-
"""
41-
file_path = Path(path)
42-
file_path.parent.mkdir(parents=True, exist_ok=True)
43-
file_path.write_text(
44-
bytes_to_base64(data),
45-
encoding="utf-8",
46-
)
61+
"""Atomically save Base64 key material with owner-only permissions."""
62+
_atomic_write(path, bytes_to_base64(data).encode("ascii"))
4763

4864

4965
def load_base64_file(path: str | Path) -> bytes:
50-
"""
51-
Load Base64 text and decode it to bytes.
52-
"""
53-
data = Path(path).read_text(encoding="utf-8")
54-
return base64_to_bytes(data)
66+
return base64_to_bytes(Path(path).read_text(encoding="ascii").strip())
5567

5668

5769
def serialize_pqc_key_to_json_value(data: bytes) -> str:
58-
"""
59-
Serialize a PQC key to a JSON-compatible Base64 string.
60-
"""
6170
return bytes_to_base64(data)
6271

6372

6473
def deserialize_pqc_key_from_json_value(data: str) -> bytes:
65-
"""
66-
Deserialize a PQC key from a JSON-compatible Base64 string.
67-
"""
6874
return base64_to_bytes(data)

0 commit comments

Comments
 (0)