Merged release/1.0.0-beta into master

This commit is contained in:
Tiara Rodney 2022-04-02 17:46:13 +00:00
commit 45f53223b0
13 changed files with 191 additions and 73 deletions

View file

@ -92,6 +92,20 @@ def command_init_backend(**kwargs):
config.backend.paste.init() config.backend.paste.init()
def command_sanitize_backend(**kwargs):
"""sanitize the backend
"""
from httpaste import load_config
config, _ = load_config(kwargs.get('config'))
config, _ = load_config(kwargs.get('config'))
config.backend.user.sanitize()
config.backend.paste.sanitize()
def parser(): def parser():
p = argparse.ArgumentParser(description='Process some integers.') p = argparse.ArgumentParser(description='Process some integers.')
@ -121,6 +135,11 @@ def parser():
help=command_init_backend.__doc__) help=command_init_backend.__doc__)
p_init_backend.add_argument('--config', '-c', required=True) p_init_backend.add_argument('--config', '-c', required=True)
p_sanitize_backend = sp.add_parser(
'sanitize-backend',
help=command_sanitize_backend.__doc__)
p_sanitize_backend.add_argument('--config', '-c', required=True)
return p return p
@ -136,7 +155,8 @@ def main():
'cgi': command_cgi, 'cgi': command_cgi,
'fcgi': command_fcgi, 'fcgi': command_fcgi,
'default-config': command_default_config, 'default-config': command_default_config,
'init-backend': command_init_backend 'init-backend': command_init_backend,
'sanitize-backend': command_sanitize_backend
}[kwargs.pop('command')](**kwargs) }[kwargs.pop('command')](**kwargs)

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

@ -58,6 +58,13 @@ class User(object):
return user.init(self.path) return user.init(self.path)
def sanitize(self):
if self.path.exists():
return user.sanitize(self.path, self.model_class, self.model_schema)
return None
class Paste(object): class Paste(object):
"""Filesystem paste model backend """Filesystem paste model backend
@ -96,3 +103,10 @@ class Paste(object):
def init(self): def init(self):
return paste.init(self.path) return paste.init(self.path)
def sanitize(self):
if self.path.exists():
return paste.sanitize(self.path, self.model_class, self.model_schema)
return None

View file

