Skip to content

Commit c3504fa

Browse files
committed
PYCBC-1705: Fix FLE imports and exceptions
Motivation ========== Update and modernize the encryption package interfaces and exceptions so that the separate cbencryption package is compatible with the 4.x version of the SDK. Changes ======= * Update import order of encryption interfaces and classes to avoid circular import error * Migrate away from comment-based type annotations * Update crypto exceptions Change-Id: I6630d20ae22acbcd471ef595a6b66aa0000fb59c Reviewed-on: https://review.couchbase.org/c/couchbase-python-client/+/234185 Tested-by: Build Bot <build@couchbase.com> Reviewed-by: Dimitris Christodoulou <dimitris.christodoulou@couchbase.com>
1 parent 70fbc32 commit c3504fa

File tree

9 files changed

+138
-162
lines changed

9 files changed

+138
-162
lines changed

couchbase/encryption/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016-2022. Couchbase, Inc.
1+
# Copyright 2016-2025. Couchbase, Inc.
22
# All Rights Reserved.
33
#
44
# Licensed under the Apache License, Version 2.0 (the "License")
@@ -14,8 +14,10 @@
1414
# limitations under the License.
1515

1616
from .crypto_manager import CryptoManager # noqa: F401
17-
from .decrypter import Decrypter # noqa: F401
18-
from .encrypter import Encrypter # noqa: F401
1917
from .encryption_result import EncryptionResult # noqa: F401
2018
from .key import Key # noqa: F401
2119
from .keyring import Keyring # noqa: F401
20+
21+
# import Encrypter/Decrypter last to avoid circular import
22+
from .decrypter import Decrypter # nopep8 # isort:skip # noqa: F401
23+
from .encrypter import Encrypter # nopep8 # isort:skip # noqa: F401
Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016-2022. Couchbase, Inc.
1+
# Copyright 2016-2025. Couchbase, Inc.
22
# All Rights Reserved.
33
#
44
# Licensed under the Apache License, Version 2.0 (the "License")
@@ -14,43 +14,39 @@
1414
# limitations under the License.
1515

1616
from abc import ABC, abstractmethod
17-
from typing import Optional, Union
17+
from typing import (Any,
18+
Optional,
19+
Union)
1820

1921

2022
class CryptoManager(ABC):
2123
"""Interface a CryptoManager must implement
2224
2325
"""
2426

25-
_DEFAULT_ENCRYPTER_ALIAS = "__DEFAULT__"
27+
_DEFAULT_ENCRYPTER_ALIAS = '__DEFAULT__'
2628

2729
@abstractmethod
28-
def encrypt(self,
29-
plaintext, # type: Union[str, bytes, bytearray]
30-
encrypter_alias=None, # type: Optional[str]
31-
) -> dict:
30+
def encrypt(self, plaintext: Union[str, bytes, bytearray], encrypter_alias: Optional[str] = None) -> dict[str, Any]:
3231
"""Encrypts the given plaintext using the given encrypter alias.
3332
3433
Args:
3534
plaintext (Union[str, bytes, bytearray]): Input to be encrypted
3635
encrypter_alias (str, optional): Alias of encrypter to use, if None, default alias is used.
3736
3837
Returns:
39-
Dict: A :class:`~couchbase.encryption.EncryptionResult` as a dict
38+
dict: A :class:`~couchbase.encryption.EncryptionResult` as a dict
4039
4140
Raises:
4241
:class:`~couchbase.exceptions.EncryptionFailureException`
4342
"""
44-
pass
4543

4644
@abstractmethod
47-
def decrypt(self,
48-
encrypted, # type: dict
49-
) -> bytes:
45+
def decrypt(self, encrypted: dict[str, Any]) -> bytes:
5046
"""Decrypts the given encrypted result based on the 'alg' key in the encrypted result.
5147
5248
Args:
53-
encrypted (Dict): A dict containing encryption information, must have an 'alg' key.
49+
encrypted (dict[str, Any]): A dict containing encryption information, must have an 'alg' key.
5450
5551
Returns:
5652
bytes: A decrypted result based on the given encrypted input.
@@ -59,12 +55,9 @@ def decrypt(self,
5955
:class:`~couchbase.exceptions.DecryptionFailureException`
6056
6157
"""
62-
pass
6358

6459
@abstractmethod
65-
def mangle(self,
66-
field_name, # type: str
67-
) -> str:
60+
def mangle(self, field_name: str) -> str:
6861
"""Mangles provided JSON field name.
6962
7063
Args:
@@ -73,12 +66,9 @@ def mangle(self,
7366
Returns:
7467
str: The mangled field name.
7568
"""
76-
pass
7769

7870
@abstractmethod
79-
def demangle(self,
80-
field_name, # type: str
81-
) -> str:
71+
def demangle(self, field_name: str) -> str:
8272
"""Demangles provided JSON field name.
8373
8474
Args:
@@ -87,12 +77,9 @@ def demangle(self,
8777
Returns:
8878
str: The demangled field name.
8979
"""
90-
pass
9180

