-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathprofiles.py
More file actions
131 lines (107 loc) · 4.03 KB
/
profiles.py
File metadata and controls
131 lines (107 loc) · 4.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
"""In-memory Ed25519 profiles loaded from the vault."""
from __future__ import annotations
import base64
import uuid
from dataclasses import dataclass
from datetime import UTC, datetime
from typing import Any
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from modulr_keymaster.vault_crypto import VaultCryptoError
@dataclass
class ProfileSecrets:
"""Holds private key material only in RAM after unlock."""
id: str
display_name: str
created_at: str
private_key: Ed25519PrivateKey
def public_key_hex(self) -> str:
raw = self.private_key.public_key().public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
return raw.hex()
def to_public_dict(self) -> dict[str, str]:
return {
"id": self.id,
"display_name": self.display_name,
"created_at": self.created_at,
"public_key_hex": self.public_key_hex(),
}
def _seed_b64_to_key(seed_b64: str) -> Ed25519PrivateKey:
try:
seed = base64.standard_b64decode(seed_b64)
except (ValueError, TypeError) as e:
raise VaultCryptoError("invalid profile seed encoding") from e
if len(seed) != 32:
raise VaultCryptoError("invalid Ed25519 seed length")
return Ed25519PrivateKey.from_private_bytes(seed)
def _key_to_seed_b64(key: Ed25519PrivateKey) -> str:
raw = key.private_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PrivateFormat.Raw,
encryption_algorithm=serialization.NoEncryption(),
)
return base64.standard_b64encode(raw).decode("ascii")
def inner_payload_to_profiles(inner: dict[str, Any]) -> list[ProfileSecrets]:
profiles_raw = inner.get("profiles")
if profiles_raw is None:
raise VaultCryptoError("missing profiles array")
if not isinstance(profiles_raw, list):
raise VaultCryptoError("profiles must be a list")
out: list[ProfileSecrets] = []
for item in profiles_raw:
if not isinstance(item, dict):
raise VaultCryptoError("invalid profile entry")
pid = item.get("id")
name = item.get("display_name")
created = item.get("created_at")
seed_b64 = item.get("ed25519_seed_b64")
if not isinstance(pid, str) or not isinstance(name, str):
raise VaultCryptoError("invalid profile fields")
if not isinstance(created, str):
raise VaultCryptoError("invalid profile created_at")
if not isinstance(seed_b64, str):
raise VaultCryptoError("invalid profile seed")
key = _seed_b64_to_key(seed_b64)
out.append(
ProfileSecrets(
id=pid,
display_name=name,
created_at=created,
private_key=key,
),
)
return out
def profiles_to_inner_payload(profiles: list[ProfileSecrets]) -> dict[str, Any]:
rows: list[dict[str, str]] = []
for p in profiles:
rows.append(
{
"id": p.id,
"display_name": p.display_name,
"created_at": p.created_at,
"ed25519_seed_b64": _key_to_seed_b64(p.private_key),
},
)
return {"profiles": rows}
def empty_inner_payload() -> dict[str, Any]:
return {"profiles": []}
DISPLAY_NAME_MAX_LEN = 80
def new_profile(display_name: str) -> ProfileSecrets:
"""Generate a new random Ed25519 identity."""
name = (display_name or "").strip()
if not name:
raise ValueError("display name is required")
if len(name) > DISPLAY_NAME_MAX_LEN:
raise ValueError(
f"display name must be at most {DISPLAY_NAME_MAX_LEN} characters",
)
key = Ed25519PrivateKey.generate()
now = datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")
return ProfileSecrets(
id=str(uuid.uuid4()),
display_name=name,
created_at=now,
private_key=key,
)