fix(model/paste): interpolate expiration

paste model now consists of expiration instead of lifetime and timestamp.
This is to ensure, that man-in-the-middle attackers cannot derive the origin
of a paste through observing transport layer server request times.
This commit is contained in:
Tiara Rodney 2022-04-02 16:50:42 +02:00
parent bcfab0fbad
commit f0c0188d58
7 changed files with 50 additions and 44 deletions

View file

@ -26,7 +26,7 @@ class SQLite(Backend):
def __init__(self, parameters: SqliteParameters): def __init__(self, parameters: SqliteParameters):
parameters['connection'] = get_sqlite_connection(parameters) parameters = SqliteParameters(parameters.path, get_sqlite_connection(parameters))
self.user = SqliteUser(parameters, User) self.user = SqliteUser(parameters, User)
self.paste = SqlitePaste(parameters, Paste) self.paste = SqlitePaste(parameters, Paste)

View file

@ -7,6 +7,15 @@ from pathlib import Path
from ast import literal_eval from ast import literal_eval
COLUMNS = [
'data',
'data_hash',
'sub',
'expiration',
'encoding'
]
def load( def load(
proto: object, proto: object,
path: Path, path: Path,
@ -22,13 +31,7 @@ def load(
return None return None
cells = {} cells = {}
for column in [ for column in COLUMNS:
'data',
'data_hash',
'sub',
'timestamp',
'lifetime',
'encoding']:
cell = row.joinpath(column) cell = row.joinpath(column)
@ -56,8 +59,7 @@ def load(
cells['sub'], cells['sub'],
cells['data'], cells['data'],
cells['data_hash'], cells['data_hash'],
cells['timestamp'], cells['expiration'],
cells['lifetime'],
cells['encoding']) cells['encoding'])
@ -68,13 +70,7 @@ def dump(model: object, path: Path, model_schema: type) -> None:
row = path.joinpath(model.pid.hex()) row = path.joinpath(model.pid.hex())
row.mkdir(parents=True, exist_ok=True) row.mkdir(parents=True, exist_ok=True)
for column in [ for column in COLUMNS:
'data',
'data_hash',
'sub',
'timestamp',
'lifetime',
'encoding']:
cell = row.joinpath(column) cell = row.joinpath(column)
cell_schema = getattr(model_schema, column) cell_schema = getattr(model_schema, column)

View file

@ -11,7 +11,7 @@ def load(proto: object, connection: Connection, model_class: type):
cur = connection.cursor() cur = connection.cursor()
cur.execute( cur.execute(
'SELECT pid, data, data_hash, sub, timestamp, lifetime, encoding FROM pastes WHERE pid=?', 'SELECT pid, data, data_hash, sub, expiration, encoding FROM pastes WHERE pid=?',
(proto.pid, (proto.pid,
)) ))
@ -24,8 +24,7 @@ def load(proto: object, connection: Connection, model_class: type):
result['sub'], result['sub'],
result['data'], result['data'],
result['data_hash'], result['data_hash'],
result['timestamp'], result['expiration'],
result['lifetime'],
result['encoding']) result['encoding'])
return None return None
@ -38,14 +37,13 @@ def dump(model: object, connection: Connection):
cur = connection.cursor() cur = connection.cursor()
cur.execute( cur.execute(
'''INSERT INTO pastes (pid, data, data_hash, sub, timestamp, lifetime, encoding) '''INSERT INTO pastes (pid, data, data_hash, sub, expiration, encoding)
VALUES (?,?,?,?,?,?,?)''', VALUES (?,?,?,?,?,?)''',
(model.pid, (model.pid,
model.data, model.data,
model.data_hash, model.data_hash,
model.sub, model.sub,
model.timestamp, model.expiration,
model.lifetime,
model.encoding)) model.encoding))
connection.commit() connection.commit()

View file

@ -1,10 +1,9 @@
CREATE TABLE IF NOT EXISTS "pastes" ( CREATE TABLE IF NOT EXISTS "pastes" (
"id" BLOB NOT NULL UNIQUE, "pid" BLOB NOT NULL UNIQUE,
"data" BLOB NOT NULL, "data" BLOB NOT NULL,
"data_hash" BLOB NOT NULL, "data_hash" BLOB NOT NULL,
"sub" BLOB UNIQUE, "sub" BLOB UNIQUE,
"timestamp" INTEGER NOT NULL, "expiration" INTEGER NOT NULL,
"lifetime" INTEGER NOT NULL, "encoding" TEXT,
"encoding" TEXT PRIMARY KEY("pid")
PRIMARY KEY("id")
); );

View file

