bases.py 25.8 KB
Newer Older
1
2
3
4
5
"""Bases: base classes for signers."""

import hashlib
import typing
from abc import ABC
6
from abc import abstractmethod
7
8
9
10
from dataclasses import dataclass
from datetime import timedelta
from enum import Enum
from secrets import compare_digest
11
from secrets import token_bytes
12
13
14
from time import time

from . import errors
15
16
17
from .encoders import B64URLEncoder
from .interfaces import EncoderInterface
from .mixins import EncoderMixin
18
from .mixins import Mixin
19
from .utils import file_mode_is_text
20
from .utils import timestamp_to_aware_datetime
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46


@dataclass(frozen=True)
class SignedDataParts:
    """Parts of a signed data container."""

    data: bytes
    salt: bytes
    signature: bytes


@dataclass(frozen=True)
class TimestampedDataParts:
    """Parts of a timestamped data container."""

    data: bytes
    timestamp: int


class HasherChoice(str, Enum):
    """Hasher selection choices."""

    blake2b = 'blake2b'
    blake2s = 'blake2s'


47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@dataclass(frozen=True)
class Blake2SignatureDump:
    """Signature container."""

    signature: str  # Composite signature
    data: str


@dataclass(frozen=True)
class Blake2Signature:
    """Signature container."""

    signature: bytes  # Composite signature
    data: bytes


63
64
class Base(Mixin, ABC):
    """Base class containing the minimum for a signer."""
65
66
67

    Hashers = HasherChoice  # Sugar to avoid having to import the enum

68
69
70
71
72
73
    MIN_SECRET_SIZE: int = 16
    """Minimum secret size allowed (during instantiation)."""

    MIN_DIGEST_SIZE: int = 16
    """Minimum digest size allowed (during instantiation)."""

74
    DEFAULT_DIGEST_SIZE: int = 16  # 16 bytes is good security/size tradeoff
75
    """Default digest size to use when no digest size is indicated."""
76
77
78

    def __init__(
        self,
79
        secret: typing.Union[str, bytes],
80
        *,
81
        personalisation: typing.Union[str, bytes] = b'',
82
        digest_size: typing.Optional[int] = None,
83
        hasher: typing.Union[HasherChoice, str] = HasherChoice.blake2b,
84
        deterministic: bool = False,
85
        separator: typing.Union[str, bytes] = b'.',
86
    ) -> None:
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
        """Sign and verify signed data using BLAKE2 in keyed hashing mode.

        Args:
            secret: Secret value which will be derived using BLAKE2 to
                produce the signing key. The minimum secret size is enforced to
                16 bytes and there is no maximum since the key will be derived to
                the maximum supported size.
            personalisation (optional): Personalisation string to force the hash
                function to produce different digests for the same input. It is
                derived using BLAKE2 to ensure it fits the hasher limits, so it
                has no practical size limit. It defaults to the class name.
            digest_size (optional): Size of output signature (digest) in bytes
                (defaults to 16 bytes). The minimum size is enforced to 16 bytes.
            hasher (optional): Hash function to use: blake2b (default) or blake2s.
            deterministic (optional): Define if signatures are deterministic or
                non-deterministic (default). Non-deterministic sigs are preferred,
                and achieved through the use of a random salt. For deterministic
                sigs, no salt is used: this means that for the same payload, the
                same sig is obtained (the advantage is that the sig is shorter).
            separator (optional): Character to separate the signature and the
                payload. It must not belong to the encoder alphabet and be ASCII
                (defaults to ".").

        Raises:
            ConversionError: A bytes parameter is not bytes and can't be converted
                to bytes.
            InvalidOptionError: A parameter is out of bounds.
114
115
116
117
118
        """
        self._hasher: typing.Union[
            typing.Type[hashlib.blake2b],
            typing.Type[hashlib.blake2s],
        ]
119
120
121
        self._hasher = self._validate_hasher(hasher)

        digest_size = self._validate_digest_size(digest_size)
122
        separator = self._validate_separator(separator)
123
124
        person = self._validate_person(personalisation)
        secret = self._validate_secret(secret)
125
126
127

        if deterministic:
            person += b'Deterministic'
128
129
130
131
        person += self.__class__.__name__.encode()

        self._deterministic: bool = deterministic
        self._digest_size: int = digest_size
