forked from 39bit/spoilerobot
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathdatabase.py
145 lines (117 loc) · 3.6 KB
/
database.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
import base64
import dataclasses
import json
import logging
import asyncpg
from cryptography.exceptions import InvalidSignature
from cryptography.fernet import Fernet
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
import config
from structs import Spoiler
from database_id import UUID
pool: asyncpg.pool.Pool
logger = logging.getLogger('db')
def v1_derive_key(uuid, salt):
"""derives a key from a uuid+unique salt using scrypt"""
return base64.urlsafe_b64encode(
Scrypt(
salt=salt,
length=32,
n=2**10,
r=8,
p=1,
backend=default_backend()
).derive(uuid.encode('ascii'))
)
def v1_hash_uuid(uuid):
"""
hashes a uuid using SHA256 (these are the primary keys of the database)
we can't use a unique salt here because we need the hash to find the row
"""
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
digest.update((uuid + config.HASH_PEPPER).encode())
return digest.finalize()
def v2_hash_uuid(uuid):
"""
Converts a (variable length) uuid into a database key and encryption key by
splitting the SHA512 hash
"""
digest = hashes.Hash(hashes.SHA512(), backend=default_backend())
digest.update((uuid + config.HASH_PEPPER).encode())
digest = digest.finalize()
return digest[:32], base64.urlsafe_b64encode(digest[32:])
async def init():
global pool
logger.info('Creating connection pool...')
pool = await asyncpg.create_pool(
database=config.DB_NAME,
user=config.DB_USERNAME,
host=config.DB_HOST,
password=config.DB_PASSWORD
)
logger.info('Creating tables...')
async with pool.acquire() as con:
await con.execute('''
CREATE TABLE IF NOT EXISTS spoilers (
hash BYTEA PRIMARY KEY,
timestamp INTEGER DEFAULT date_part('epoch', now()),
salt BYTEA,
token BYTEA
)
''')
await con.execute('''
CREATE TABLE IF NOT EXISTS spoilers_v2 (
hash BYTEA PRIMARY KEY,
timestamp INTEGER DEFAULT date_part('epoch', now()),
token BYTEA,
owner BIGINT
)
''')
await pool.execute('VACUUM;')
await pool.execute('TRUNCATE spoilers_v2;')
logger.info('Initialized.')
async def _get_spoiler_v1(uuid: str):
"""
Gets a spoiler from the v1 schema
"""
# try to find uuid by hash in the database
db_hash = v1_hash_uuid(uuid)
data = await pool.fetchrow(
'SELECT salt, token FROM spoilers WHERE hash=$1',
db_hash
)
if not data:
return None
# Decrypt the data and decode it
try:
key = v1_derive_key(uuid, data['salt'])
data = Fernet(key).decrypt(data['token'])
except InvalidSignature:
# this shouldn't happen unless someone messes with the database
return None
return Spoiler(**json.loads(data))
async def get_spoiler(uuid: UUID):
db_hash, key = v2_hash_uuid(uuid.db_key)
data = await pool.fetchval(
'SELECT token FROM spoilers_v2 WHERE hash=$1',
db_hash
)
if not data:
return await _get_spoiler_v1(uuid.db_key)
# Decrypt the data and decode it
try:
data = Fernet(key).decrypt(data)
except InvalidSignature:
# this shouldn't happen unless someone messes with the database
return None
return Spoiler(**json.loads(data))
async def insert_spoiler(uuid: UUID, spoiler: Spoiler, owner_id: int):
data = json.dumps(dataclasses.asdict(spoiler)).encode()
db_hash, key = v2_hash_uuid(uuid.db_key)
token = Fernet(key).encrypt(data)
await pool.execute(
'INSERT INTO spoilers_v2 (hash, token, owner) VALUES ($1, $2, $3)',
db_hash, token, owner_id
)