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

71 statements  

1import functools 

2import secrets 

3from base64 import urlsafe_b64decode, urlsafe_b64encode 

4from typing import Optional 

5 

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 

12 

13from couchers.config import config 

14 

15 

16def b64encode(data: bytes) -> str: 

17 return urlsafe_b64encode(data).decode("ascii") 

18 

19 

20def b64decode(data: str) -> bytes: 

21 return urlsafe_b64decode(data) 

22 

23 

24def urlsafe_random_bytes(length=32) -> str: 

25 return b64encode(random_bytes(length)) 

26 

27 

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) 

33 

34 

35def cookiesafe_secure_token(): 

36 return random_hex(32) 

37 

38 

39def hash_password(password: str): 

40 return nacl.pwhash.str(password.encode("utf-8")) 

41 

42 

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 

49 

50 

51def random_hex(length=32): 

52 """ 

53 Length in binary 

54 """ 

55 return random_bytes(length).hex() 

56 

57 

58def secure_compare(val1, val2): 

59 return sodium_memcmp(val1, val2) 

60 

61 

62def generate_hash_signature(message: bytes, key: bytes) -> bytes: 

63 """ 

64 Computes a blake2b keyed hash for the message. 

65 

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) 

71 

72 

73def verify_hash_signature(message: bytes, key: bytes, sig: bytes) -> bool: 

74 """ 

75 Verifies a hash signature generated with generate_hash_signature. 

76 

77 Returns true if the signature matches, otherwise false. 

78 """ 

79 return secure_compare(sig, generate_hash_signature(message, key)) 

80 

81 

82def generate_random_5digit_string(): 

83 """Return a random 5-digit string""" 

84 return "%05d" % secrets.randbelow(100000) 

85 

86 

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) 

92 

93 

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"]) 

100 

101 

102UNSUBSCRIBE_KEY_NAME = "unsubscribe" 

103PAGE_TOKEN_KEY_NAME = "pagination" 

104 

105 

106# AEAD: Authenticated Encryption with Associated Data 

107 

108_aead_key_len = crypto_aead.crypto_aead_xchacha20poly1305_ietf_KEYBYTES 

109_aead_nonce_len = crypto_aead.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES 

110 

111 

112def aead_generate_nonce(): 

113 return random_bytes(_aead_nonce_len) 

114 

115 

116def aead_generate_key(): 

117 return random_bytes(_aead_key_len) 

118 

119 

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 

125 

126 

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) 

129 

130 

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 

135 

136 

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) 

141 

142 

143def encrypt_page_token(plaintext_page_token: str): 

144 return b64encode(simple_encrypt(PAGE_TOKEN_KEY_NAME, plaintext_page_token.encode("utf8"))) 

145 

146 

147def decrypt_page_token(encrypted_page_token: str): 

148 return simple_decrypt(PAGE_TOKEN_KEY_NAME, b64decode(encrypted_page_token)).decode("utf8")