132
        self._separator: bytes = separator
133
134
135
136
137
138
139
140
        self._person: bytes = self._derive_person(person)
        self._key: bytes = self._derive_key(secret, person=self._person)  # bye secret :)

    @property
    def _salt_size(self) -> int:
        """Get the salt size."""
        return self._hasher.SALT_SIZE

141
    def _validate_secret(self, secret_: typing.Union[str, bytes]) -> bytes:
142
143
144
145
146
147
148
149
150
151
152
153
        """Validate the secret value and return it clean.

        Args:
            secret_: Secret value to validate.

        Returns:
            Cleaned secret value.

        Raises:
            ConversionError: The value is not bytes and can't be converted to bytes.
            InvalidOptionError: The value is out of bounds.
        """
154
        secret = self._force_bytes(secret_)
155
156
157
158
159
160

        if len(secret) < self.MIN_SECRET_SIZE:
            raise errors.InvalidOptionError(
                f'secret should be longer than {self.MIN_SECRET_SIZE} bytes',
            )

161
162
        return secret

163
    def _validate_person(self, person: typing.Union[str, bytes]) -> bytes:
164
165
166
167
168
169
170
171
172
173
174
        """Validate the personalisation value and return it clean.

        Args:
            person: Personalisation value to validate.

        Returns:
            Cleaned personalisation value.

        Raises:
            ConversionError: The value is not bytes and can't be converted to bytes.
        """
175
176
177
        return self._force_bytes(person)

    def _validate_digest_size(self, digest_size: typing.Optional[int]) -> int:
178
179
180
181
182
183
184
185
186
187
188
        """Validate the digest_size value and return it clean.

        Args:
            digest_size: Digest size value to validate.

        Returns:
            Cleaned digest size value.

        Raises:
            InvalidOptionError: The value is out of bounds.
        """
189
        if digest_size is None:
190
            digest_size = self.DEFAULT_DIGEST_SIZE
191

192
193
        if self.MIN_DIGEST_SIZE <= digest_size <= self._hasher.MAX_DIGEST_SIZE:
            return digest_size
194

195
196
197
198
        raise errors.InvalidOptionError(
            f'digest_size should be between {self.MIN_DIGEST_SIZE} and '
            f'{self._hasher.MAX_DIGEST_SIZE}',
        )
199

200
201
    @staticmethod
    def _validate_hasher(
202
203
        hasher: typing.Union[HasherChoice, str],
    ) -> typing.Union[typing.Type[hashlib.blake2b], typing.Type[hashlib.blake2s]]:
204
205
206
207
208
209
210
211
212
213
214
        """Validate and choose hashing function.

        Args:
            hasher: Hasher value to validate.

        Returns:
            A hashing function based on the hasher value.

        Raises:
            InvalidOptionError: Invalid hasher choice.
        """
215
        if hasher == HasherChoice.blake2b:
216
            return hashlib.blake2b
217
        elif hasher == HasherChoice.blake2s:
218
219
220
221
            return hashlib.blake2s

        raise errors.InvalidOptionError(
            f'invalid hasher choice, must be one of: '
222
            f'{", ".join(h for h in HasherChoice)}',
223
224
        )

225
    def _validate_separator(self, separator: typing.Union[str, bytes]) -> bytes:
226
227
228
229
230
231
232
233
234
235
236
237
        """Validate the separator value and return it clean.

        Args:
            separator: Separator value to validate.

        Returns:
            Cleaned separator value.

        Raises:
            ConversionError: The value is not bytes and can't be converted to bytes.
            InvalidOptionError:  The value is out of bounds.
        """
238
239
240
        if not separator:
            raise errors.InvalidOptionError('the separator character must have a value')

241
242
243
        if not separator.isascii():
            raise errors.InvalidOptionError('the separator character must be ASCII')

244
245
        return self._force_bytes(separator)

246
    def _derive_person(self, person: bytes) -> bytes:
247
248
249
250
251
252
253
254
255
256
257
258
        """Derive given personalisation value to ensure it fits the hasher correctly.

        Args:
            person: Personalisation value to validate.

        Returns:
            Cleaned personalisation value.

        Raises:
            ConversionError: The value is not bytes and can't be converted to bytes.
            InvalidOptionError: The value is out of bounds.
        """
