1# Copyright 2013 Donald Stufft and individual contributors
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14from __future__ import annotations
15
16from typing import TYPE_CHECKING, ClassVar, Generic, TypeVar
17
18import nacl.bindings
19from nacl import encoding
20from nacl import exceptions as exc
21from nacl.encoding import Encoder
22from nacl.utils import EncryptedMessage, StringFixer, random
23
24if TYPE_CHECKING:
25 from typing_extensions import Self
26
27
28class PublicKey(encoding.Encodable, StringFixer):
29 """
30 The public key counterpart to an Curve25519 :class:`nacl.public.PrivateKey`
31 for encrypting messages.
32
33 :param public_key: [:class:`bytes`] Encoded Curve25519 public key
34 :param encoder: A class that is able to decode the `public_key`
35
36 :cvar SIZE: The size that the public key is required to be
37 """
38
39 SIZE: ClassVar[int] = nacl.bindings.crypto_box_PUBLICKEYBYTES
40
41 def __init__(
42 self,
43 public_key: bytes,
44 encoder: encoding.Encoder = encoding.RawEncoder,
45 ):
46 self._public_key = encoder.decode(public_key)
47 if not isinstance(self._public_key, bytes):
48 raise exc.TypeError("PublicKey must be created from 32 bytes")
49
50 if len(self._public_key) != self.SIZE:
51 raise exc.ValueError(
52 f"The public key must be exactly {self.SIZE} bytes long"
53 )
54
55 def __bytes__(self) -> bytes:
56 return self._public_key
57
58 def __hash__(self) -> int:
59 return hash(bytes(self))
60
61 def __eq__(self, other: object) -> bool:
62 if not isinstance(other, self.__class__):
63 return False
64 return nacl.bindings.sodium_memcmp(bytes(self), bytes(other))
65
66 def __ne__(self, other: object) -> bool:
67 return not (self == other)
68
69
70class PrivateKey(encoding.Encodable, StringFixer):
71 """
72 Private key for decrypting messages using the Curve25519 algorithm.
73
74 .. warning:: This **must** be protected and remain secret. Anyone who
75 knows the value of your :class:`~nacl.public.PrivateKey` can decrypt
76 any message encrypted by the corresponding
77 :class:`~nacl.public.PublicKey`
78
79 :param private_key: The private key used to decrypt messages
80 :param encoder: The encoder class used to decode the given keys
81
82 :cvar SIZE: The size that the private key is required to be
83 :cvar SEED_SIZE: The size that the seed used to generate the
84 private key is required to be
85 """
86
87 SIZE: ClassVar[int] = nacl.bindings.crypto_box_SECRETKEYBYTES
88 SEED_SIZE: ClassVar[int] = nacl.bindings.crypto_box_SEEDBYTES
89
90 def __init__(
91 self,
92 private_key: bytes,
93 encoder: encoding.Encoder = encoding.RawEncoder,
94 ):
95 # Decode the secret_key
96 private_key = encoder.decode(private_key)
97 # verify the given secret key type and size are correct
98 if not (
99 isinstance(private_key, bytes) and len(private_key) == self.SIZE
100 ):
101 raise exc.TypeError(
102 f"PrivateKey must be created from a {self.SIZE} bytes long raw secret key"
103 )
104
105 raw_public_key = nacl.bindings.crypto_scalarmult_base(private_key)
106
107 self._private_key = private_key
108 self.public_key = PublicKey(raw_public_key)
109
110 @classmethod
111 def from_seed(
112 cls,
113 seed: bytes,
114 encoder: encoding.Encoder = encoding.RawEncoder,
115 ) -> PrivateKey:
116 """
117 Generate a PrivateKey using a deterministic construction
118 starting from a caller-provided seed
119
120 .. warning:: The seed **must** be high-entropy; therefore,
121 its generator **must** be a cryptographic quality
122 random function like, for example, :func:`~nacl.utils.random`.
123
124 .. warning:: The seed **must** be protected and remain secret.
125 Anyone who knows the seed is really in possession of
126 the corresponding PrivateKey.
127
128 :param seed: The seed used to generate the private key
129 :rtype: :class:`~nacl.public.PrivateKey`
130 """
131 # decode the seed
132 seed = encoder.decode(seed)
133 # Verify the given seed type and size are correct
134 if not (isinstance(seed, bytes) and len(seed) == cls.SEED_SIZE):
135 raise exc.TypeError(
136 f"PrivateKey seed must be a {cls.SEED_SIZE} bytes long binary sequence"
137 )
138 # generate a raw key pair from the given seed
139 _raw_pk, raw_sk = nacl.bindings.crypto_box_seed_keypair(seed)
140 # construct a instance from the raw secret key
141 return cls(raw_sk)
142
143 def __bytes__(self) -> bytes:
144 return self._private_key
145
146 def __hash__(self) -> int:
147 return hash((type(self), bytes(self.public_key)))
148
149 def __eq__(self, other: object) -> bool:
150 if not isinstance(other, self.__class__):
151 return False
152 return self.public_key == other.public_key
153
154 def __ne__(self, other: object) -> bool:
155 return not (self == other)
156
157 @classmethod
158 def generate(cls) -> PrivateKey:
159 """
160 Generates a random :class:`~nacl.public.PrivateKey` object
161
162 :rtype: :class:`~nacl.public.PrivateKey`
163 """
164 return cls(random(PrivateKey.SIZE), encoder=encoding.RawEncoder)
165
166
167class Box(encoding.Encodable, StringFixer):
168 """
169 The Box class boxes and unboxes messages between a pair of keys
170
171 The ciphertexts generated by :class:`~nacl.public.Box` include a 16
172 byte authenticator which is checked as part of the decryption. An invalid
173 authenticator will cause the decrypt function to raise an exception. The
174 authenticator is not a signature. Once you've decrypted the message you've
175 demonstrated the ability to create arbitrary valid message, so messages you
176 send are repudiable. For non-repudiable messages, sign them after
177 encryption.
178
179 :param private_key: :class:`~nacl.public.PrivateKey` used to encrypt and
180 decrypt messages
181 :param public_key: :class:`~nacl.public.PublicKey` used to encrypt and
182 decrypt messages
183
184 :cvar NONCE_SIZE: The size that the nonce is required to be.
185 """
186
187 NONCE_SIZE: ClassVar[int] = nacl.bindings.crypto_box_NONCEBYTES
188 _shared_key: bytes
189
190 def __init__(self, private_key: PrivateKey, public_key: PublicKey):
191 if not isinstance(private_key, PrivateKey) or not isinstance(
192 public_key, PublicKey
193 ):
194 raise exc.TypeError(
195 "Box must be created from a PrivateKey and a PublicKey"
196 )
197 self._shared_key = nacl.bindings.crypto_box_beforenm(
198 public_key.encode(encoder=encoding.RawEncoder),
199 private_key.encode(encoder=encoding.RawEncoder),
200 )
201
202 def __bytes__(self) -> bytes:
203 return self._shared_key
204
205 @classmethod
206 def decode(
207 cls, encoded: bytes, encoder: Encoder = encoding.RawEncoder
208 ) -> Self:
209 """
210 Alternative constructor. Creates a Box from an existing Box's shared key.
211 """
212 # Create an empty box
213 box: Self = cls.__new__(cls)
214
215 # Assign our decoded value to the shared key of the box
216 box._shared_key = encoder.decode(encoded)
217
218 return box
219
220 def encrypt(
221 self,
222 plaintext: bytes,
223 nonce: bytes | None = None,
224 encoder: encoding.Encoder = encoding.RawEncoder,
225 ) -> EncryptedMessage:
226 """
227 Encrypts the plaintext message using the given `nonce` (or generates
228 one randomly if omitted) and returns the ciphertext encoded with the
229 encoder.
230
231 .. warning:: It is **VITALLY** important that the nonce is a nonce,
232 i.e. it is a number used only once for any given key. If you fail
233 to do this, you compromise the privacy of the messages encrypted.
234
235 :param plaintext: [:class:`bytes`] The plaintext message to encrypt
236 :param nonce: [:class:`bytes`] The nonce to use in the encryption
237 :param encoder: The encoder to use to encode the ciphertext
238 :rtype: [:class:`nacl.utils.EncryptedMessage`]
239 """
240 if nonce is None:
241 nonce = random(self.NONCE_SIZE)
242
243 if len(nonce) != self.NONCE_SIZE:
244 raise exc.ValueError(
245 f"The nonce must be exactly {self.NONCE_SIZE} bytes long"
246 )
247
248 ciphertext = nacl.bindings.crypto_box_easy_afternm(
249 plaintext,
250 nonce,
251 self._shared_key,
252 )
253
254 encoded_nonce = encoder.encode(nonce)
255 encoded_ciphertext = encoder.encode(ciphertext)
256
257 return EncryptedMessage._from_parts(
258 encoded_nonce,
259 encoded_ciphertext,
260 encoder.encode(nonce + ciphertext),
261 )
262
263 def decrypt(
264 self,
265 ciphertext: bytes,
266 nonce: bytes | None = None,
267 encoder: encoding.Encoder = encoding.RawEncoder,
268 ) -> bytes:
269 """
270 Decrypts the ciphertext using the `nonce` (explicitly, when passed as a
271 parameter or implicitly, when omitted, as part of the ciphertext) and
272 returns the plaintext message.
273
274 :param ciphertext: [:class:`bytes`] The encrypted message to decrypt
275 :param nonce: [:class:`bytes`] The nonce used when encrypting the
276 ciphertext
277 :param encoder: The encoder used to decode the ciphertext.
278 :rtype: [:class:`bytes`]
279 """
280 # Decode our ciphertext
281 ciphertext = encoder.decode(ciphertext)
282
283 if nonce is None:
284 # If we were given the nonce and ciphertext combined, split them.
285 nonce = ciphertext[: self.NONCE_SIZE]
286 ciphertext = ciphertext[self.NONCE_SIZE :]
287
288 if len(nonce) != self.NONCE_SIZE:
289 raise exc.ValueError(
290 f"The nonce must be exactly {self.NONCE_SIZE} bytes long"
291 )
292
293 plaintext = nacl.bindings.crypto_box_open_easy_afternm(
294 ciphertext,
295 nonce,
296 self._shared_key,
297 )
298
299 return plaintext
300
301 def shared_key(self) -> bytes:
302 """
303 Returns the Curve25519 shared secret, that can then be used as a key in
304 other symmetric ciphers.
305
306 .. warning:: It is **VITALLY** important that you use a nonce with your
307 symmetric cipher. If you fail to do this, you compromise the
308 privacy of the messages encrypted. Ensure that the key length of
309 your cipher is 32 bytes.
310 :rtype: [:class:`bytes`]
311 """
312
313 return self._shared_key
314
315
316_Key = TypeVar("_Key", PublicKey, PrivateKey)
317
318
319class SealedBox(encoding.Encodable, StringFixer, Generic[_Key]):
320 """
321 The SealedBox class boxes and unboxes messages addressed to
322 a specified key-pair by using ephemeral sender's key pairs,
323 whose private part will be discarded just after encrypting
324 a single plaintext message.
325
326 The ciphertexts generated by :class:`~nacl.public.SecretBox` include
327 the public part of the ephemeral key before the :class:`~nacl.public.Box`
328 ciphertext.
329
330 :param recipient_key: a :class:`~nacl.public.PublicKey` used to encrypt
331 messages and derive nonces, or a :class:`~nacl.public.PrivateKey` used
332 to decrypt messages.
333
334 .. versionadded:: 1.2
335 """
336
337 _public_key: bytes
338 _private_key: bytes | None
339
340 def __init__(self, recipient_key: _Key):
341 if isinstance(recipient_key, PublicKey):
342 self._public_key = recipient_key.encode(
343 encoder=encoding.RawEncoder
344 )
345 self._private_key = None
346 elif isinstance(recipient_key, PrivateKey):
347 self._private_key = recipient_key.encode(
348 encoder=encoding.RawEncoder
349 )
350 self._public_key = recipient_key.public_key.encode(
351 encoder=encoding.RawEncoder
352 )
353 else:
354 raise exc.TypeError(
355 "SealedBox must be created from a PublicKey or a PrivateKey"
356 )
357
358 def __bytes__(self) -> bytes:
359 return self._public_key
360
361 def encrypt(
362 self,
363 plaintext: bytes,
364 encoder: encoding.Encoder = encoding.RawEncoder,
365 ) -> bytes:
366 """
367 Encrypts the plaintext message using a random-generated ephemeral
368 key pair and returns a "composed ciphertext", containing both
369 the public part of the key pair and the ciphertext proper,
370 encoded with the encoder.
371
372 The private part of the ephemeral key-pair will be scrubbed before
373 returning the ciphertext, therefore, the sender will not be able to
374 decrypt the generated ciphertext.
375
376 :param plaintext: [:class:`bytes`] The plaintext message to encrypt
377 :param encoder: The encoder to use to encode the ciphertext
378 :return bytes: encoded ciphertext
379 """
380
381 ciphertext = nacl.bindings.crypto_box_seal(plaintext, self._public_key)
382
383 encoded_ciphertext = encoder.encode(ciphertext)
384
385 return encoded_ciphertext
386
387 def decrypt(
388 self: SealedBox[PrivateKey],
389 ciphertext: bytes,
390 encoder: encoding.Encoder = encoding.RawEncoder,
391 ) -> bytes:
392 """
393 Decrypts the ciphertext using the ephemeral public key enclosed
394 in the ciphertext and the SealedBox private key, returning
395 the plaintext message.
396
397 :param ciphertext: [:class:`bytes`] The encrypted message to decrypt
398 :param encoder: The encoder used to decode the ciphertext.
399 :return bytes: The original plaintext
400 :raises TypeError: if this SealedBox was created with a
401 :class:`~nacl.public.PublicKey` rather than a
402 :class:`~nacl.public.PrivateKey`.
403 """
404 # Decode our ciphertext
405 ciphertext = encoder.decode(ciphertext)
406
407 if self._private_key is None:
408 raise TypeError(
409 "SealedBoxes created with a public key cannot decrypt"
410 )
411 plaintext = nacl.bindings.crypto_box_seal_open(
412 ciphertext,
413 self._public_key,
414 self._private_key,
415 )
416
417 return plaintext