@ -71,7 +71,7 @@ def get(**kwargs):
config.salt, config.hmac_iterations) config.salt, config.hmac_iterations)
try: try:
data, lifetime, encoding = call() data, expiration, encoding = call()
except paste_model.LifetimeError as e: except paste_model.LifetimeError as e:
if kwargs.get('user') is not None: if kwargs.get('user') is not None:
paste_model.remove_safe(pid, sub, pkey, config.backend.paste, paste_model.remove_safe(pid, sub, pkey, config.backend.paste,
@ -85,7 +85,7 @@ def get(**kwargs):
raise ForbiddenError(str(e)) raise ForbiddenError(str(e))
# burn after read # burn after read
if lifetime < 0: if expiration < 0:
if kwargs.get('user') is not None: if kwargs.get('user') is not None:
paste_model.remove_safe(pid, sub, pkey, config.backend.paste, paste_model.remove_safe(pid, sub, pkey, config.backend.paste,
config.salt, config.hmac_iterations) config.salt, config.hmac_iterations)

View file

@ -12,6 +12,7 @@ class PasteDataSchema:
sub = bytes sub = bytes
timestamp = int timestamp = int
lifetime = int lifetime = int
expiration = int
encoding = str encoding = str
@ -54,6 +55,14 @@ class PasteEncoding(PasteDataSchema.encoding):
""" """
class PasteExpiration(PasteDataSchema.expiration):
"""Paste Expiration
< 0: after first acccess
0: never
"""
class PasteLifetime(PasteDataSchema.lifetime): class PasteLifetime(PasteDataSchema.lifetime):
"""Paste Lifetime """Paste Lifetime
""" """
@ -128,8 +137,6 @@ class Paste(NamedTuple):
#: paste data hash #: paste data hash
data_hash: Optional[PasteHash] = None data_hash: Optional[PasteHash] = None
#: paste timestamp #: paste timestamp
timestamp: Optional[PasteTimestamp] = None expiration: Optional[PasteExpiration] = None
#: paste lifetime
lifetime: Optional[PasteLifetime] = None
#: paste encoding #: paste encoding
encoding: Optional[PasteEncoding] = None encoding: Optional[PasteEncoding] = None

View file

@ -10,7 +10,7 @@ from httpaste.helper.crypto import dhash, shash, encrypt, decrypt
from httpaste.helper.common import generate_random_string from httpaste.helper.common import generate_random_string
from httpaste.model import (Paste, PasteId, Sub, MasterKey, PasteKey, Salt, from httpaste.model import (Paste, PasteId, Sub, MasterKey, PasteKey, Salt,
PasteData, PasteHash, PasteTimestamp, PasteSub, PasteData, PasteHash, PasteTimestamp, PasteSub,
PasteLifetime, PasteEncoding) PasteLifetime, PasteEncoding, PasteExpiration)
class NotFoundError(Exception): class NotFoundError(Exception):
@ -87,8 +87,7 @@ def load(proto: Paste, backend: object) -> Optional[Paste]:
raise SubError('Paste not owned by user') raise SubError('Paste not owned by user')
if model.lifetime >= 0 and model.timestamp + \ if model.expiration > 0 and model.expiration < int(time.time()):
(60 * model.lifetime) < int(time.time()):
raise LifetimeError('Paste expired') raise LifetimeError('Paste expired')
@ -121,8 +120,7 @@ def load_safe(
proto.sub, proto.sub,
data, data,
model.data_hash, model.data_hash,
model.timestamp, model.expiration,
model.lifetime,
model.encoding) model.encoding)
@ -195,6 +193,11 @@ def create(
sub = None sub = None
timestamp = PasteTimestamp(int(time.time())) timestamp = PasteTimestamp(int(time.time()))
if lifetime < 0:
expiration = -1
else:
expiration = PasteExpiration(timestamp + (lifetime * 60))
safe_data = PasteData(encrypt(data, pid, salt, hmac_iter)) safe_data = PasteData(encrypt(data, pid, salt, hmac_iter))
model = Paste( model = Paste(
@ -202,8 +205,7 @@ def create(
sub, sub,
safe_data, safe_data,
data_hash, data_hash,
timestamp, expiration,
lifetime,
encoding) encoding)
dump(model, backend) dump(model, backend)
@ -234,6 +236,11 @@ def create_safe(data: PasteData,
safe_sub = PasteSub(shash(sub, data_hash, pid)) safe_sub = PasteSub(shash(sub, data_hash, pid))
timestamp = PasteTimestamp(int(time.time())) timestamp = PasteTimestamp(int(time.time()))
if lifetime < 0:
expiration = -1
else:
expiration = PasteExpiration(timestamp + (lifetime * 60))
safe_data = PasteData(encrypt(data, pkey, salt, hmac_iter)) safe_data = PasteData(encrypt(data, pkey, salt, hmac_iter))
dump(Paste( dump(Paste(
@ -241,8 +248,7 @@ def create_safe(data: PasteData,
safe_sub, safe_sub,
safe_data, safe_data,
data_hash, data_hash,
timestamp, expiration,
lifetime,
encoding encoding
), backend) ), backend)
@ -279,7 +285,7 @@ def get(pid: PasteId, backend: object, salt: Salt = Config.salt, hmac_iter: int
data = decrypt(model.data, pid, salt, hmac_iter) data = decrypt(model.data, pid, salt, hmac_iter)
return PasteData(data), model.lifetime, model.encoding return PasteData(data), model.expiration, model.encoding
def get_safe( def get_safe(