@ -5,6 +5,16 @@ acting as cells.
""" """
from pathlib import Path from pathlib import Path
from ast import literal_eval from ast import literal_eval
from time import time
COLUMNS = [
'data',
'data_hash',
'sub',
'expiration',
'encoding'
]
def load( def load(
@ -22,13 +32,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 +60,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 +71,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)
@ -102,6 +99,22 @@ def init(path: Path):
return None return None
def sanitize(path: Path, model_class: type, model_schema: type):
for row in path.iterdir():
expiration_cell = row.joinpath('expiration')
if not expiration_cell.exists():
continue
expiration = literal_eval(expiration_cell.read_text())
if expiration < int(time()) and expiration > 0:
delete(model_class(bytes.fromhex(row.name)), path)
def _rm_tree(pth: Path): def _rm_tree(pth: Path):
for child in pth.iterdir(): for child in pth.iterdir():
if child.is_file(): if child.is_file():

View file

@ -6,6 +6,12 @@ acting as cells.
from pathlib import Path from pathlib import Path
from ast import literal_eval from ast import literal_eval
COLUMNS = [
'sub',
'key_hash',
'index',
]
def load( def load(
proto: object, proto: object,
@ -22,15 +28,17 @@ def load(
return None return None
cells = {} cells = {}
for column in ['key_hash', 'index']: for column in COLUMNS[1:]:
cell = row.joinpath(column) cell = row.joinpath(column)
if getattr(model_schema, column) == bytes: if not cell.exists():
cells[column] = None
elif getattr(model_schema, column) == bytes:
cells[column] = cell.read_bytes() cells[column] = cell.read_bytes()
elif getattr(model_schema, column) == str:
cells[column] = cell.read_text()
else: else:
cells[column] = literal_eval(cell.read_text()) cells[column] = literal_eval(cell.read_text())
return model_class( return model_class(
@ -46,7 +54,7 @@ def dump(model: object, path: Path, model_schema: object) -> None:
row = path.joinpath(model.sub.hex()) row = path.joinpath(model.sub.hex())
row.mkdir(parents=True, exist_ok=True) row.mkdir(parents=True, exist_ok=True)
for column in ['key_hash', 'index']: for column in COLUMNS[1:]:
cell = row.joinpath(column) cell = row.joinpath(column)
@ -74,6 +82,11 @@ def init(path: Path):
return None return None
def sanitize(path: Path, model_class: type, model_schema: type):
return None
def _rm_tree(pth: Path): def _rm_tree(pth: Path):
for child in pth.iterdir(): for child in pth.iterdir():
if child.is_file(): if child.is_file():

View file

@ -45,6 +45,10 @@ class User(object):
return user.init(self.connection) return user.init(self.connection)
def sanitize(self):
return user.sanitize(self.connection, self.model_class)
class Paste(object): class Paste(object):
"""SQLite paste model backend """SQLite paste model backend
@ -74,6 +78,10 @@ class Paste(object):
return paste.init(self.connection) return paste.init(self.connection)
def sanitize(self):
return paste.sanitize(self.connection, self.model_class)
def get_connection(parameters: Parameters): def get_connection(parameters: Parameters):
"""get an sqlite connection object """get an sqlite connection object

View file

@ -2,6 +2,7 @@
""" """
from os import path from os import path
from sqlite3 import Connection from sqlite3 import Connection
from time import time
def load(proto: object, connection: Connection, model_class: type): def load(proto: object, connection: Connection, model_class: type):
@ -11,7 +12,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 +25,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 +38,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()
@ -69,3 +68,14 @@ def init(connection: Connection):
cur.execute(fh.read()) cur.execute(fh.read())
connection.commit() connection.commit()
def sanitize(connection: Connection, model_class: type) -> bool:
cur = connection.cursor()
cur.execute('''SELECT pid FROM pastes WHERE expiration < ? AND expiration > 0''', (int(time()),))
for row in cur.fetchall():
delete(model_class(row['pid']))

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

@ -53,3 +53,8 @@ def init(connection: Connection):
cur.execute(fh.read()) cur.execute(fh.read())
connection.commit() connection.commit()
def sanitize(connection: Connection, model_class) -> bool:
return None

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

@ -1,6 +1,6 @@
"""Model """Model
""" """
from typing import NamedTuple, Optional, Dict, Union from typing import NamedTuple, Optional, Dict, Union, Any, TypedDict
class PasteDataSchema: class PasteDataSchema:
@ -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
""" """
@ -89,9 +98,11 @@ class Sub(UserDataSchema.sub):
""" """
class Index(Dict[PasteId, PasteKey]): class Index(TypedDict):
"""User Paste Index """User Paste Index
""" """
auth_expires: int
pastes: Dict[str, Dict[str, Any]]
class SerializedIndex(UserDataSchema.index): class SerializedIndex(UserDataSchema.index):
@ -128,8 +139,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(
@ -294,4 +300,4 @@ def get_safe(
model = load_safe(Paste(pid, sub), pkey, backend, salt, hmac_iter) model = load_safe(Paste(pid, sub), pkey, backend, salt, hmac_iter)
return PasteData(model.data), model.lifetime, model.encoding return PasteData(model.data), model.expiration, model.encoding

View file

@ -2,6 +2,7 @@
"""user model interface """user model interface
""" """
import json import json
from time import time
from typing import Optional from typing import Optional
from httpaste import Config from httpaste import Config
@ -34,7 +35,7 @@ class IndexError(Exception):
""" """
def load( def _load(
proto: User, proto: User,
master_key: str, master_key: str,
backend: object, backend: object,
@ -54,16 +55,19 @@ def load(
return None return None
try: try:
return User( serialized_data = decrypt(model.index, master_key, salt, hmac_iter)
*model[:-1],
Index(**json.loads(decrypt(model.index, master_key, salt, hmac_iter)))
)
except DecryptionError as e: except DecryptionError as e:
raise IndexError('unable to decrypt user index') from e raise IndexError('unable to decrypt user index') from e
else:
data = json.loads(serialized_data)
return User(
*model[:-1],
Index(**data)
)
def dump( def _dump(
model: User, model: User,
key: MasterKey, key: MasterKey,
backend: object, backend: object,
@ -77,7 +81,7 @@ def dump(
:param salt: randomization salt :param salt: randomization salt
""" """
if not isinstance(model.index, Index): if model.index is not None and not isinstance(model.index, dict):
raise BaseException('index serialization pre-processing not allowed.') raise BaseException('index serialization pre-processing not allowed.')
@ -102,11 +106,13 @@ def load_paste_key(
:param salt: randomization salt :param salt: randomization salt
""" """
for k, v in load(User(sub), key, backend, salt, hmac_iter).index.items(): model = _load(User(sub), key, backend, salt, hmac_iter)
for k, v in model.index.get('pastes').items():
if bytes.fromhex(k) == pid: if bytes.fromhex(k) == pid:
return PasteKey(bytes.fromhex(v)) return PasteKey(bytes.fromhex(v.get('key')))
return None return None
@ -128,12 +134,13 @@ def dump_paste_key(
:param backend: user model backend :param backend: user model backend
""" """
model = load(User(sub), key, backend, salt, hmac_iter) model = _load(User(sub), key, backend, salt, hmac_iter)
dump(User(*model[:-1], Index({ model.index.setdefault('pastes', {})[pid.hex()] = {
**model.index, 'key': pkey.hex()
**{pid.hex(): pkey.hex()} }
})), key, backend, salt, hmac_iter)
_dump(model, key, backend, salt, hmac_iter)
def authenticate( def authenticate(
@ -154,22 +161,36 @@ def authenticate(
proto = User(sub) proto = User(sub)
bogus_decline_msg = 'unable to authenticate'
try: try:
model = load(proto, key, backend, salt, hmac_iter) model = _load(proto, key, backend, salt, hmac_iter)
except IndexError as e: except IndexError as e:
raise AuthenticationError('you dun goofed') raise AuthenticationError(bogus_decline_msg) from e
if not model: if not model:
model = User(sub, key_hash, Index({})) data = {
dump(model, key, backend, salt, hmac_iter) 'auth_expires': int(time()) + (1 * 60)
}
model = User(sub, key_hash, Index(data))
_dump(model, key, backend, salt, hmac_iter)
else: else:
if model.key_hash != key_hash: if model.key_hash != key_hash:
raise AuthenticationError('you dun goofed') raise AuthenticationError(bogus_decline_msg)
return { return {
'sub': sub, 'sub': sub,
'master_key': key 'master_key': key
} }
__all__ = [
AuthenticationError,
load_paste_key,
dump_paste_key,
authenticate
]