Coverage for src/couchers/crypto.py: 73%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import functools
2import secrets
3from base64 import urlsafe_b64decode, urlsafe_b64encode
4from typing import Optional
6import nacl.pwhash
7from nacl.bindings import crypto_aead
8from nacl.bindings.crypto_generichash import generichash_blake2b_salt_personal
9from nacl.bindings.utils import sodium_memcmp
10from nacl.exceptions import InvalidkeyError
11from nacl.utils import random as random_bytes
13from couchers.config import config
16def b64encode(data: bytes) -> str:
17 return urlsafe_b64encode(data).decode("ascii")
20def b64decode(data: str) -> bytes:
21 return urlsafe_b64decode(data)
24def urlsafe_random_bytes(length=32) -> str:
25 return b64encode(random_bytes(length))
28def urlsafe_secure_token():
29 """
30 A cryptographically secure random token that can be put in a URL
31 """
32 return urlsafe_random_bytes(32)
35def cookiesafe_secure_token():
36 return random_hex(32)
39def hash_password(password: str):
40 return nacl.pwhash.str(password.encode("utf-8"))
43def verify_password(hashed: bytes, password: str):
44 try:
45 correct = nacl.pwhash.verify(hashed, password.encode("utf-8"))
46 return correct
47 except InvalidkeyError:
48 return False
51def random_hex(length=32):
52 """
53 Length in binary
54 """
55 return random_bytes(length).hex()
58def secure_compare(val1, val2):
59 return sodium_memcmp(val1, val2)
62def generate_hash_signature(message: bytes, key: bytes) -> bytes:
63 """
64 Computes a blake2b keyed hash for the message.
66 This can be used as a fast yet secure symmetric signature: by checking that
67 the hashes agree, we can make sure the signature was generated by a party
68 with knowledge of the key.
69 """
70 return generichash_blake2b_salt_personal(message, key=key, digest_size=32)
73def verify_hash_signature(message: bytes, key: bytes, sig: bytes) -> bool:
74 """
75 Verifies a hash signature generated with generate_hash_signature.
77 Returns true if the signature matches, otherwise false.
78 """
79 return secure_compare(sig, generate_hash_signature(message, key))
82def generate_random_5digit_string():
83 """Return a random 5-digit string"""
84 return "%05d" % secrets.randbelow(100000)
87def verify_token(a: str, b: str):
88 """Return True if strings a and b are equal, in such a way as to
89 reduce the risk of timing attacks.
90 """
91 return secrets.compare_digest(a, b)
94@functools.lru_cache
95def get_secret(name: str):
96 """
97 Derives a secret key from the root secret using a key derivation function
98 """
99 return generate_hash_signature(name.encode("utf8"), config["SECRET"])
102UNSUBSCRIBE_KEY_NAME = "unsubscribe"
103PAGE_TOKEN_KEY_NAME = "pagination"
106# AEAD: Authenticated Encryption with Associated Data
108_aead_key_len = crypto_aead.crypto_aead_xchacha20poly1305_ietf_KEYBYTES
109_aead_nonce_len = crypto_aead.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES
112def aead_generate_nonce():
113 return random_bytes(_aead_nonce_len)
116def aead_generate_key():
117 return random_bytes(_aead_key_len)
120def aead_encrypt(key: bytes, secret_data: bytes, plaintext_data: bytes = b"", nonce: Optional[bytes] = None) -> bytes:
121 if not nonce:
122 nonce = aead_generate_nonce()
123 encrypted = crypto_aead.crypto_aead_xchacha20poly1305_ietf_encrypt(secret_data, plaintext_data, nonce, key)
124 return nonce, encrypted
127def aead_decrypt(key: bytes, nonce: bytes, encrypted_secret_data: bytes, plaintext_data: bytes = b"") -> bytes:
128 return crypto_aead.crypto_aead_xchacha20poly1305_ietf_decrypt(encrypted_secret_data, plaintext_data, nonce, key)
131def simple_encrypt(key_name: str, data: bytes) -> bytes:
132 key = get_secret(key_name)
133 nonce, data = aead_encrypt(key, data)
134 return nonce + data
137def simple_decrypt(key_name: str, data: bytes) -> bytes:
138 key = get_secret(key_name)
139 nonce, data = data[:_aead_nonce_len], data[_aead_nonce_len:]
140 return aead_decrypt(key, nonce, data)
143def encrypt_page_token(plaintext_page_token: str):
144 return b64encode(simple_encrypt(PAGE_TOKEN_KEY_NAME, plaintext_page_token.encode("utf8")))
147def decrypt_page_token(encrypted_page_token: str):
148 return simple_decrypt(PAGE_TOKEN_KEY_NAME, b64decode(encrypted_page_token)).decode("utf8")