fix(controller/paste): proxy public and private

This commit is contained in:
Tiara Rodney 2022-04-02 03:03:35 +02:00
parent 0d6c010cce
commit 6b96bf8efb
24 changed files with 679 additions and 584 deletions

View file

@ -1,5 +1,141 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""httpaste - secure pasting over http """PSEUDO-MAN-PAGE
NAME
httpaste - versatile HTTP pastebin
SYNOPSIS
HTTP [POST|PUT|DELETE|GET] {url}paste/[public|private]
{url}ui
DESCRIPTION
This program offers an HTTP interface for storing public and private data
(a.k.a. pastes). Public data can be accessed through an URL, where as
private pastes additionally require HTTP basic authentication. Creation of
authentication credentials happens on the fly, there is no sign-up process.
Public pastes can only be accessed by knowing their paste ids, they are not
listed on any index, since it isn't technically possible (by design).
All pastes are symetrically encrypted with an HMAC derived key using
{hmac_iterations} iterations and SHA-512 hashing, a server-side salt and a
randomly generated password. Public paste's passwords are derived from
their ids. Private paste's passwords are randomly generated and stored
inside a symetrically encrypted personal database, with the encryption key
also being derived through the same HMAC mechanism, where the HTTP basic
authentication credentials act as the master password.
Paste ids, usernames, and any other identifiable attributes are only stored
inside databases as keyed and salted BLAKE2 hashes.
Default Paste Encoding: {paste_default_encoding}
Default Paste Lifetime: {paste_lifetime} minutes
Minimum Paste Lifetime: 1 minute
Maximum Paste Lifetime: {paste_max_lifetime} hours
EXAMPLES
POST Public Paste
$ echo '#My public paste' | curl {url}paste/public \\
-F 'data=<-'
{url}/paste/private/I0ah7fyA
GET Public Paste
$ curl {url}/paste/public/I0ah7fyA
#My public paste
POST Private Paste
$ echo '#My private paste' | curl {url}paste/private \\
-F 'data=<-' \\
-u myusername:mypassword
{url}paste/private/4FtNL75g
GET Private Paste
$ curl {url}paste/private -u myusername:mypassword
#My private paste
POST Paste (with non-default expiration)
$ echo '#My paste expires in 20 minutes' | curl \\
{url}paste/public?lifetime=20 \\
-F 'data=<-' \\
{url}paste/public/xMxEmNi8
POST Paste (with expiration after first read)
$ echo '#My paste expires after first read' | curl \\
{url}paste/public?lifetime=-1 \\
-F 'data=<-' \\
{url}paste/public/4FtNL75g
POST Paste with binary data
$ cat my.pdf | base64 | curl \\
"{url}paste/public?encoding=base64" \\
-F "data=<-"
{url}paste/public/zYWpEzXU
GET Paste (with shell syntax highlighting)
$ curl "{url}paste/public/I0ah7fyA?syntax=json"
#My public paste
GET Paste (with line numbers)
$ curl "{url}paste/public/I0ah7fyA?syntax=shell&linenos=true"
0001: #My public paste
0002:
GET Paste (with formatted output)
$ curl "{url}paste/public/I0ah7fyA?syntax=shell&format=html"
--e.g. HTML OUTPUT WITH INLINE CSS--
GET Paste (and set HTTP response content type)
$ curl "{url}paste/public/zYWpEzXU?mime=application/pdf"
--e.g. BINARY--
SEE ALSO
Documentation <https://victorykit.bitbucket.org/httpaste>
Sources <https://bitbucket.org/victorykit/httpaste>
Host (HTTPS) <https://httpaste.it>
(HTTP) <http://httpaste.it>
NOTES
THIS PROGRAM IS FREE SOFTWARE.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
""" """
from typing import NamedTuple, Tuple, Any from typing import NamedTuple, Tuple, Any
from string import ascii_uppercase, digits, ascii_letters, punctuation from string import ascii_uppercase, digits, ascii_letters, punctuation
@ -13,23 +149,28 @@ from connexion.resolver import RestyResolver
from httpaste.model import Backend from httpaste.model import Backend
from httpaste.backend import get_backend_map from httpaste.backend import get_backend_map
from httpaste.helper.exception import BadRequestHttpException, ForbiddenHttpException, GoneHttpException, NotFoundHttpException, UnauthorizedHttpException
from httpaste.helper.common import generate_random_string from httpaste.helper.common import generate_random_string
from httpaste.helper.http import (
BadRequestError,
ForbiddenError,
GoneError,
NotFoundError,
UnauthorizedError)
CONFIGPATH_ENVIRON = 'HTTPASTE_CONFIG' CONFIGPATH_ENVIRON = 'HTTPASTE_CONFIG'
def get_sanitized_config_charset(charset:str): def get_sanitized_config_charset(charset: str):
for x in [ "$", "%"]: for x in ["$", "%"]:
charset = charset.replace(x, f'{x}{x}') charset = charset.replace(x, f'{x}{x}')
return charset return charset
class ConfigException(BaseException): class ConfigError(Exception):
"""Config Exception """Config Exception
""" """
@ -37,13 +178,17 @@ class ConfigException(BaseException):
class Config: class Config:
"""httpaste global config """httpaste global config
""" """
salt: bytes = get_sanitized_config_charset(generate_random_string(32, ascii_letters + digits + punctuation)).encode('utf-8') salt: bytes = get_sanitized_config_charset(generate_random_string(
32, ascii_letters + digits + punctuation)).encode('utf-8')
paste_id_size: int = 8 paste_id_size: int = 8
paste_id_charset: str = ascii_letters + digits paste_id_charset: str = ascii_letters + digits
paste_key_size: int = 32 paste_key_size: int = 32
paste_key_charset: str = get_sanitized_config_charset(ascii_letters + digits + punctuation) paste_key_charset: str = get_sanitized_config_charset(
ascii_letters + digits + punctuation)
paste_lifetime: int = 5 paste_lifetime: int = 5
backend: Backend = None backend: Backend = None
hmac_iterations: int = 20000
paste_default_encoding: str = 'utf-8'
class ServerConfig: class ServerConfig:
@ -53,14 +198,17 @@ class ServerConfig:
bind_address = None bind_address = None
def get_config_path(environ:str=CONFIGPATH_ENVIRON): def get_config_path(environ: str = CONFIGPATH_ENVIRON):
"""
"""
try: try:
return os.environ[environ] return os.environ[environ]
except KeyError as e: except KeyError as e:
raise ConfigException('environment variable \'{environ}\' not set.') from e raise ConfigError(
'environment variable \'{environ}\' not set.') from e
def load_config(path: str) -> Tuple[Config, ServerConfig]: def load_config(path: str) -> Tuple[Config, ServerConfig]:
@ -78,8 +226,10 @@ def load_config(path: str) -> Tuple[Config, ServerConfig]:
bcl, bparamcl = backends[btype] bcl, bparamcl = backends[btype]
except KeyError as e: except KeyError as e:
bids = ', '.join(backends.keys()) bids = ', '.join(backends.keys())
msg = f'invalid backend \'{btype}\' in \'{path}\'. must be any of [{bids}]' raise ConfigError(' '.join((
raise ConfigException(msg) from e f'invalid backend \'{btype}\' in \'{path}\'. ',
f'must be any of [{bids}]'
))) from e
config = dict(_config.items('general')) config = dict(_config.items('general'))
server_config = dict(_config.items('server')) server_config = dict(_config.items('server'))
@ -105,6 +255,8 @@ def load_config(path: str) -> Tuple[Config, ServerConfig]:
def default_config() -> str: def default_config() -> str:
"""
"""
config = ConfigParser() config = ConfigParser()
@ -148,26 +300,35 @@ def get_flask_app(
options = {"swagger_ui": server_config.swagger_ui} options = {"swagger_ui": server_config.swagger_ui}
app = FlaskApp( application = FlaskApp(__name__, specification_dir='schema/')
__name__,
specification_dir='schema/')
app.add_api( application.add_api(
'httpaste.openapi.json', 'httpaste.openapi.json',
options=options, options=options,
resolver=RestyResolver('httpaste.controller') resolver=RestyResolver('httpaste.controller')
) )
app.add_error_handler( for err_cls in [
BadRequestHttpException, BadRequestError,
BadRequestHttpException.render) ForbiddenError,
app.add_error_handler(ForbiddenHttpException, ForbiddenHttpException.render) GoneError,
app.add_error_handler(GoneHttpException, GoneHttpException.render) NotFoundError,
app.add_error_handler(NotFoundHttpException, NotFoundHttpException.render) UnauthorizedError
app.add_error_handler(UnauthorizedHttpException, UnauthorizedHttpException.render) ]:
application.add_error_handler(
err_cls, getattr(err_cls, 'render')
)
with app.app.app_context(): with application.app.app_context():
application.app.httpaste = config
app.app.httpaste = config return application
return app
__all__ = [
Config,
ServerConfig,
load_config,
default_config,
get_flask_app
]

View file

@ -4,14 +4,14 @@ import argparse
import os import os
def _this_dir(basename:str)-> str: def _this_dir(basename: str) -> str:
"""build path with script directory name and provided basename """build path with script directory name and provided basename
""" """
return os.path.join(os.path.dirname(__file__), basename) return os.path.join(os.path.dirname(__file__), basename)
def _path_output(path, echo:bool=False) -> str: def _path_output(path, echo: bool = False) -> str:
"""print path content or path """print path content or path
""" """
@ -30,12 +30,14 @@ def command_standalone(**kwargs):
""" """
from httpaste import load_config, get_flask_app from httpaste import load_config, get_flask_app
from gevent.pywsgi import WSGIServer
config, server_config = load_config(kwargs.get('config_path')) config, server_config = load_config(kwargs.get('config_path'))
application = get_flask_app(config, server_config) application = get_flask_app(config, server_config)
application.run(port=8080) http_server = WSGIServer(('', kwargs.get('port')), application)
http_server.serve_forever()
def command_wsgi(**kwargs): def command_wsgi(**kwargs):
@ -109,10 +111,14 @@ def parser():
p_fcgi = sp.add_parser('fcgi', help=command_fcgi.__doc__) p_fcgi = sp.add_parser('fcgi', help=command_fcgi.__doc__)
p_fcgi.add_argument('--echo', '-e', action='store_true') p_fcgi.add_argument('--echo', '-e', action='store_true')
p_default_config = sp.add_parser('default-config', help=command_default_config.__doc__) p_default_config = sp.add_parser(
'default-config',
help=command_default_config.__doc__)
p_default_config.add_argument('--dump', '-d') p_default_config.add_argument('--dump', '-d')
p_init_backend = sp.add_parser('init-backend', help=command_init_backend.__doc__) p_init_backend = sp.add_parser(
'init-backend',
help=command_init_backend.__doc__)
p_init_backend.add_argument('--config', '-c', required=True) p_init_backend.add_argument('--config', '-c', required=True)
return p return p

View file

@ -17,7 +17,7 @@ from .file import Paste as FilePaste
class SQLite(Backend): class SQLite(Backend):
"""SQLite backend interface """SQLite backend interface
""" """
parameter_class = SqliteParameters parameter_class = SqliteParameters
@ -33,7 +33,7 @@ class SQLite(Backend):
class File(Backend): class File(Backend):
"""File backend interface """File backend interface
""" """
parameter_class = FileParameters parameter_class = FileParameters

View file

@ -1,6 +1,6 @@
"""Filesystem backend """Filesystem backend
""" """
import os from os import path
from pathlib import Path from pathlib import Path
from typing import NamedTuple, Optional from typing import NamedTuple, Optional
@ -12,9 +12,12 @@ class Parameters(NamedTuple):
"""Filesystem backend parameters """Filesystem backend parameters
""" """
#: path of base directory
base_dirname: str base_dirname: str
user_dirname:str = 'users' #: basename of users table directory
paste_dirname:str = 'pastes' user_dirname: Optional[str] = 'users'
#: basename of pastes table directory
paste_dirname: Optional[str] = 'pastes'
class User(object): class User(object):
@ -24,22 +27,25 @@ class User(object):
dirname: Path dirname: Path
path: Path path: Path
def __init__(self, parameters: Parameters, model_class:type, model_schema:type): def __init__(
self,
parameters: Parameters,
model_class: type,
model_schema: type):
self.model_class = model_class self.model_class = model_class
self.model_schema = model_schema self.model_schema = model_schema
self.dirname = os.path.join(parameters.base_dirname, parameters.user_dirname) self.dirname = path.join(parameters.base_dirname,
parameters.user_dirname)
self.path = Path(self.dirname) self.path = Path(self.dirname)
def load(self, proto: object): def load(self, proto: object):
return user.load(proto, self.path, self.model_class, self.model_schema) return user.load(proto, self.path, self.model_class, self.model_schema)
def dump(self, model: object): def dump(self, model: object):
return user.dump(model, self.path, self.model_schema) return user.dump(model, self.path, self.model_schema)
@ -60,13 +66,18 @@ class Paste(object):
dirname: str dirname: str
path: Path path: Path
def __init__(self, parameters: Parameters, model_class:type, model_schema:type): def __init__(
self,
parameters: Parameters,
model_class: type,
model_schema: type):
self.model_class = model_class self.model_class = model_class
self.model_schema = model_schema self.model_schema = model_schema
self.dirname = os.path.join(parameters.base_dirname, parameters.paste_dirname) self.dirname = path.join(parameters.base_dirname,
parameters.paste_dirname)
self.path = Path(self.dirname) self.path = Path(self.dirname)
@ -84,4 +95,4 @@ class Paste(object):
def init(self): def init(self):
return paste.init(self.path) return paste.init(self.path)

View file

@ -7,7 +7,11 @@ from pathlib import Path
from ast import literal_eval from ast import literal_eval
def load(proto: object, path: Path, model_class: type, model_schema: type) -> object: def load(
proto: object,
path: Path,
model_class: type,
model_schema: type) -> object:
"""load a paste """load a paste
""" """
@ -18,17 +22,34 @@ def load(proto: object, path: Path, model_class: type, model_schema: type) -> ob
return None return None
cells = {} cells = {}
for column in ['data', 'data_hash', 'sub', 'timestamp', 'lifetime', 'encoding']: for column in [
'data',
'data_hash',
'sub',
'timestamp',
'lifetime',
'encoding']:
cell = row.joinpath(column) cell = row.joinpath(column)
cell_schema = getattr(model_schema, column)
try:
cell_schema = getattr(model_schema, column)
except AttributeError:
raise RuntimeError(
'Schema {model_schema.__name__} has no attribute {column}'
)
if not cell.exists(): if not cell.exists():
cells[column] = None cells[column] = None
elif cell_schema == bytes: elif cell_schema == bytes:
cells[column] = cell.read_bytes() cells[column] = cell.read_bytes()
elif cell_schema == str:
cells[column] = cell.read_text()
else: else:
cells[column] = literal_eval(cell.read_text()) try:
cells[column] = literal_eval(cell.read_text())
except ValueError as e:
raise ValueError(f'error evaluating column [{column}]') from e
return model_class( return model_class(
proto.pid, proto.pid,
@ -47,7 +68,13 @@ 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 ['data', 'data_hash', 'sub', 'timestamp', 'lifetime', 'encoding']: for column in [
'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)
@ -63,7 +90,6 @@ def dump(model: object, path: Path, model_schema: type) -> None:
def delete(proto: object, path: Path) -> bool: def delete(proto: object, path: Path) -> bool:
row = path.joinpath(proto.pid.hex()) row = path.joinpath(proto.pid.hex())
if row.exists(): if row.exists():
@ -82,4 +108,4 @@ def _rm_tree(pth: Path):
child.unlink() child.unlink()
else: else:
rm_tree(child) rm_tree(child)
pth.rmdir() pth.rmdir()

View file

@ -7,7 +7,11 @@ from pathlib import Path
from ast import literal_eval from ast import literal_eval
def load(proto: object, path: Path, model_class: type, model_schema: type) -> object: def load(
proto: object,
path: Path,
model_class: type,
model_schema: type) -> object:
"""load a paste """load a paste
""" """
@ -76,4 +80,4 @@ def _rm_tree(pth: Path):
child.unlink() child.unlink()
else: else:
rm_tree(child) rm_tree(child)
pth.rmdir() pth.rmdir()

View file

@ -11,7 +11,9 @@ class Parameters(NamedTuple):
"""SQLite backend parameters """SQLite backend parameters
""" """
#: local path or URI
path: str path: str
#: a sqlite3.Connection object (does not apply to config)
connection: Optional[object] = None connection: Optional[object] = None
@ -21,7 +23,7 @@ class User(object):
connection: Connection connection: Connection
def __init__(self, parameters: Parameters, model_class:type): def __init__(self, parameters: Parameters, model_class: type):
self.model_class = model_class self.model_class = model_class
@ -43,13 +45,14 @@ class User(object):
return user.init(self.connection) return user.init(self.connection)
class Paste(object): class Paste(object):
"""SQLite paste model backend """SQLite paste model backend
""" """
connection: Connection connection: Connection
def __init__(self, parameters: Parameters, model_class:type): def __init__(self, parameters: Parameters, model_class: type):
self.model_class = model_class self.model_class = model_class

View file

@ -68,4 +68,4 @@ def init(connection: Connection):
cur.execute(fh.read()) cur.execute(fh.read())
connection.commit() connection.commit()

View file

@ -52,4 +52,4 @@ def init(connection: Connection):
cur.execute(fh.read()) cur.execute(fh.read())
connection.commit() connection.commit()

View file

@ -1,90 +1,16 @@
"""NAME
httpaste - secure, easy-to-use, anonymous production pastebin
SYNOPSIS
HTTP [POST/PUT/DELETE/GET] {url}
DESCRIPTION
TODO
EXAMPLES
POST Public Paste
$ echo '#My public paste' | curl {url}paste/public \\
-F 'data=<-'
{url}/paste/private/I0ah7fyA
GET Public Paste
$ curl {url}/paste/public/I0ah7fyA
#My public paste
POST Private Paste
$ echo '#My private paste' | curl {url}paste/private \\
-F 'data=<-' \\
-u myusername:mypassword
{url}paste/private/4FtNL75g
GET Private Paste
$ curl {url}paste/private -u myusername:mypassword
#My private paste
POST Paste (with non-default expiration)
$ echo '#My paste expires in 20 minutes' | curl \\
{url}paste/public?lifetime=20 \\
-F 'data=<-' \\
{url}paste/public/xMxEmNi8
POST Paste (with expiration after first read)
$ echo '#My paste expires after first read' | curl \\
{url}paste/public?lifetime=-1 \\
-F 'data=<-' \\
{url}paste/public/4FtNL75g
GET Paste (with shell syntax highlighting)
$ curl {url}paste/public/I0ah7fyA?syntax=json
#My public paste
GET Paste (with line numbers)
$ curl {url}paste/public/I0ah7fyA?syntax=shell&linenos=true
0001: #My public paste
0002:
GET Paste (with formatted output)
$ curl {url}paste/public/I0ah7fyA?syntax=shell&format=html
--HTML OUTPUT WITH INLINE CSS--
SEE ALSO
https://bitbucket.org/victorykit/httpaste
https://paste.victoryk.it
"""
import connexion import connexion
from flask import current_app
import httpaste
def get(**kwargs): def get(**kwargs):
print(dir(connexion.request)) config = current_app.httpaste
return __doc__.format(url=connexion.request.url), 200
return httpaste.__doc__.format(
url=connexion.request.url,
hmac_iterations=config.hmac_iterations,
paste_lifetime=config.paste_lifetime,
paste_max_lifetime=str(round(config.paste_max_lifetime / 60)),
paste_default_encoding=config.paste_default_encoding
), 200

View file

@ -1,14 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>httpaste</title>
</head>
<body>
<form><dl>
<dt>Data</dt>
<dd><input type="text"></dd>
<dt>Lifetime</dt>
<dd><input type="text"></dd>
</dl></form>
</body>
</html>

View file

@ -0,0 +1,153 @@
from connexion import request
from connexion.lifecycle import ConnexionResponse
from flask import current_app
from httpaste.helper.common import decode, DecodeError, join_url
import httpaste.model.paste as paste_model
import httpaste.model.user as user_model
from httpaste.helper.http import BadRequestError, GoneError, NotFoundError
from httpaste.model import (
PasteKey,
PasteData,
PasteLifetime,
PasteEncoding,
MasterKey,
Sub)
def delete(**kwargs):
"""
"""
pid = httpaste.model.PasteKey(request['id'].encode('utf-8'))
key = httpaste.model.MasterKey(request['token_info'].get('master_key'))
sub = httpaste.model.Sub(request['token_info'].get('sub'))
pkey = httpaste.model.user.load_paste_key(
pid,
sub,
key,
current_app.httpaste.backend.user,
current_app.httpaste.salt)
httpaste.model.paste.remove_safe(
pid,
sub,
pkey,
current_app.httpaste.backend.paste,
current_app.httpaste.salt)
return None, 200
def get(**kwargs):
"""
"""
config = current_app.httpaste
pid = PasteKey(kwargs['id'].encode('utf-8'))
syntax = kwargs.get('syntax')
formatter = kwargs.get('format', 'terminal256')
linenos = kwargs.get('linenos', False)
mime = kwargs.get('mime', 'text/plain')
if kwargs.get('user') is not None:
# authenticated
key = MasterKey(kwargs['token_info'].get('master_key'))
sub = Sub(kwargs['token_info'].get('sub'))
pkey = user_model.load_paste_key(pid, sub, key, config.backend.user,
config.salt, config.hmac_iterations)
def call(): return paste_model.get_safe(pid, pkey, sub,
config.backend.paste,
config.salt, config.hmac_iterations)
else:
# unauthenticated
def call(): return paste_model.get(pid, config.backend.paste,
config.salt, config.hmac_iterations)
try:
data, lifetime, encoding = call()
except paste_model.LifetimeError as e:
if kwargs.get('user') is not None:
paste_model.remove_safe(pid, sub, pkey, config.backend.paste,
config.salt, config.hmac_iterations)
else:
paste_model.remove(pid, config.backend.paste)
raise GoneError(str(e)) from e
except paste_model.NotFoundError as e:
raise NotFoundError(str(e))
except paste_model.SubError as e:
raise ForbiddenError(str(e))
# burn after read
if lifetime < 0:
if kwargs.get('user') is not None:
paste_model.remove_safe(pid, sub, pkey, config.backend.paste,
config.salt, config.hmac_iterations)
else:
paste_model.remove(pid, config.backend.paste)
if syntax is not None:
data = highlight(data, str(syntax), formatter, linenos)
if encoding is not None:
data = data.decode(encoding)
return ConnexionResponse(
status_code=200,
content_type=mime,
body=data
)
def post(**kwargs):
"""
"""
config = current_app.httpaste
if kwargs['body'].get('data') is None:
raise BadRequestError('form field \'data\' missing.')
encoding = PasteEncoding(kwargs.get('encoding', 'utf-8'))
lifetime = PasteLifetime(kwargs.get('lifetime', config.paste_lifetime))
if encoding not in ['utf-8', 'utf-16', 'ascii']:
try:
decoded_data = decode(kwargs['body'].get('data'), encoding)
except DecodeError as e:
raise BadRequestError(str(e))
else:
pdata = PasteData(decoded_data)
encoding = None
else:
pdata = PasteData(kwargs['body']['data'].encode(encoding))
if kwargs.get('user') is not None:
# authenticated
key = MasterKey(kwargs['token_info'].get('master_key'))
sub = Sub(kwargs['token_info'].get('sub'))
pid, pkey = paste_model.create_safe(pdata, lifetime, sub, encoding,
config.backend.paste, config.salt, config.hmac_iterations)
user_model.dump_paste_key(pid, pkey, sub, key, config.backend.user,
config.salt, config.hmac_iterations)
else:
# unauthenticated
pid = paste_model.create(pdata, lifetime, encoding, config.backend.paste,
config.salt, config.hmac_iterations)
base_url = join_url(request.root_url, request.path)
url = '/'.join((base_url, pid.decode('utf-8')))
return '\n'.join((url, '')), 200

View file

@ -1,38 +1,4 @@
from typing import TypedDict, Optional from httpaste.controller import paste
import connexion
from flask import current_app
from httpaste.helper.exception import BadRequestHttpException, GoneHttpException, NotFoundHttpException
import httpaste.model
import httpaste.model.paste
import httpaste.model.user
class Form(TypedDict):
data: str
rsa_public_key: Optional[str]
class TokenInfo(TypedDict):
sub: str
token: str
class Lifetime(int):
"""
"""
class PostRequest(TypedDict):
body: Form
token_info: TokenInfo
lifetime: Lifetime
class GetRequest(TypedDict):
id: str
token_info: TokenInfo
def search(**kwargs): def search(**kwargs):
@ -44,111 +10,15 @@ def search(**kwargs):
return 'Hallo', 200 return 'Hallo', 200
def put(**kwargs):
"""
"""
print(args)
return 'Hallo', 200
def delete(**kwargs):
"""
"""
pid = httpaste.model.PasteKey(request['id'].encode('utf-8'))
key = httpaste.model.MasterKey(request['token_info'].get('master_key'))
sub = httpaste.model.Sub(request['token_info'].get('sub'))
pkey = httpaste.model.user.load_paste_key(
pid,
sub,
key,
current_app.httpaste.backend.user,
current_app.httpaste.salt)
httpaste.model.paste.remove_safe(pid, sub, pkey, current_app.httpaste.backend.paste, current_app.httpaste.salt)
return None, 200
def get(**kwargs): def get(**kwargs):
""" """
""" """
request = GetRequest(**kwargs) return paste.get(**kwargs)
pid = httpaste.model.PasteKey(request['id'].encode('utf-8'))
key = httpaste.model.MasterKey(request['token_info'].get('master_key'))
sub = httpaste.model.Sub(request['token_info'].get('sub'))
pkey = httpaste.model.user.load_paste_key(
pid,
sub,
key,
current_app.httpaste.backend.user,
current_app.httpaste.salt)
try:
data, lifetime, encoding = httpaste.model.paste.get_safe(
pid,
pkey,
sub,
current_app.httpaste.backend.paste,
current_app.httpaste.salt)
except httpaste.model.paste.PasteLifetimeException as e:
httpaste.model.paste.remove_safe(pid, sub, pkey, current_app.httpaste.backend.paste, current_app.httpaste.salt)
raise GoneHttpException(str(e))
except httpaste.model.paste.PasteNotFoundException as e:
raise NotFoundHttpException(str(e))
if lifetime < 0:
httpaste.model.paste.remove_safe(pid, sub, pkey, current_app.httpaste.backend.paste, current_app.httpaste.salt)
return data.decode('utf-8'), 200
def post(**kwargs): def post(**kwargs):
""" """
""" """
request = PostRequest(**kwargs) return paste.post(**kwargs)
if not request['body'].get('data'):
raise BadRequestHttpException('form field \'data\' missing.')
encoding = request.get('encoding', 'utf-8')
if encoding not in ['utf-8', 'utf-16', 'ascii']:
try:
data = httpaste.model.paste.PasteData(decode(request['body'].get('data'), encoding))
encoding = None
except DecodeException as e:
raise BadRequestException(str(e))
else:
data = httpaste.model.paste.PasteData(
request['body']['data'].encode(encoding))
request.setdefault('lifetime', current_app.httpaste.paste_lifetime)
key = httpaste.model.MasterKey(request['token_info'].get('master_key'))
sub = httpaste.model.Sub(request['token_info'].get('sub'))
lifetime = httpaste.model.PasteLifetime(request['lifetime'])
pid, pkey = httpaste.model.paste.create_safe(data, lifetime, sub, encoding,
current_app.httpaste.backend.paste,
current_app.httpaste.salt)
httpaste.model.user.dump_paste_key(pid, pkey, sub, key,
current_app.httpaste.backend.user,
current_app.httpaste.salt)
return '/'.join((connexion.request.url, pid.decode('utf-8'))) + '\n', 200

View file

@ -1,34 +1,4 @@
from typing import TypedDict, Optional from httpaste.controller import paste
import connexion
from connexion.lifecycle import ConnexionResponse
from flask import current_app
from httpaste.helper.exception import BadRequestHttpException, \
GoneHttpException, \
NotFoundHttpException, \
ForbiddenHttpException
import httpaste.model.paste
from httpaste.helper.syntax import highlight
from httpaste.helper.common import decode, DecodeException
class Form(TypedDict):
data: str
class TokenInfo(TypedDict):
sub: str
token: str
class PostRequest(TypedDict):
body: Form
token_info: TokenInfo
class GetRequest(TypedDict):
id: str
token_info: TokenInfo
def search(**kwargs): def search(**kwargs):
@ -40,86 +10,15 @@ def search(**kwargs):
return 'Hallo', 200 return 'Hallo', 200
def get(**kwargs): def get(**kwargs):
""" """
""" """
request = GetRequest(**kwargs) return paste.get(**kwargs)
pid = httpaste.model.PasteKey(request['id'].encode('utf-8'))
syntax = request.get('syntax')
formatter = request.get('format', 'terminal256')
linenos = request.get('linenos', False)
mime = request.get('mime', 'text/plain')
try:
data, lifetime, encoding = httpaste.model.paste.get(pid,
current_app.httpaste.backend.paste,
current_app.httpaste.salt)
except httpaste.model.paste.PasteLifetimeException as e:
httpaste.model.paste.remove(pid, current_app.httpaste.backend.paste)
raise GoneHttpException(str(e)) from e
except httpaste.model.paste.PasteNotFoundException as e:
raise NotFoundHttpException(str(e))
except httpaste.model.paste.PasteSubException as e:
raise ForbiddenHttpException(str(e))
if lifetime < 0:
httpaste.model.paste.remove(pid, current_app.httpaste.backend.paste)
if syntax:
data = highlight(data, str(syntax), formatter, linenos)
if encoding:
data = data.decode(encoding)
return ConnexionResponse(
status_code=200,
content_type=mime,
body=data
)
def post(**kwargs): def post(**kwargs):
""" """
""" """
request = PostRequest(**kwargs) return paste.post(**kwargs)
request.setdefault('lifetime', current_app.httpaste.paste_lifetime)
if not request['body'].get('data'):
raise BadRequestHttpException('form field \'data\' missing.')
encoding = request.get('encoding', 'utf-8')
if encoding not in ['utf-8', 'utf-16', 'ascii']:
try:
data = httpaste.model.PasteData(decode(request['body'].get('data'), encoding))
encoding = None
except DecodeException as e:
raise BadRequestHttpException(str(e))
else:
data = httpaste.model.PasteData(request['body']['data'].encode(encoding))
lifetime = httpaste.model.PasteLifetime(request['lifetime'])
pid = httpaste.model.paste.create(data, lifetime, encoding,
current_app.httpaste.backend.paste,
current_app.httpaste.salt)
return '/'.join((connexion.request.url, pid.decode('utf-8'))) + '\n', 200

View file

View file

@ -0,0 +1,23 @@
"""
"""
from flask import current_app
from httpaste.helper.http import ForbiddenError
from httpaste.model.user import authenticate, AuthenticationError
def post(*args, **kwargs):
"""
"""
config = current_app.httpaste
user_id = args[0].encode('utf-8')
password = args[1].encode('utf-8')
try:
return authenticate(user_id, password, config.backend.user, config.salt, config.hmac_iterations)
except AuthenticationError as e:
raise ForbiddenError('You shall not pass!') from e

View file

@ -1,8 +1,8 @@
from random import choice from random import choice
from base64 import b64decode from base64 import b64decode
from urllib.parse import urljoin
class DecodeError(Exception):
class DecodeException(BaseException):
""" """
""" """
@ -12,12 +12,21 @@ def generate_random_string(length: int, charset: str) -> str:
return ''.join(choice(charset) for _ in range(length)) return ''.join(choice(charset) for _ in range(length))
def decode(data:str, encoding:str) -> bytes: def decode(data: str, encoding: str) -> bytes:
if encoding == 'base64': if encoding == 'base64':
return b64decode(data.encode('ascii')) try:
return b64decode(data.encode('ascii'))
except UnicodeEncodeError as e:
raise DecodeError('unable to decode with base64.') from e
else: else:
raise DecodeException(f'unknown encoding \'{encoding}\'.') raise DecodeError(f'unknown encoding \'{encoding}\'.')
def join_url(base:str, url: str) -> str:
return urljoin(base, url, True)

View file

@ -11,7 +11,10 @@ from cryptography.fernet import Fernet, InvalidToken
from httpaste import Config from httpaste import Config
class DecryptionException(BaseException): DEFAULT_HMAC_ITERATIONS = 20000
class DecryptionError(Exception):
""" """
""" """
@ -35,7 +38,7 @@ def dhash(data: bytes):
return hashlib.sha512(data).digest() return hashlib.sha512(data).digest()
def derive_key(main_key: str, salt: bytes = Config.salt) -> bytes: def derive_key(main_key: str, salt: bytes = Config.salt, iterations:int=DEFAULT_HMAC_ITERATIONS) -> bytes:
"""derive a key from a main key """derive a key from a main key
:param main_key: main key to derive from :param main_key: main key to derive from
@ -46,13 +49,13 @@ def derive_key(main_key: str, salt: bytes = Config.salt) -> bytes:
algorithm=hashes.SHA256(), algorithm=hashes.SHA256(),
length=32, length=32,
salt=salt, salt=salt,
iterations=390000, iterations=iterations,
) )
return base64.urlsafe_b64encode(kdf.derive(main_key)) return base64.urlsafe_b64encode(kdf.derive(main_key))
def encrypt(data: bytes, key: bytes, salt: bytes) -> bytes: def encrypt(data: bytes, key: bytes, salt: bytes, hmac_iterations:int=DEFAULT_HMAC_ITERATIONS) -> bytes:
"""encrypt a data block """encrypt a data block
:param data: data block :param data: data block
@ -60,10 +63,10 @@ def encrypt(data: bytes, key: bytes, salt: bytes) -> bytes:
:param salt: randomization salt :param salt: randomization salt
""" """
return Fernet(derive_key(key, salt)).encrypt(data) return Fernet(derive_key(key, salt, hmac_iterations)).encrypt(data)
def decrypt(data: bytes, key: bytes, salt: bytes): def decrypt(data: bytes, key: bytes, salt: bytes, hmac_iterations:int=DEFAULT_HMAC_ITERATIONS):
"""encrypt a data block """encrypt a data block
:param data: data block :param data: data block
@ -73,8 +76,8 @@ def decrypt(data: bytes, key: bytes, salt: bytes):
try: try:
return Fernet(derive_key(key, salt)).decrypt(data) return Fernet(derive_key(key, salt, hmac_iterations)).decrypt(data)
except InvalidToken as e: except InvalidToken as e:
raise DecryptionException('unable to decrypt') from e raise DecryptionError('unable to decrypt') from e

View file

@ -1,5 +1,5 @@
class BadRequestHttpException(RuntimeError): class BadRequestError(RuntimeError):
def __init__(self, msg=None): def __init__(self, msg=None):
super().__init__(msg) super().__init__(msg)
@ -12,7 +12,7 @@ class BadRequestHttpException(RuntimeError):
}, 400 }, 400
class UnauthorizedHttpException(RuntimeError): class UnauthorizedError(RuntimeError):
def __init__(self, msg=None): def __init__(self, msg=None):
super().__init__(msg) super().__init__(msg)
@ -25,8 +25,7 @@ class UnauthorizedHttpException(RuntimeError):
}, 401 }, 401
class ForbiddenError(RuntimeError):
class ForbiddenHttpException(RuntimeError):
def __init__(self, msg=None): def __init__(self, msg=None):
super().__init__(msg) super().__init__(msg)
@ -39,7 +38,7 @@ class ForbiddenHttpException(RuntimeError):
}, 403 }, 403
class GoneHttpException(RuntimeError): class GoneError(RuntimeError):
def __init__(self, msg=None): def __init__(self, msg=None):
super().__init__(msg) super().__init__(msg)
@ -52,7 +51,7 @@ class GoneHttpException(RuntimeError):
}, 410 }, 410
class NotFoundHttpException(RuntimeError): class NotFoundError(RuntimeError):
def __init__(self, msg=None): def __init__(self, msg=None):
super().__init__(msg) super().__init__(msg)
@ -62,4 +61,4 @@ class NotFoundHttpException(RuntimeError):
"detail": str(error), "detail": str(error),
"status": 404, "status": 404,
"title": "Not Found", "title": "Not Found",
}, 404 }, 404

View file

@ -2,7 +2,11 @@ from pygments.lexers import get_lexer_by_name, find_lexer_class_by_name
from pygments.formatters import find_formatter_class, HtmlFormatter from pygments.formatters import find_formatter_class, HtmlFormatter
def highlight(data:str, lexer_alias:str, format_alias:str, linenos:bool=False): def highlight(
data: str,
lexer_alias: str,
format_alias: str,
linenos: bool = False):
from pygments import highlight from pygments import highlight
@ -10,7 +14,7 @@ def highlight(data:str, lexer_alias:str, format_alias:str, linenos:bool=False):
formatter = HtmlFormatter(noclasses=True, linenos=linenos) formatter = HtmlFormatter(noclasses=True, linenos=linenos)
else: else:
formatter = find_formatter_class(format_alias)(linenos=linenos) formatter = find_formatter_class(format_alias)(linenos=linenos)
return highlight(data, get_lexer_by_name(lexer_alias), formatter) return highlight(data, get_lexer_by_name(lexer_alias), formatter)

View file

@ -18,9 +18,9 @@ class PasteDataSchema:
class UserDataSchema: class UserDataSchema:
"""User Interface Schema between Model and Backend """User Interface Schema between Model and Backend
""" """
sub= bytes sub = bytes
key_hash= bytes key_hash = bytes
index= bytes index = bytes
class Backend(object): class Backend(object):
@ -132,4 +132,4 @@ class Paste(NamedTuple):
#: paste lifetime #: paste lifetime
lifetime: Optional[PasteLifetime] = None lifetime: Optional[PasteLifetime] = None
#: paste encoding #: paste encoding
encoding: Optional[PasteEncoding] = None encoding: Optional[PasteEncoding] = None

View file

@ -7,32 +7,37 @@ import time
from httpaste import Config from httpaste import Config
from httpaste.helper.crypto import dhash, shash, encrypt, decrypt from httpaste.helper.crypto import dhash, shash, encrypt, decrypt
from httpaste.model import Paste, PasteId, Sub, MasterKey, PasteKey, Salt, \
PasteData, PasteHash, PasteTimestamp, PasteSub, \
PasteLifetime, PasteEncoding
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,
PasteData, PasteHash, PasteTimestamp, PasteSub,
PasteLifetime, PasteEncoding)
class PasteNotFoundException(BaseException): class NotFoundError(Exception):
"""Paste Exception """Paste Exception
""" """
class PasteSubException(BaseException): class SubError(Exception):
"""Paste Sub Exception """Paste Sub Exception
""" """
class PasteChecksumException(BaseException): class ChecksumError(Exception):
"""Paste Checksum Exception """Paste Checksum Exception
""" """
class PasteLifetimeException(BaseException): class LifetimeError(Exception):
"""Paste Lifetime Exception """Paste Lifetime Exception
""" """
class BackendError(Exception):
"""
"""
def generate_paste_id( def generate_paste_id(
length: int = Config.paste_id_size, length: int = Config.paste_id_size,
charset: str = Config.paste_id_charset) -> bytes: charset: str = Config.paste_id_charset) -> bytes:
@ -66,19 +71,26 @@ def load(proto: Paste, backend: object) -> Optional[Paste]:
safe_pid = PasteId(dhash(proto.pid)) safe_pid = PasteId(dhash(proto.pid))
model = backend.load(Paste(safe_pid)) try:
model = backend.load(Paste(safe_pid))
except Exception as e:
raise BackendError(f'{e.__class__.__name__}: {e}') from e
if not model: if not model:
raise PasteNotFoundException('Paste does not exist') raise NotFoundError('Paste does not exist')
if proto.sub and model.sub != shash(proto.sub, model.data_hash, proto.pid) or not proto.sub and model.sub: if proto.sub and model.sub != shash(
proto.sub,
model.data_hash,
proto.pid) or not proto.sub and model.sub:
raise PasteSubException('Paste not owned by user') raise SubError('Paste not owned by user')
if model.lifetime >= 0 and model.timestamp + (60 * model.lifetime) < int(time.time()): if model.lifetime >= 0 and model.timestamp + \
(60 * model.lifetime) < int(time.time()):
raise PasteLifetimeException('Paste expired') raise LifetimeError('Paste expired')
return model return model
@ -87,7 +99,8 @@ def load_safe(
proto: Paste, proto: Paste,
key: PasteKey, key: PasteKey,
backend: object, backend: object,
salt: Salt = Config.salt): salt: Salt = Config.salt,
hmac_iter: int = Config.hmac_iterations):
"""load an encrypted paste model """load an encrypted paste model
:param proto: paste model prototype :param proto: paste model prototype
@ -97,11 +110,11 @@ def load_safe(
model = load(proto, backend) model = load(proto, backend)
data = decrypt(model.data, key, salt) data = decrypt(model.data, key, salt, hmac_iter)
if model.data_hash and dhash(data) != model.data_hash: if model.data_hash and dhash(data) != model.data_hash:
raise PasteChecksumException('Paste data scrambled') raise ChecksumError('Paste data scrambled')
return Paste( return Paste(
proto.pid, proto.pid,
@ -120,7 +133,10 @@ def dump(model: Paste, backend: object) -> None:
:param backend: model backend object :param backend: model backend object
""" """
backend.dump(model) try:
backend.dump(model)
except Exception as e:
raise BackendError(str(e)) from e
def delete(proto: Paste, backend: object) -> None: def delete(proto: Paste, backend: object) -> None:
@ -129,21 +145,29 @@ def delete(proto: Paste, backend: object) -> None:
try: try:
model = load(proto, backend) model = load(proto, backend)
except PasteLifetimeException: except LifetimeError:
pass pass
safe_pid = PasteId(dhash(proto.pid)) safe_pid = PasteId(dhash(proto.pid))
backend.delete(Paste(safe_pid)) try:
backend.delete(Paste(safe_pid))
except Exception as e:
raise BackendError(str(e)) from e
def delete_safe(proto: Paste, key:PasteKey, backend: object,salt: Salt = Config.salt) -> None: def delete_safe(
proto: Paste,
key: PasteKey,
backend: object,
salt: Salt = Config.salt,
hmac_iter: int = Config.hmac_iterations) -> None:
""" """
""" """
try: try:
model = load_safe(proto, key, backend, salt) model = load_safe(proto, key, backend, salt, hmac_iter)
except PasteLifetimeException: except LifetimeError:
pass pass
safe_pid = PasteId(dhash(proto.pid)) safe_pid = PasteId(dhash(proto.pid))
@ -156,7 +180,8 @@ def create(
lifetime: PasteLifetime, lifetime: PasteLifetime,
encoding: PasteEncoding, encoding: PasteEncoding,
backend: object, backend: object,
salt: Salt = Config.salt) -> PasteId: salt: Salt = Config.salt,
hmac_iter: int = Config.hmac_iterations) -> PasteId:
"""create an unencrypted paste """create an unencrypted paste
:param data: paste data :param data: paste data
@ -170,9 +195,16 @@ def create(
sub = None sub = None
timestamp = PasteTimestamp(int(time.time())) timestamp = PasteTimestamp(int(time.time()))
safe_data = PasteData(encrypt(data, pid, salt)) safe_data = PasteData(encrypt(data, pid, salt, hmac_iter))
model = Paste(safe_pid, sub, safe_data, data_hash, timestamp, lifetime, encoding) model = Paste(
safe_pid,
sub,
safe_data,
data_hash,
timestamp,
lifetime,
encoding)
dump(model, backend) dump(model, backend)
@ -184,8 +216,8 @@ def create_safe(data: PasteData,
sub: Sub, sub: Sub,
encoding: PasteEncoding, encoding: PasteEncoding,
backend: object, backend: object,
salt: Salt = Config.salt) -> Tuple[PasteId, salt: Salt = Config.salt,
PasteKey]: hmac_iter: int = Config.hmac_iterations) -> Tuple[PasteId,PasteKey]:
"""create an encrypted paste """create an encrypted paste
:param data: paste data :param data: paste data
@ -202,7 +234,7 @@ 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()))
safe_data = PasteData(encrypt(data, pkey, salt)) safe_data = PasteData(encrypt(data, pkey, salt, hmac_iter))
dump(Paste( dump(Paste(
safe_pid, safe_pid,
@ -226,20 +258,26 @@ def remove(pid: PasteId, backend: object):
delete(proto, backend) delete(proto, backend)
def remove_safe(pid: PasteId, sub:Sub, key:PasteKey, backend:object,salt: Salt = Config.salt): def remove_safe(
pid: PasteId,
sub: Sub,
key: PasteKey,
backend: object,
salt: Salt = Config.salt,
hmac_iter: int = Config.hmac_iterations):
proto = Paste(pid, sub) proto = Paste(pid, sub)
delete_safe(proto, key, backend, salt) delete_safe(proto, key, backend, salt, hmac_iter)
def get(pid: PasteId, backend: object, salt: Salt = Config.salt) -> PasteData: def get(pid: PasteId, backend: object, salt: Salt = Config.salt, hmac_iter: int = Config.hmac_iterations) -> PasteData:
"""conveniently load an unencrypted paste """conveniently load an unencrypted paste
""" """
model = load(Paste(pid), backend) model = load(Paste(pid), backend)
data = decrypt(model.data, pid, salt) data = decrypt(model.data, pid, salt, hmac_iter)
return PasteData(data), model.lifetime, model.encoding return PasteData(data), model.lifetime, model.encoding
@ -249,10 +287,11 @@ def get_safe(
pkey: PasteKey, pkey: PasteKey,
sub: Sub, sub: Sub,
backend: object, backend: object,
salt: Salt = Config.salt) -> PasteData: salt: Salt = Config.salt,
hmac_iter: int = Config.hmac_iterations) -> PasteData:
"""conveniently load an encrypted paste """conveniently load an encrypted paste
""" """
model = load_safe(Paste(pid, sub), pkey, backend, salt) model = load_safe(Paste(pid, sub), pkey, backend, salt, hmac_iter)
return PasteData(model.data), model.lifetime, model.encoding return PasteData(model.data), model.lifetime, model.encoding

View file

@ -1,27 +1,35 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""user model interface """user model interface
""" """
import hashlib
import sqlite3
import os
import base64
import json import json
from typing import NamedTuple, Dict, Optional, Union from typing import Optional
from flask import g, current_app
from httpaste import Config from httpaste import Config
from httpaste.helper.crypto import dhash, shash, encrypt, decrypt, derive_key, DecryptionException from httpaste.helper.crypto import (
from httpaste.model import User, KeyHash, Index, SerializedIndex, Salt, \ dhash,
PasteKey, PasteId, MasterKey, Sub shash,
encrypt,
decrypt,
derive_key,
DecryptionError)
from httpaste.model import (
User,
KeyHash,
Index,
SerializedIndex,
Salt,
PasteKey,
PasteId,
MasterKey,
Sub)
class AuthenticationError(BaseException): class AuthenticationError(Exception):
"""Authentication Error """Authentication Error
""" """
class IndexError(BaseException): class IndexError(Exception):
"""Index Decryption Error """Index Decryption Error
""" """
@ -30,7 +38,8 @@ def load(
proto: User, proto: User,
master_key: str, master_key: str,
backend: object, backend: object,
salt: Salt = Config.salt) -> Optional[User]: salt: Salt = Config.salt,
hmac_iter: int = Config.hmac_iterations) -> Optional[User]:
"""load user model """load user model
:param model: user model prototype :param model: user model prototype
@ -47,9 +56,9 @@ def load(
try: try:
return User( return User(
*model[:-1], *model[:-1],
Index(**json.loads(decrypt(model.index, master_key, salt))) Index(**json.loads(decrypt(model.index, master_key, salt, hmac_iter)))
) )
except DecryptionException as e: except DecryptionError as e:
raise IndexError('unable to decrypt user index') from e raise IndexError('unable to decrypt user index') from e
@ -58,7 +67,8 @@ def dump(
model: User, model: User,
key: MasterKey, key: MasterKey,
backend: object, backend: object,
salt: Salt = Config.salt) -> None: salt: Salt = Config.salt,
hmac_iter: int = Config.hmac_iterations) -> None:
"""dump a user model """dump a user model
:param model: user model :param model: user model
@ -73,7 +83,7 @@ def dump(
serialized_index = json.dumps(model.index).encode('utf-8') serialized_index = json.dumps(model.index).encode('utf-8')
safe_index = SerializedIndex(encrypt(serialized_index, key, salt)) safe_index = SerializedIndex(encrypt(serialized_index, key, salt, hmac_iter))
backend.dump(User(*model[:-1], safe_index)) backend.dump(User(*model[:-1], safe_index))
@ -82,7 +92,7 @@ def load_paste_key(
pid: PasteId, pid: PasteId,
sub: Sub, sub: Sub,
key: MasterKey, key: MasterKey,
backend: object, salt: Salt = Config.salt) -> Optional[PasteKey]: backend: object, salt: Salt = Config.salt, hmac_iter: int = Config.hmac_iterations) -> Optional[PasteKey]:
"""load a user paste key """load a user paste key
:param pid: paste id :param pid: paste id
@ -92,7 +102,7 @@ def load_paste_key(
:param salt: randomization salt :param salt: randomization salt
""" """
for k, v in load(User(sub), key, backend, salt).index.items(): for k, v in load(User(sub), key, backend, salt, hmac_iter).index.items():
if bytes.fromhex(k) == pid: if bytes.fromhex(k) == pid:
@ -107,7 +117,8 @@ def dump_paste_key(
sub: Sub, sub: Sub,
key: MasterKey, key: MasterKey,
backend: object, backend: object,
salt: str = Config.salt) -> None: salt: str = Config.salt,
hmac_iter: int = Config.hmac_iterations) -> None:
"""dump a user paste key """dump a user paste key
:param pid: paste id :param pid: paste id
@ -117,19 +128,20 @@ def dump_paste_key(
:param backend: user model backend :param backend: user model backend
""" """
model = load(User(sub), key, backend, salt) model = load(User(sub), key, backend, salt, hmac_iter)
dump(User(*model[:-1], Index({ dump(User(*model[:-1], Index({
**model.index, **model.index,
**{pid.hex(): pkey.hex()} **{pid.hex(): pkey.hex()}
})), key, backend, salt) })), key, backend, salt, hmac_iter)
def authenticate( def authenticate(
user_id: bytes, user_id: bytes,
password: bytes, password: bytes,
backend: object, backend: object,
salt: Salt = Config.salt): salt: Salt = Config.salt,
hmac_iter: int = Config.hmac_iterations):
"""authenticate a user """authenticate a user
:param user_id: human-readable user id :param user_id: human-readable user id
@ -137,20 +149,20 @@ def authenticate(
""" """
sub = Sub(dhash(user_id)) sub = Sub(dhash(user_id))
key = MasterKey(derive_key(password, salt)) key = MasterKey(derive_key(password, salt, hmac_iter))
key_hash = KeyHash(dhash(key)) key_hash = KeyHash(dhash(key))
proto = User(sub) proto = User(sub)
try: try:
model = load(proto, key, backend, salt) model = load(proto, key, backend, salt, hmac_iter)
except IndexError as e: except IndexError as e:
raise AuthenticationError('you dun goofed') raise AuthenticationError('you dun goofed')
if not model: if not model:
model = User(sub, key_hash, Index({})) model = User(sub, key_hash, Index({}))
dump(model, key, backend, salt) dump(model, key, backend, salt, hmac_iter)
else: else:
if model.key_hash != key_hash: if model.key_hash != key_hash:
@ -161,18 +173,3 @@ def authenticate(
'sub': sub, 'sub': sub,
'master_key': key 'master_key': key
} }
def _authenticate_connexion(user_id: str, password: str):
from httpaste.helper.exception import ForbiddenHttpException
from flask import current_app
try:
return authenticate(user_id.encode('utf-8'), password.encode('utf-8'),
current_app.httpaste.backend.user,
current_app.httpaste.salt)
except AuthenticationError as e:
raise ForbiddenHttpException('You shall not pass!') from e

View file

@ -32,14 +32,6 @@
}, },
}, },
"/paste/public": { "/paste/public": {
"get": {
"description": "get last 100 public pastes",
"responses": {
"200": {
"description": "",
}
}
},
"post": { "post": {
"description": "create a new public paste", "description": "create a new public paste",
"requestBody": { "requestBody": {
@ -58,11 +50,11 @@
], ],
"responses": { "responses": {
"200": { "200": {
"description": "", "description": "paste location",
"content": { "content": {
"text/plain": { "text/plain": {
"schema": { "schema": {
"type": "string" "$ref": "#/components/schemas/PasteURL"
} }
} }
} }
@ -72,10 +64,17 @@
}, },
"/paste/private": { "/paste/private": {
"get": { "get": {
"description": "get private paste ids", "description": "get private paste locations",
"responses": { "responses": {
"200": { "200": {
"description": "", "description": "paste locations",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/NewLineDelimitedPasteURLs"
}
}
}
} }
} }
}, },
@ -92,15 +91,18 @@
"parameters": [ "parameters": [
{ {
"$ref": "#/components/parameters/lifetime" "$ref": "#/components/parameters/lifetime"
},
{
"$ref": "#/components/parameters/encoding"
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "", "description": "paste location",
"content": { "content": {
"text/plain": { "text/plain": {
"schema": { "schema": {
"type": "string" "$ref": "#/components/schemas/PasteURL"
} }
} }
} }
@ -108,78 +110,6 @@
} }
} }
}, },
"/paste/private/{id}": {
"get": {
"description": "get a private paste",
"security": [
{
"basicAuth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/id"
},
{
"$ref": "#/components/parameters/syntax"
}
],
"responses": {
"200": {
"description": "",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
}
}
}
}
},
"put": {
"description": "update an existing private paste",
"security": [
{
"basicAuth": []
}
],
"requestBody": {
"$ref": "#/components/requestBodies/pastePost"
},
"parameters": [
{
"$ref": "#/components/parameters/id"
},
{
"$ref": "#/components/parameters/lifetime"
}
],
"responses": {
"200": {
"description": "",
}
}
},
"delete": {
"description": "delete an existing private paste",
"security": [
{
"basicAuth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/id"
}
],
"responses": {
"200": {
"description": "",
}
}
}
},
"/paste/public/{id}": { "/paste/public/{id}": {
"get": { "get": {
"description": "get a public paste", "description": "get a public paste",
@ -205,11 +135,41 @@
], ],
"responses": { "responses": {
"200": { "200": {
"description": "", "description": "paste data. content type may vary.",
"content": { "content": {
"text/plain": { "text/plain": {
"schema": { "schema": {
"type": "string" "$ref": "#/components/schemas/PasteData"
}
}
}
}
}
}
},
"/paste/private/{id}": {
"get": {
"description": "get a private paste",
"security": [
{
"basicAuth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/id"
},
{
"$ref": "#/components/parameters/syntax"
}
],
"responses": {
"200": {
"description": "paste data. content type may vary.",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/PasteData"
} }
} }
} }
@ -219,6 +179,20 @@
} }
}, },
"components": { "components": {
"schemas": {
"PasteData": {
"description": "Paste Data",
"type": "string"
},
"PasteURL": {
"description": "URL pointing to paste",
"type": "string"
},
"NewLineDelimitedPasteURLs": {
"description": "new line delimited paste URLs",
"type": "string"
}
},
"requestBodies": { "requestBodies": {
"pastePost": { "pastePost": {
"description": "Optional description in *Markdown*", "description": "Optional description in *Markdown*",
@ -270,6 +244,7 @@
"in": "query", "in": "query",
"required": false, "required": false,
"schema": { "schema": {
"description": "https://pygments.org/docs/lexers/",
"type": "string" "type": "string"
} }
}, },
@ -279,6 +254,7 @@
"in": "query", "in": "query",
"required": false, "required": false,
"schema": { "schema": {
"description": "https://pygments.org/docs/formatters/",
"type": "string" "type": "string"
} }
}, },
@ -292,13 +268,13 @@
} }
}, },
"encoding": { "encoding": {
"description": "syntax highlighting with line numbers", "description": "data encoding",
"name": "encoding", "name": "encoding",
"in": "query", "in": "query",
"required": false, "required": false,
"schema": { "schema": {
"type": "string", "type": "string",
"enum": ["base64"] "enum": ["base64", "utf-8", "utf-16", "ascii"]
} }
}, },
"mime": { "mime": {
@ -315,7 +291,7 @@
"basicAuth": { "basicAuth": {
"type": "http", "type": "http",
"scheme": "basic", "scheme": "basic",
"x-basicInfoFunc": "httpaste.model.user._authenticate_connexion" "x-basicInfoFunc": "httpaste.controller.user.session.post"
} }
} }
} }