Coverage for nlp_webserver/security.py: 57%

49 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-08-27 10:34 -0500

1r""" 

2crate_anon/nlp_webserver/security.py 

3 

4=============================================================================== 

5 

6 Copyright (C) 2015, University of Cambridge, Department of Psychiatry. 

7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

8 

9 This file is part of CRATE. 

10 

11 CRATE is free software: you can redistribute it and/or modify 

12 it under the terms of the GNU General Public License as published by 

13 the Free Software Foundation, either version 3 of the License, or 

14 (at your option) any later version. 

15 

16 CRATE is distributed in the hope that it will be useful, 

17 but WITHOUT ANY WARRANTY; without even the implied warranty of 

18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

19 GNU General Public License for more details. 

20 

21 You should have received a copy of the GNU General Public License 

22 along with CRATE. If not, see <https://www.gnu.org/licenses/>. 

23 

24=============================================================================== 

25 

26Security functions for CRATE implementation of an NLPRP server. 

27 

28""" 

29 

30import bcrypt 

31import binascii 

32import base64 

33from typing import Optional 

34 

35from cryptography.fernet import Fernet 

36 

37# noinspection PyUnresolvedReferences 

38from paste.httpheaders import AUTHORIZATION 

39from pyramid.request import Request 

40 

41from crate_anon.nlp_webserver.constants import NlpServerConfigKeys 

42from crate_anon.nlp_webserver.settings import SETTINGS 

43 

44 

45def generate_encryption_key() -> None: 

46 """ 

47 Generates a key to be used for reversible encryption of passwords and 

48 prints it to screen. The key should then be put in the config file. 

49 

50 To be called via the command line. 

51 """ 

52 key = Fernet.generate_key() 

53 print(key.decode()) 

54 

55 

56def encrypt_password(password: str) -> bytes: 

57 """ 

58 Encrypts a password using the configured key. 

59 """ 

60 key = SETTINGS[NlpServerConfigKeys.ENCRYPTION_KEY] 

61 # Turn key into bytes object 

62 key = key.encode() 

63 cipher_suite = Fernet(key) 

64 # Turn password into bytes object 

65 password_bytes = password.encode() 

66 return cipher_suite.encrypt(password_bytes) 

67 

68 

69def decrypt_password(encrypted_pw: bytes, cipher_suite: Fernet) -> str: 

70 """ 

71 Decrypts a password using the specified cipher suite. 

72 

73 Args: 

74 encrypted_pw: the encrypted password, as bytes 

75 cipher_suite: a Python object with the method ``decrypt(encrypted_pw)`` 

76 

77 Returns: 

78 the decrypted password as a string 

79 """ 

80 # Get the password as bytes 

81 password_bytes = cipher_suite.decrypt(encrypted_pw) 

82 # Return the password as a string 

83 return password_bytes.decode() 

84 

85 

86def hash_password(pw: str) -> str: 

87 """ 

88 Encrypts a password using bcrypt. 

89 """ 

90 pwhash = bcrypt.hashpw(pw.encode("utf8"), bcrypt.gensalt()) 

91 return pwhash.decode("utf8") 

92 

93 

94def check_password(pw: str, hashed_pw: str) -> bool: 

95 """ 

96 Checks a password against its hash. 

97 

98 Args: 

99 pw: the clear-text password 

100 hashed_pw: the stored hashed version 

101 

102 Returns: 

103 do they match? 

104 

105 """ 

106 expected_hash = hashed_pw.encode("utf8") 

107 return bcrypt.checkpw(pw.encode("utf8"), expected_hash) 

108 

109 

110class Credentials: 

111 """ 

112 Represents username/password credentials sent by the user to us. 

113 """ 

114 

115 def __init__(self, username: str, password: str) -> None: 

116 self.username = username 

117 self.password = password 

118 

119 

120def get_auth_credentials(request: Request) -> Optional[Credentials]: 

121 """ 

122 Gets username and password as a :class:`Credentials` obejct, from an HTTP 

123 request. Returns ``None`` if there is a problem. 

124 """ 

125 authorization = AUTHORIZATION(request.environ) 

126 try: 

127 authmeth, auth = authorization.split(" ", 1) 

128 except ValueError: # not enough values to unpack 

129 return None 

130 if authmeth.lower() == "basic": # ensure rquest is using basicauth 

131 try: 

132 # auth = auth.strip().decode('base64') 

133 auth = base64.b64decode(auth.strip()) 

134 except binascii.Error: # can't decode 

135 return None 

136 # Turn it back into a string 

137 auth = "".join(chr(x) for x in auth) 

138 try: 

139 username, password = auth.split(":", 1) 

140 except ValueError: # not enough values to unpack 

141 return None 

142 return Credentials(username, password) 

143 

144 return None