259
260
261
        return self._hasher(person, digest_size=self._hasher.PERSON_SIZE).digest()

    def _derive_key(self, secret: bytes, *, person: bytes = b'') -> bytes:
262
263
264
265
266
267
268
269
270
271
272
        """Derive given secret to ensure it fits correctly as the hasher key.

        Args:
            secret: Secret value to derive.

        Keyword Args:
            person (optional): Personalisation value to change the secret derivation.

        Returns:
            An raw derived secret value to use as hasher key.
        """
273
274
275
276
277
278
        return self._hasher(
            secret,
            person=person,
            digest_size=self._hasher.MAX_KEY_SIZE,
        ).digest()

279
280

class Blake2SignerBase(EncoderMixin, Base, ABC):
281
    """Base class for a signer based on BLAKE2 in keyed hashing mode."""
282
283
284

    def __init__(
        self,
285
        secret: typing.Union[str, bytes],
286
        *,
287
        personalisation: typing.Union[str, bytes] = b'',
288
289
290
        digest_size: typing.Optional[int] = None,
        hasher: typing.Union[HasherChoice, str] = HasherChoice.blake2b,
        deterministic: bool = False,
291
        separator: typing.Union[str, bytes] = b'.',
292
293
        encoder: typing.Type[EncoderInterface] = B64URLEncoder,
    ) -> None:
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
        """Sign and verify signed data using BLAKE2 in keyed hashing mode.

        Args:
            secret: Secret value which will be derived using BLAKE2 to
                produce the signing key. The minimum secret size is enforced to
                16 bytes and there is no maximum since the key will be derived to
                the maximum supported size.
            personalisation (optional): Personalisation string to force the hash
                function to produce different digests for the same input. It is
                derived using BLAKE2 to ensure it fits the hasher limits, so it
                has no practical size limit. It defaults to the class name.
            digest_size (optional): Size of output signature (digest) in bytes
                (defaults to 16 bytes). The minimum size is enforced to 16 bytes.
            hasher (optional): Hash function to use: blake2b (default) or blake2s.
            deterministic (optional): Define if signatures are deterministic or
                non-deterministic (default). Non-deterministic sigs are preferred,
                and achieved through the use of a random salt. For deterministic
                sigs, no salt is used: this means that for the same payload, the
                same sig is obtained (the advantage is that the sig is shorter).
            separator (optional): Character to separate the signature and the
                payload. It must not belong to the encoder alphabet and be ASCII
                (defaults to ".").
            encoder (optional): Encoder class to use for the signature, nothing
                else is encoded (defaults to a Base64 URL safe encoder).

        Raises:
            ConversionError: A bytes parameter is not bytes and can't be converted
                to bytes.
            InvalidOptionError: A parameter is out of bounds.
323
324
325
326
327
328
        """
        super().__init__(
            secret,
            personalisation=personalisation,
            digest_size=digest_size,
            hasher=hasher,
329
            separator=separator,
330
331
332
            deterministic=deterministic,
            encoder=encoder,
        )
333

334
    def _validate_separator(self, separator: typing.Union[str, bytes]) -> bytes:
335
336
337
338
339
340
341
342
343
344
345
346
        """Validate the separator value and return it clean.

        Args:
            separator: Separator value to validate.

        Returns:
            Cleaned separator value.

        Raises:
            ConversionError: The value is not bytes and can't be converted to bytes.
            InvalidOptionError: The value is out of bounds.
        """
347
348
349
350
351
352
353
354
355
        sep = super()._validate_separator(separator)

        if sep in self._encoder.alphabet:
            raise errors.InvalidOptionError(
                'the separator character must not belong to the encoder alphabet',
            )

        return sep

356
357
358
359
360
    def _get_salt(self) -> bytes:
        """Get a salt for the signature considering its type.

        For non-deterministic signatures, a pseudo random salt is generated.
        """
361
362
363
364
        if self._deterministic:
            return b''

        salt = token_bytes(self._salt_size)
365
        # Produce an encoded salt to use it as is, so we don't have to deal with
366
367
368
        # decoding it when unsigning. The only downside is that we loose a few
        # bits but it's tolerable since we are using the maximum allowed size.
        return self._encode(salt)[:self._salt_size]
369

