diff --git a/covert/idstore.py b/covert/idstore.py index 24dbf6a..0b23877 100644 --- a/covert/idstore.py +++ b/covert/idstore.py @@ -1,5 +1,6 @@ import mmap import os +import time from contextlib import suppress from copy import copy from pathlib import Path @@ -51,6 +52,8 @@ def update(pwhash, allow_create=True, new_pwhash=None): # Yield the ID store for operations but do an update even on break/return etc with suppress(GeneratorExit): yield a.index["I"] + # Remove expired records + remove_expired(a.index["I"]) # Reset archive for re-use in encryption a.reset() a.fds = [BytesIO(f.data) for f in a.flist] @@ -158,3 +161,22 @@ def idkeys(pwhash): k = pubkey.Key(comment=key, pk=value["i"]) if k not in keys: keys[k] = k return keys + + +def remove_expired(ids: dict) -> None: + """Delete all records that have expired.""" + t = time.time() + for k in list(ids): + v = ids[k] + # The entire peer + if "e" in v and v["e"] < t: + del ids[k] + continue + if "r" in v: + r = v["r"] + # The entire ratchet + if r["e"] < t: + del v["r"] + continue + # Message keys + r["msg"] = [m for m in r['msg'] if m["e"] > t] diff --git a/covert/ratchet.py b/covert/ratchet.py index c89412b..32fc85d 100644 --- a/covert/ratchet.py +++ b/covert/ratchet.py @@ -1,6 +1,6 @@ import itertools from contextlib import suppress -from time import time +import time import nacl.bindings as sodium from nacl.exceptions import CryptoError @@ -11,10 +11,10 @@ MAXSKIP = 20 def expire_soon(): - return int(time()) + 600 # 10 minutes + return int(time.time()) + 600 # 10 minutes def expire_later(): - return int(time()) + 86400 * 28 # four weeks + return int(time.time()) + 86400 * 28 # four weeks def chainstep(chainkey: bytes, addn=b""): """Perform a chaining step, returns (new chainkey, message key).""" diff --git a/tests/test_ratchet.py b/tests/test_ratchet.py index 2513e2f..9508faf 100644 --- a/tests/test_ratchet.py +++ b/tests/test_ratchet.py @@ -1,8 +1,10 @@ +from copy import deepcopy from secrets import token_bytes import pytest from nacl.exceptions import CryptoError +from covert.idstore import remove_expired from covert.pubkey import Key from covert.ratchet import Ratchet @@ -95,3 +97,43 @@ def test_ratchet_lost_messages(): # Fail to decode own message with pytest.raises(CryptoError): b.receive(header1) + + +def test_expiration(mocker): + soon = 600 + later = 86400 * 28 + mocker.patch("time.time", return_value=1e9) + r = Ratchet() + assert r.e == 1_000_000_000 + later + + r.init_bob(bytes(32), Key(), Key()) + r.readmsg() + assert r.msg[0]["e"] == 1_000_000_000 + soon + + ids = { + "id:alice": {"I": bytes(32)}, + "id:alice:bob": { + "i": bytes(32), + "e": 2_000_000_000, + "r": r.store(), + }, + } + + ids2 = deepcopy(ids) + remove_expired(ids2) + assert ids == ids2 + + mocker.patch("time.time", return_value=1e9 + soon + 1) + ids2 = deepcopy(ids) + remove_expired(ids2) + assert not ids2["id:alice:bob"]["r"]["msg"] + + mocker.patch("time.time", return_value=1e9 + later + 1) + ids2 = deepcopy(ids) + remove_expired(ids2) + assert not "r" in ids2["id:alice:bob"] + + mocker.patch("time.time", return_value=2e9 + 1) + ids2 = deepcopy(ids) + remove_expired(ids2) + assert not "id:alice:bob" in ids2