Skip to content

Commit

Permalink
Time-based expiration of ID store keys (#89)
Browse files Browse the repository at this point in the history
- Allows actual Forward Secrecy, as keys of messages read or skipped now expire after 10 minutes.
- Due to lacking agent implementation, expiration only occurs when ID store is used by running Covert.
  • Loading branch information
covert-encryption authored Mar 8, 2022
1 parent 7a712f5 commit e9ef845
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 3 deletions.
22 changes: 22 additions & 0 deletions covert/idstore.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import mmap
import os
import time
from contextlib import suppress
from copy import copy
from pathlib import Path
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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]
6 changes: 3 additions & 3 deletions covert/ratchet.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)."""
Expand Down
42 changes: 42 additions & 0 deletions tests/test_ratchet.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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

0 comments on commit e9ef845

Please sign in to comment.