370
371
372
373
374
375
376
377
378
379
380
381
382
    def _force_bytes_parts(
        self,
        signature: typing.Union[Blake2Signature, Blake2SignatureDump],
    ) -> Blake2Signature:
        """Force given value into bytes, meaning a Blake2Signature container."""
        return Blake2Signature(
            data=self._force_bytes(signature.data),
            signature=self._force_bytes(signature.signature),
        )

    def _compose(self, data: bytes, *, signature: bytes) -> bytes:
        """Compose data and signature into a single stream."""
        return signature + self._separator + data
383
384
385
386

    def _decompose(self, signed_data: bytes) -> SignedDataParts:
        """Decompose a signed data stream into its parts.

387
388
        Raises:
            SignatureError: Invalid signed data.
389
        """
390
        if self._separator not in signed_data:
391
392
            raise errors.SignatureError('separator not found in signed data')

393
        composite_signature, data = signed_data.split(self._separator, 1)
394

395
396
397
        if not composite_signature:
            raise errors.SignatureError('signature information is missing')

398
399
400
401
402
403
404
405
406
407
        if self._deterministic:
            salt = b''
            signature = composite_signature
        else:
            salt = composite_signature[:self._salt_size]
            signature = composite_signature[self._salt_size:]

        return SignedDataParts(data=data, salt=salt, signature=signature)

    def _signify(self, *, salt: bytes, data: bytes) -> bytes:
408
        """Return signature for given data using salt and all the hasher options.
409

410
        The signature is encoded using the chosen encoder.
411
412
413
414
415
416
417
418
419
        """
        signature = self._hasher(
            data,
            salt=salt,
            key=self._key,
            person=self._person,
            digest_size=self._digest_size,
        ).digest()

420
        return self._encode(signature)
421
422

    def _sign(self, data: bytes) -> bytes:
423
        """Sign given data and produce a signature stream composed of salt and signature.
424

425
        The signature stream (salt and signature) is encoded using the chosen encoder.
426
427
428
429
        """
        salt = self._get_salt()
        signature = self._signify(salt=salt, data=data)

430
        return salt + signature
431

432
433
    def _unsign(self, parts: SignedDataParts) -> bytes:
        """Verify signed data parts and recover original data.
434

435
436
        Args:
            parts: Signed data parts to unsign.
437

438
439
        Returns:
            Original data.
440

441
442
443
        Raises:
            SignatureError: Signed data structure is not valid.
            InvalidSignatureError: Signed data signature is invalid.
444
        """
445
446
447
        good_signature = self._signify(salt=parts.salt, data=parts.data)

        if compare_digest(good_signature, parts.signature):
448
449
450
451
452
453
            return parts.data

        raise errors.InvalidSignatureError('signature is not valid')


class Blake2TimestampSignerBase(Blake2SignerBase, ABC):
454
    """Base class for a timestamp signer based on BLAKE2 in keyed hashing mode."""
455

456
    def _get_timestamp(self) -> bytes:
457
        """Get the encoded timestamp value."""
458
        timestamp = int(time())  # It's easier to encode and decode an integer
459
460
461
462
463
464
465
466
        try:
            timestamp_b = timestamp.to_bytes(4, 'big', signed=False)
        except OverflowError:  # This will happen in ~2106-02-07
            raise RuntimeError(
                'can not represent this timestamp in bytes: this library is '
                'too old and needs to be updated!',
            )

467
        return self._encode(timestamp_b)
468

469
    def _decode_timestamp(self, encoded_timestamp: bytes) -> int:
470
471
        """Decode an encoded timestamp whose signature should have been validated.

472
473
        Raises:
            DecodeError: Timestamp can't be decoded.
474
        """
475
        return int.from_bytes(self._decode(encoded_timestamp), 'big', signed=False)
476

477
478
479
    def _compose_timestamp(self, data: bytes, *, timestamp: bytes) -> bytes:
        """Compose timestamp value with data."""
        return timestamp + self._separator + data
480

481
482
    def _decompose_timestamp(self, timestamped_data: bytes) -> TimestampedDataParts:
        """Decompose data + timestamp value.
483

484
485
486
        Raises:
            SignatureError: Invalid timestamped data.
            DecodeError: Timestamp can't be decoded.
487
        """
488
        if self._separator not in timestamped_data:
489
490
            raise errors.SignatureError('separator not found in timestamped data')

