Skip to content

Commit fdba260

Browse files
authored
Merge pull request #3 from minvws/iv-generator
Added IV generator for counters
2 parents aed2424 + 6bf5275 commit fdba260

8 files changed

Lines changed: 122 additions & 8 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
__pycache__
22
.venv
33
ssl/*
4+
secrets/*

app.conf

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,9 @@ path=secrets/keystore.json
4646
[hsm_keystore]
4747
library = /usr/lib/softhsm/libsofthsm2.so
4848
slot = 1588871416
49-
slot_pin = 1234
49+
slot_pin = 1234
50+
51+
[iv]
52+
path = secrets/iv.json
53+
block_size = 100
54+
start_value = 9223372036854775807

app.conf.example

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ ssl_key_file = server.key
2121
key_name = REK
2222
key_version = 1
2323
alg = AES-256-GCM
24-
max_encryptions = 9223372036854775807
2524
key_renewal_at = 1000
2625
iv_prefix = RIVF
2726
; How old may a RID be before it is considered invalid for exchanging to a PDN/BSN via VAD?
@@ -40,6 +39,14 @@ host = localhost
4039
port = 6379
4140
db = 0
4241

42+
[iv]
43+
; Path to IV counter
44+
path = secrets/iv.json
45+
; Block size to reserve. If the IV counter is below this value, a new block will be reserved and written to disks
46+
block_size = 100
47+
; Start value for the IV counter
48+
start_value = 9223372036854775807
49+
4350
[hsm_keystore]
4451
library = /usr/local/lib/softhsm/libsofthsm2.so
4552
slot = 0

app/config.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ class ConfigRid(BaseModel):
2626
key_name : str= Field(default="REK")
2727
key_version : str= Field(default=1)
2828
alg : str= Field(default="AES-256-GCM")
29-
max_encryptions : str= Field(default=2**63 - 1)
3029
key_renewal_at : str= Field(default=1000)
3130
iv_prefix : str = Field(default="RIVF")
3231
max_age_for_pdn_exchange_via_vad : int = Field(default=10)
@@ -41,6 +40,10 @@ class ConfigBpg(BaseModel):
4140
class Config:
4241
extra = "allow"
4342

43+
class ConfigIv(BaseModel):
44+
path: str = Field(default="iv.json")
45+
block_size: int = Field(default=100)
46+
start: int = Field(default=9223372036854775807) # 2^63 - 1
4447

4548
class ConfigUvicorn(BaseModel):
4649
swagger_enabled: bool = Field(default=False)
@@ -79,6 +82,7 @@ class Config(BaseModel):
7982
rid: ConfigRid
8083
bpg: ConfigBpg
8184
redis: ConfigRedis
85+
iv: ConfigIv
8286
json_keystore: ConfigJsonKeystore
8387
hsm_keystore: ConfigHsmKeystore
8488

app/container.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from app.services.crypto.hsm_crypto_service import HsmCryptoService
99
from app.services.crypto.json_keystore import JsonKeyStorage
1010
from app.services.crypto.memory_crypto_service import MemoryCryptoService
11+
from app.services.iv_service import IvService
1112
from app.services.pdn_service import PdnService
1213
from app.services.rid_cache import RidCache
1314
from app.services.rid_service import RidService
@@ -26,13 +27,18 @@ def container_config(binder: inject.Binder) -> None:
2627
else:
2728
raise ValueError(f"Unknown keystore type {config.app.keystore}")
2829

30+
31+
iv_service = IvService(config.iv.path, config.iv.block_size)
32+
iv_service.set_iv_counter(config.iv.start, if_not_exists=True)
33+
binder.bind(IvService, iv_service)
34+
2935
redis_service = redis.Redis(config.redis.host, config.redis.port, config.redis.db)
3036
binder.bind(redis.Redis, redis_service)
3137

3238
rid_cache = RidCache(redis_service)
3339
binder.bind(RidCache, rid_cache)
3440

35-
rid_service = RidService(config.rid, crypto_service, rid_cache)
41+
rid_service = RidService(config.rid, crypto_service, rid_cache, iv_service)
3642
binder.bind(RidService, rid_service)
3743

3844
bpg_service = BpgService(config.bpg, crypto_service)

app/services/iv_service.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import json
2+
import os
3+
4+
from filelock import FileLock
5+
6+
"""
7+
The IV service is responsible for generating and storing an IV counter. It is used to generate unique IVs for each message that is sent.
8+
This means that the IV counter must return a unique number each time. We do this by "reserving" a block of counters that can be used.
9+
Once we hit the low water mark for that block, we reserve a new block and store the new low water mark. This way, we can never (?) use
10+
a duplicate counter, but we can miss counters in case of a crash. That is not a problem though.
11+
12+
Note we don't case about thread safety here, as we assume that the IV service is only used by one thread at a time.
13+
14+
15+
Counter Low Water Mark
16+
--------------------------
17+
100 95
18+
99 95
19+
98 95
20+
97 95
21+
96 95
22+
23+
Here we hit the low water mark, so we reserve a new block and store the LWM to disk
24+
25+
95 90
26+
94 90
27+
...
28+
"""
29+
30+
class IvError(Exception):
31+
pass
32+
33+
class IvService:
34+
def __init__(self, filename="iv.json", block_size = 100) -> None:
35+
self.filename = filename
36+
self.block_size = block_size
37+
38+
try:
39+
self.remaining = self.low_watermark = self.load_counter()
40+
except IvError:
41+
self.remaining = self.low_watermark = 0
42+
43+
def set_iv_counter(self, counter: int, if_not_exists: bool = True) -> None:
44+
"""
45+
Manually set the IV counter.
46+
"""
47+
if if_not_exists and os.path.exists(self.filename):
48+
return
49+
self.store_counter(counter)
50+
self.low_watermark = self.remaining = counter
51+
52+
def get_iv_counter(self) -> int:
53+
"""
54+
Return the current IV counter
55+
"""
56+
if self.remaining <= self.low_watermark:
57+
self.low_watermark -= self.block_size
58+
self.store_counter(self.low_watermark)
59+
60+
self.remaining -= 1
61+
return self.remaining
62+
63+
def load_counter(self) -> int:
64+
"""
65+
Load counter from disk
66+
"""
67+
try:
68+
lock = FileLock(self.filename + ".lock", timeout=1)
69+
with lock:
70+
with open(self.filename, 'r') as f:
71+
data = json.load(f)
72+
return data['remaining']
73+
except Exception as e:
74+
raise IvError(e)
75+
76+
def store_counter(self, counter: int) -> None:
77+
"""
78+
Store counter to disk
79+
"""
80+
try:
81+
lock = FileLock(self.filename + ".lock", timeout=1)
82+
with lock:
83+
with open(self.filename, 'w') as f:
84+
json.dump({'remaining': counter}, f)
85+
except Exception as e:
86+
raise IvError(e)
87+

app/services/rid_service.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from app.services.crypto.crypto_service import CryptoService
99
from app.services.jwe import ALGORITHMS, JWEVerifyException
1010
from app.services.rid_cache import RidCache
11+
from app.services.iv_service import IvService
1112
from app.types import BasePseudonym, Rid
1213

1314
class RidException(Exception):
@@ -43,18 +44,19 @@ class VerificationException(RidException):
4344

4445

4546
class RidService:
46-
def __init__(self, config: ConfigRid, crypto_service: CryptoService, rid_cache: RidCache) -> None:
47+
def __init__(self, config: ConfigRid, crypto_service: CryptoService, rid_cache: RidCache, iv_service: IvService) -> None:
4748
self.crypto_service = crypto_service
4849
self.config = config
4950
self.rid_cache = rid_cache
51+
self.iv_service = iv_service
5052

5153
if self.config.alg == "AES-256-GCM":
5254
self.jwe_alg = ALGORITHMS.A256GCM
5355
else:
5456
raise ValueError("Unsupported algorithm")
5557

5658
self.rid_env_key_id = self.config.key_name + "-" + str(self.config.key_version)
57-
self.rid_enc_remaining = int(self.config.max_encryptions)
59+
self.rid_enc_remaining = self.iv_service.get_iv_counter()
5860

5961
def is_healthy(self) -> bool:
6062
"""
@@ -67,7 +69,7 @@ def generate_iv(self) -> bytes:
6769
Generate an IV for the encryption
6870
:return: IV bytes
6971
"""
70-
self.rid_enc_remaining -= 1
72+
self.rid_enc_remaining = self.iv_service.get_iv_counter()
7173
if self.rid_enc_remaining == 0:
7274
raise EncryptionExhaustedException("No more encryption attempts left for key")
7375

requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,6 @@ pydantic_core~=2.23.3
1616
anyio~=4.4.0
1717
click~=8.1.7
1818
annotated-types~=0.7.0
19-
setuptools~=72.2.0
19+
setuptools~=72.2.0
20+
cryptography~=43.0.1
21+
cffi~=1.17.1

0 commit comments

Comments
 (0)