diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py index b6f9a931e07588d20a92801501e88570d39c5723..7a29f671bb0262c1552ddf3b80d33c26782603b6 100644 --- a/src/_zkapauthorizer/model.py +++ b/src/_zkapauthorizer/model.py @@ -17,14 +17,17 @@ This module implements models (in the MVC sense) for the client side of the storage plugin. """ +from base64 import b64decode from datetime import datetime from functools import wraps from json import loads from sqlite3 import OperationalError from sqlite3 import connect as _connect +from typing import List import attr from aniso8601 import parse_datetime +from challenge_bypass_ristretto import UnblindedToken as _UnderlyingUnblindedToken from twisted.logger import Logger from twisted.python.filepath import FilePath from zope.interface import Interface, implementer @@ -439,12 +442,63 @@ class VoucherStore(object): for token in unblinded_tokens ), ) - # Clean up the no-longer-needed random tokens. + self._delete_corresponding_tokens(cursor, voucher, unblinded_tokens) + + def _delete_corresponding_tokens(self, cursor, voucher : bytes, unblinded_tokens : List["UnblindedToken"]) -> None: + """ + Delete rows from the [tokens] table corresponding to the given unblinded + tokens. + """ + # The only way to match tokens with unblinded tokens is to compare the + # preimages they each contain. Unfortunately this means we have to + # load all of the tokens from the database. Hopefully this will never + # be a truly huge number because we clean up the table as we make + # progress on redemption. + def token_preimage(token_b64 : str) -> bytes: + # challenge-bypass-ristretto-ffi does not expose a preimage + # accessor for tokens. :( We will try to get it to do so. + # Meanwhile... + token_bytes = b64decode(token_b64) + preimage_bytes = token_bytes[:64] + return preimage_bytes + + def unblinded_token_preimage(unblinded_token : UnblindedToken) -> bytes: + # UnblindedToken exposes a preimage accessor but we have the wrong + # kind... + unblinded_token_obj = _UnderlyingUnblindedToken.decode_base64(unblinded_token.unblinded_token) + preimage_obj = unblinded_token_obj.preimage() + preimage_b64 = preimage_obj.encode_base64() + preimage_bytes = b64decode(preimage_b64) + return preimage_bytes + + # Get the preimages for the unblinded tokens in an easily-querable + # structure. + preimages = set(map(unblinded_token_preimage, unblinded_tokens)) + # Load tokens from the database for the comparison. We can also limit + # this search to tokens related to the specific voucher that we used + # for redemption. + cursor.execute( + "SELECT [text] FROM [tokens] WHERE [voucher] = ?", + (voucher.decode("ascii"),), + ) + tokens_to_delete = [] + for rows in iter(cursor.fetchmany, []): + for (token,) in rows: + preimage = token_preimage(token) + if preimage in preimages: + # This token has a preimage that matches the preimage of + # one of the unblinded tokens. This means this is a token + # which was signed. This means we can drop this token + # now. Create the tuple now since we'll need it to + # execute the SQL below. + tokens_to_delete.append((token,)) + + # Now delete them. cursor.executemany( """ - DELETE FROM [tokens] WHERE [voucher] = ? AND [redemption-group] = ? + DELETE FROM [tokens] WHERE [text] = ? """, - (voucher, group_id), + tokens_to_delete, ) @with_cursor @@ -503,7 +557,7 @@ class VoucherStore(object): of tokens available to be spent. In this case, all tokens remain available to future calls and do not need to be reset. - :return list[UnblindedTokens]: The removed unblinded tokens. + :return list[UnblindedToken]: The removed unblinded tokens. """ if count > _SQLITE3_INTEGER_MAX: # An unreasonable number of tokens and also large enough to