491
        encoded_timestamp, data = timestamped_data.split(self._separator, 1)
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508

        if not encoded_timestamp:
            raise errors.SignatureError('timestamp information is missing')

        timestamp = self._decode_timestamp(encoded_timestamp)

        return TimestampedDataParts(data=data, timestamp=timestamp)

    @staticmethod
    def _get_ttl_from_max_age(max_age: typing.Union[int, float, timedelta]) -> float:
        """Get the time-to-live value in seconds."""
        if isinstance(max_age, timedelta):
            return max_age.total_seconds()

        return float(max_age)

    def _sign_with_timestamp(self, data: bytes) -> bytes:
509
        """Sign given data and produce a timestamped signature stream.
510

511
512
        The timestamped signature stream (timestamp, signature and salt) is
        encoded using the chosen encoder.
513

514
515
        Returns:
            A signature stream composed of salt, signature and timestamp.
516
        """
517
518
        timestamp = self._get_timestamp()
        timestamped_data = self._compose_timestamp(data, timestamp=timestamp)
519

520
        return self._compose(timestamp, signature=self._sign(timestamped_data))
521
522
523

    def _unsign_with_timestamp(
        self,
524
        parts: SignedDataParts,
525
526
527
        *,
        max_age: typing.Union[int, float, timedelta],
    ) -> bytes:
528
        """Verify signed data parts with timestamp and recover original data.
529

530
531
532
533
534
        Args:
            parts: Signed data parts to unsign.

        Keyword Args:
            max_age: Ensure the signature is not older than this time in seconds.
535

536
537
        Returns:
            Original data.
538

539
540
541
542
543
        Raises:
            SignatureError: Signed data structure is not valid.
            InvalidSignatureError: Signed data signature is invalid.
            ExpiredSignatureError: Signed data signature has expired.
            DecodeError: Timestamp can't be decoded.
544
        """
545
        timestamped_data = self._unsign(parts)
546

547
        timestamped_parts = self._decompose_timestamp(timestamped_data)
548
549

        now = time()
550
        age = now - timestamped_parts.timestamp
551
        ttl = self._get_ttl_from_max_age(max_age)
552

553
        if age > ttl:
554
555
            raise errors.ExpiredSignatureError(
                f'signature has expired, age {age} > {ttl} seconds',
556
                timestamp=timestamp_to_aware_datetime(timestamped_parts.timestamp),
557
            )
558

559
560
561
        if age < 0:  # Signed in the future
            raise errors.ExpiredSignatureError(
                f'signature has expired, age {age} < 0 seconds',
562
                timestamp=timestamp_to_aware_datetime(timestamped_parts.timestamp),
563
564
            )

565
        return timestamped_parts.data
566
567


568
569
class Blake2DualSignerBase(Blake2TimestampSignerBase, ABC):
    """Base class for a dual signer: with and without timestamp."""
570
571
572

    def __init__(
        self,
573
        secret: typing.Union[str, bytes],
574
575
        *,
        max_age: typing.Union[None, int, float, timedelta] = None,
576
        personalisation: typing.Union[str, bytes] = b'',
577
578
579
        digest_size: typing.Optional[int] = None,
        hasher: typing.Union[HasherChoice, str] = HasherChoice.blake2b,
        deterministic: bool = False,
580
        separator: typing.Union[str, bytes] = b'.',
581
        encoder: typing.Type[EncoderInterface] = B64URLEncoder,
582
    ) -> None:
