Coverage for oarepo_c4gh/key/software.py: 100%

36 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-08-07 12:05 +0000

1"""A base class for all software-defined keys. 

2 

3This module implements the Diffie-Hellman key exchange using software 

4keys and NaCl bindings. The class contained here also provides an 

5interface for setting the private key instance property by derived 

6classes that should implement particular key loaders. 

7 

8""" 

9 

10from .key import Key 

11from nacl.public import PrivateKey, PublicKey 

12from nacl.encoding import RawEncoder 

13from nacl.bindings import ( 

14 crypto_kx_server_session_keys, 

15 crypto_kx_client_session_keys, 

16) 

17from ..exceptions import Crypt4GHKeyException 

18import secrets 

19 

20 

21class SoftwareKey(Key): 

22 """This class implements the actual Diffie-Hellman key exchange 

23 with locally stored private key in the class instance. 

24 

25 """ 

26 

27 def __init__(self, key_data: bytes, only_public: bool = False) -> None: 

28 """Performs rudimentary key data validation and initializes 

29 either only the public key or both the public and private key. 

30 

31 Parameters: 

32 key_data: the 32 bytes of key material 

33 only_public: whether this contains only the public point 

34 

35 Raises: 

36 AssertionError: is the key_data does not contain exactly 32 bytes 

37 

38 """ 

39 assert len(key_data) == 32, ( 

40 f"The X25519 key must be 32 bytes long" f" ({len(key_data)})!" 

41 ) 

42 if only_public: 

43 self._public_key = key_data 

44 self._private_key = None 

45 else: 

46 private_key_obj = PrivateKey(key_data) 

47 self._private_key = bytes(private_key_obj) 

48 public_key_obj = private_key_obj.public_key 

49 self._public_key = bytes(public_key_obj) 

50 

51 @property 

52 def public_key(self) -> bytes: 

53 """Returns the public key corresponding to the private key 

54 used. 

55 

56 """ 

57 return self._public_key 

58 

59 def compute_write_key(self, reader_public_key: bytes) -> bytes: 

60 """Computes secret symmetric key used for writing Crypt4GH 

61 encrypted header packets. The instance of this class 

62 represents the writer key. 

63 

64 Parameters: 

65 reader_public_key: the 32 bytes of the reader public key 

66 

67 Returns: 

68 Writer symmetric key as 32 bytes. 

69 

70 Raises: 

71 Crypt4GHKeyException: if only public key is available 

72 

73 The algorithm used is not just a Diffie-Hellman key exchange 

74 to establish shared secret but it also includes derivation of 

75 two symmetric keys used in bi-directional connection. This 

76 pair of keys is derived from the shared secret concatenated 

77 with client public key and server public key by hashing such 

78 binary string with BLAKE2B-512 hash. 

79 

80 For server - and therefore the writer - participant it is the 

81 "transmit" key of the imaginary connection. 

82 

83 ``` 

84 rx || tx = BLAKE2B-512(p.n || client_pk || server_pk) 

85 ``` 

86 

87 The order of shared secret and client and server public keys 

88 in the binary string being matches must be the same on both 

89 sides. Therefore the same symmetric keys are derived. However 

90 for maintaining this ordering, each party must know which one 

91 it is - otherwise even with correctly computed shared secret 

92 the resulting pair of keys would be different. 

93 

94 """ 

95 if self._private_key is None: 

96 raise Crypt4GHKeyException( 

97 "Only keys with private part can be used" 

98 " for computing shared key" 

99 ) 

100 _, shared_key = crypto_kx_server_session_keys( 

101 self._public_key, self._private_key, reader_public_key 

102 ) 

103 return shared_key 

104 

105 def compute_read_key(self, writer_public_key: bytes) -> bytes: 

106 """Computes secret symmetric key used for reading Crypt4GH 

107 encrypted header packets. The instance of this class 

108 represents the reader key. 

109 

110 See detailed description of ``compute_write_key``. 

111 

112 For this function the "receive" key is used - which is the 

113 same as the "transmit" key of the writer. 

114 

115 Parameters: 

116 writer_public_key: the 32 bytes of the writer public key 

117 

118 Returns: 

119 Reader symmetric key as 32 bytes. 

120 

121 Raises: 

122 Crypt4GHKeyException: if only public key is available 

123 

124 """ 

125 if self._private_key is None: 

126 raise Crypt4GHKeyException( 

127 "Only keys with private part can be used" 

128 " for computing shared key" 

129 ) 

130 shared_key, _ = crypto_kx_client_session_keys( 

131 self._public_key, self._private_key, writer_public_key 

132 ) 

133 return shared_key 

134 

135 @property 

136 def can_compute_symmetric_keys(self) -> bool: 

137 """Returns True if this key contains the private part. 

138 

139 Returns: 

140 True if private key is available. 

141 

142 """ 

143 return self._private_key is not None 

144 

145 @classmethod 

146 def generate(self) -> None: 

147 token = secrets.token_bytes(32) 

148 return SoftwareKey(token)