9281
@abstractmethod
93-
def is_mangled(self,
94-
field_name, # type: str
95-
) -> bool:
82+
def is_mangled(self, field_name: str) -> bool:
9683
"""Checks if provided JSON field name has been mangled.
9784
9885
Args:
@@ -102,4 +89,3 @@ def is_mangled(self,
10289
bool: True if the field is mangled, False otherwise.
10390
10491
"""
105-
pass

couchbase/encryption/decrypter.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016-2022. Couchbase, Inc.
1+
# Copyright 2016-2025. Couchbase, Inc.
22
# All Rights Reserved.
33
#
44
# Licensed under the Apache License, Version 2.0 (the "License")
@@ -14,26 +14,22 @@
1414
# limitations under the License.
1515

1616
from abc import ABC, abstractmethod
17-
from typing import TYPE_CHECKING, Optional
17+
from typing import Optional
1818

19-
if TYPE_CHECKING:
20-
from couchbase.encryption import EncryptionResult, Keyring
19+
from couchbase.encryption import EncryptionResult, Keyring
2120

2221

2322
class Decrypter(ABC):
2423
"""Interface a Decrypter must implement
2524
2625
"""
2726

28-
def __init__(self,
29-
keyring, # type: Keyring
30-
alg=None, # type: Optional[str]
31-
):
27+
def __init__(self, keyring: Keyring, alg: Optional[str] = None) -> None:
3228
self._keyring = keyring
3329
self._alg = alg
3430

3531
@property
36-
def keyring(self):
32+
def keyring(self) -> Keyring:
3733
return self._keyring
3834

3935
def algorithm(self) -> str:
@@ -45,9 +41,7 @@ def algorithm(self) -> str:
4541
return self._alg
4642