583
        """Sign and verify signed and optionally timestamped data using BLAKE2.
584

585
        It uses BLAKE2 in keyed hashing mode.
586
587
588

        Setting `max_age` will produce a timestamped signed stream.

589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
        Args:
            secret: Secret value which will be derived using BLAKE2 to
                produce the signing key. The minimum secret size is enforced to
                16 bytes and there is no maximum since the key will be derived to
                the maximum supported size.
            max_age (optional): Use a timestamp signer instead of a regular one
                to ensure that the signature is not older than this time in seconds.
            personalisation (optional): Personalisation string to force the hash
                function to produce different digests for the same input. It is
                derived using BLAKE2 to ensure it fits the hasher limits, so it
                has no practical size limit. It defaults to the class name.
            digest_size (optional): Size of output signature (digest) in bytes
                (defaults to 16 bytes). The minimum size is enforced to 16 bytes.
            hasher (optional): Hash function to use: blake2b (default) or blake2s.
            deterministic (optional): Define if signatures are deterministic or
                non-deterministic (default). Non-deterministic sigs are preferred,
                and achieved through the use of a random salt. For deterministic
                sigs, no salt is used: this means that for the same payload, the
                same sig is obtained (the advantage is that the sig is shorter).
            separator (optional): Character to separate the signature and the
                payload. It must not belong to the encoder alphabet and be ASCII
                (defaults to ".").
            encoder (optional): Encoder class to use (defaults to a Base64 URL
                safe encoder).

        Raises:
            ConversionError: A bytes parameter is not bytes and can't be converted
                to bytes.
            InvalidOptionError: A parameter is out of bounds.
618
619
620
621
622
623
624
625
626
        """
        if max_age is not None:
            personalisation = self._force_bytes(personalisation) + b'Timestamp'

        self._max_age: typing.Union[None, int, float, timedelta] = max_age

        super().__init__(
            secret,
            personalisation=personalisation,
627
            digest_size=digest_size,
628
629
            hasher=hasher,
            deterministic=deterministic,
630
            separator=separator,
631
            encoder=encoder,
632
633
        )

634
    def _proper_sign(self, data: bytes) -> bytes:
635
        """Sign given data with a (timestamp) signer producing a signature stream.
636

637
638
        The signature stream (salt, signature and/or timestamp) are encoded using
        the chosen encoder.
639
640
641
        """
        if self._max_age is None:
            return self._sign(data)
642

643
644
        return self._sign_with_timestamp(data)

645
    def _proper_unsign(self, parts: SignedDataParts) -> bytes:
646
647
        """Unsign signed data properly with the corresponding signer.

648
649
650
651
652
        Raises:
            SignatureError: Signed data structure is not valid.
            InvalidSignatureError: Signed data signature is invalid.
            ExpiredSignatureError: Signed data signature has expired.
            DecodeError: Timestamp can't be decoded.
653
        """
654
        if self._max_age is None:
655
            return self._unsign(parts)
656

657
        return self._unsign_with_timestamp(parts, max_age=self._max_age)
658
659


660
661
662
663
664
665
666
667
668
669
class Blake2SerializerSignerBase(Blake2DualSignerBase, ABC):
    """Base class for a serializer signer that implements `dumps` and `loads`."""

    @abstractmethod
    def _dumps(self, data: typing.Any, **kwargs: typing.Any) -> bytes:
        """Dump data serializing it.

        Implement this method with all the tasks necessary to serialize data, such
        as encoding, compression, etc.

670
671
        Args:
            data: Data to serialize.
672

673
674
675
676
677
        Keyword Args:
            **kwargs: Additional keyword only arguments for the method.

        Returns:
            Serialized data.
678
679
680
681
682
683
684
685
686
        """

    @abstractmethod
    def _loads(self, dumped_data: bytes, **kwargs: typing.Any) -> typing.Any:
        """Load serialized data to recover it.

        Implement this method with all the tasks necessary to unserialize data,
        such as decoding, decompression, etc.

687
688
689
690
691
        Args:
            dumped_data: Data to unserialize.

        Keyword Args
            **kwargs: Additional keyword only arguments for the method.
692

693
694
        Returns:
            Original data.
695
        """
696
697
698
699
700

    @staticmethod
    def _read(file: typing.IO) -> typing.AnyStr:
        """Read data from a file.

701
702
        Raises:
            FileError: File can't be read.
703
704
705
706
707
708
709
710
711
        """
        try:
            return file.read()
        except OSError as exc:
            raise errors.FileError('file can not be read') from exc

    def _write(self, file: typing.IO, data: str) -> None:
        """Write data to file.

712
713
714
        Notes:
            The file can be either in text or binary mode, therefore given data
            is properly converted before writing.
715

716
717
718
719
        Raises:
            FileError: File can't be written.
            ConversionError: Data can't be converted to bytes (can happen when
                file is in binary mode).
720
721
722
723
724
725
726
        """
        data_ = data if file_mode_is_text(file) else self._force_bytes(data)

        try:
            file.write(data_)
        except OSError as exc:
            raise errors.FileError('file can not be written') from exc