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
« 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.
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.
8"""
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
21class SoftwareKey(Key):
22 """This class implements the actual Diffie-Hellman key exchange
23 with locally stored private key in the class instance.
25 """
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.
31 Parameters:
32 key_data: the 32 bytes of key material
33 only_public: whether this contains only the public point
35 Raises:
36 AssertionError: is the key_data does not contain exactly 32 bytes
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)
51 @property
52 def public_key(self) -> bytes:
53 """Returns the public key corresponding to the private key
54 used.
56 """
57 return self._public_key
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.
64 Parameters:
65 reader_public_key: the 32 bytes of the reader public key
67 Returns:
68 Writer symmetric key as 32 bytes.
70 Raises:
71 Crypt4GHKeyException: if only public key is available
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.
80 For server - and therefore the writer - participant it is the
81 "transmit" key of the imaginary connection.
83 ```
84 rx || tx = BLAKE2B-512(p.n || client_pk || server_pk)
85 ```
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.
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
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.
110 See detailed description of ``compute_write_key``.
112 For this function the "receive" key is used - which is the
113 same as the "transmit" key of the writer.
115 Parameters:
116 writer_public_key: the 32 bytes of the writer public key
118 Returns:
119 Reader symmetric key as 32 bytes.
121 Raises:
122 Crypt4GHKeyException: if only public key is available
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
135 @property
136 def can_compute_symmetric_keys(self) -> bool:
137 """Returns True if this key contains the private part.
139 Returns:
140 True if private key is available.
142 """
143 return self._private_key is not None
145 @classmethod
146 def generate(self) -> None:
147 token = secrets.token_bytes(32)
148 return SoftwareKey(token)