4743
@abstractmethod
48-
def decrypt(self,
49-
encrypted, # type: EncryptionResult
50-
) -> bytes:
44+
def decrypt(self, encrypted: EncryptionResult) -> bytes:
5145
"""Decrypts the given :class:`~couchbase.encryption.EncryptionResult` ciphertext.
5246
5347
The Decrypter's algorithm should match the `alg` property of the given

couchbase/encryption/encrypter.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016-2022. Couchbase, Inc.
1+
# Copyright 2016-2025. Couchbase, Inc.
22
# All Rights Reserved.
33
#
44
# Licensed under the Apache License, Version 2.0 (the "License")
@@ -20,10 +20,8 @@
2020

2121

2222
class Encrypter(ABC):
23-
def __init__(self,
24-
keyring, # type: Keyring
25-
key, # type: str
26-
):
23+
24+
def __init__(self, keyring: Keyring, key: str) -> None:
2725
self._keyring = keyring
2826
self._key = key
2927

@@ -36,9 +34,7 @@ def key(self) -> str:
3634
return self._key
3735

3836
@abstractmethod
39-
def encrypt(self,
40-
plaintext, # type: Union[str, bytes, bytearray]
41-
) -> EncryptionResult:
37+
def encrypt(self, plaintext: Union[str, bytes, bytearray]) -> EncryptionResult:
4238
"""Encrypts the given plaintext
4339
4440
Args:
@@ -52,4 +48,3 @@ def encrypt(self,
5248
:class:`~couchbase.exceptions.InvalidCryptoKeyException`: If the :class:`.Encrypter` has an invalid
5349
key for encryption.
5450
"""
55-
pass

couchbase/encryption/encryption_result.py

Lines changed: 24 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016-2022. Couchbase, Inc.
1+
# Copyright 2016-2025. Couchbase, Inc.
22
# All Rights Reserved.
33
#
44
# Licensed under the Apache License, Version 2.0 (the "License")
@@ -25,93 +25,67 @@
2525

2626
class EncryptionResult:
2727
def __init__(self,
28-
alg, # type: str
29-
kid=None, # type: Optional[str]
30-
ciphertext=None, # type: Optional[str]
31-
**kwargs, # type: Optional[Any]
28+
alg: str = '',
29+
kid: Optional[str] = None,
30+
ciphertext: Optional[str] = None,
31+
**kwargs: Any
3232
):
33-
self._map = {"alg": alg}
33+
if not alg:
34+
raise InvalidArgumentException('EncryptionResult must include alg property.')
35+
36+
self._map: dict[str, Any] = {'alg': alg}
3437

3538
if kid:
36-
self._map["kid"] = kid
39+
self._map['kid'] = kid
3740

3841
if ciphertext and self._valid_base64(ciphertext):
39-
self._map["ciphertext"] = ciphertext
42+
self._map['ciphertext'] = ciphertext
4043

4144
if kwargs:
4245
self._map.update(**kwargs)
4346

4447
@classmethod
45-
def new_encryption_result_from_dict(cls,
46-
values, # type: dict
47-
) -> EncryptionResult:
48-
49-
alg = values.pop("alg", None)
50-
if not alg:
51-
raise InvalidArgumentException(
52-
"EncryptionResult must include alg property."
53-
)
54-
55-
return EncryptionResult(alg, **values)
48+
def new_encryption_result_from_dict(cls, values: dict[str, Any]) -> EncryptionResult:
49+
return EncryptionResult(**values)
5650

57-
def put(self,
58-
key, # type: str
59-
val # type: Any
60-
):
51+
def put(self, key: str, val: Any) -> None:
6152
self._map[key] = val
6253

63-
def put_and_base64_encode(self,
64-
key, # type: str
65-
val, # type: bytes
66-
):
54+
def put_and_base64_encode(self, key: str, val: bytes) -> None:
6755
if not isinstance(val, bytes):
68-
raise ValueError("Provided value must be of type bytes.")
56+
raise ValueError('Provided value must be of type bytes.')
6957
self._map[key] = base64.b64encode(val)
7058

71-
def get(self,
72-
key, # type: str
73-
) -> Any:
59+
def get(self, key: str) -> Any:
7460
val = self._map.get(key, None)
7561
if not val:
76-
raise CryptoKeyNotFoundException(
77-
message="No mapping to EncryptionResult value found for key: '{}'.".format(
78-
key)
79-
)
62+
raise CryptoKeyNotFoundException(message=f"No mapping to EncryptionResult value found for key: '{key}'.")
8063

8164
return val
8265

8366
def algorithm(self) -> str:
84-
return self._map["alg"]
67+
return self._map['alg']
8568

86-
def get_with_base64_decode(self,
87-
key, # type: str
88-
) -> bytes:
69+
def get_with_base64_decode(self, key: str) -> bytes:
8970
val = self._map.get(key, None)
9071
if not val:
91-
raise CryptoKeyNotFoundException(
92-
message="No mapping to EncryptionResult value found for key: '{}'.".format(
93-
key)
94-
)
72+
raise CryptoKeyNotFoundException(message=f"No mapping to EncryptionResult value found for key: '{key}'.")
9573

9674
return base64.b64decode(val)
9775

9876
def asdict(self) -> dict:
9977
return self._map
10078

101-
def _valid_base64(self,
102-
val, # type: Union[str, bytes, bytearray]
103-
) -> bool:
79+
def _valid_base64(self, val: Union[str, bytes, bytearray]) -> bool:
10480
try:
10581
if isinstance(val, str):
106-
bytes_val = bytes(val, "ascii")
82+
bytes_val = bytes(val, 'ascii')
10783
elif isinstance(val, bytes):
10884
bytes_val = val
10985
elif isinstance(val, bytearray):
11086
bytes_val = val
11187
else:
112-
raise ValueError(
113-
"Provided value must be of type str, bytes or bytearray"
114-
)
88+
raise ValueError('Provided value must be of type str, bytes or bytearray')
11589

11690
return base64.b64encode(base64.b64decode(bytes_val)) == bytes_val
11791

couchbase/encryption/key.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016-2022. Couchbase, Inc.
1+
# Copyright 2016-2025. Couchbase, Inc.
22
# All Rights Reserved.
33
#
44
# Licensed under the Apache License, Version 2.0 (the "License")
@@ -17,10 +17,7 @@
1717

1818

1919
class Key:
20-
def __init__(self,
21-
id, # type: str
22-
bytes_, # type: Union[bytes, bytearray]
23-
):
20+
def __init__(self, id: str, bytes_: Union[bytes, bytearray]) -> None:
2421
self._id = id
2522
self._bytes = bytes_ if isinstance(bytes_, bytes) else bytes(bytes_)
2623

couchbase/encryption/keyring.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016-2022. Couchbase, Inc.
1+
# Copyright 2016-2025. Couchbase, Inc.
22
# All Rights Reserved.
33
#
44
# Licensed under the Apache License, Version 2.0 (the "License")
@@ -14,21 +14,18 @@
1414
# limitations under the License.
1515

1616
from abc import ABC, abstractmethod
17-
from typing import TYPE_CHECKING
1817

19-
if TYPE_CHECKING:
20-
from couchbase.encryption import Key
18+
from couchbase.encryption import Key
2119

2220

2321
class Keyring(ABC):
22+
2423
@abstractmethod
25-
def get_key(self,
26-
key_id, # type: str
27-
) -> Key:
24+
def get_key(self, key_id: str) -> Key:
2825
"""Returns requested key
2926
3027
Args:
31-
keyid (str): Key ID to retrieve
28+
key_id (str): Key ID to retrieve
3229
3330
Returns:
3431
:class:`~couchbase.encryption.Key`: The corresponding :class:`~couchbase.encryption.Key`

0 commit comments

Comments
 (0)