Skip to content

Commit 48b03e0

Browse files
committed
Merge branch 'feat/encrypt_padding' into 'master'
Feat/encrypt padding See merge request TankerHQ/sdk-python!239
2 parents be9c2c3 + 8794753 commit 48b03e0

5 files changed

Lines changed: 211 additions & 8 deletions

File tree

cffi_defs.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@ struct tanker_encrypt_options
353353
char const* const* share_with_groups;
354354
uint32_t nb_groups;
355355
bool share_with_self;
356+
uint32_t padding_step;
356357
};
357358

358359
struct tanker_sharing_options
@@ -420,7 +421,7 @@ tanker_future_t* tanker_set_verification_method(
420421

421422
tanker_future_t* tanker_get_verification_methods(tanker_t* session);
422423

423-
uint64_t tanker_encrypted_size(uint64_t clear_size);
424+
uint64_t tanker_encrypted_size(uint64_t clear_size, uint32_t padding_step);
424425

425426
tanker_expected_t* tanker_decrypted_size(uint8_t const* encrypted_data,
426427
uint64_t encrypted_size);
@@ -516,7 +517,8 @@ tanker_future_t* tanker_encryption_session_open(
516517
tanker_future_t* tanker_encryption_session_close(
517518
tanker_encryption_session_t* session);
518519

519-
uint64_t tanker_encryption_session_encrypted_size(uint64_t clear_size);
520+
uint64_t tanker_encryption_session_encrypted_size(
521+
tanker_encryption_session_t* session, uint64_t clear_size);
520522

521523
tanker_expected_t* tanker_encryption_session_get_resource_id(
522524
tanker_encryption_session_t* session);

run-ci.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,10 @@ def deploy() -> None:
116116
env["TWINE_USERNAME"] = env["GITLAB_USERNAME"]
117117

118118
wheels_path = Path.cwd() / "dist"
119-
for wheel in wheels_path.glob("tankersdk-*.whl"):
119+
wheels = list(wheels_path.glob("tankersdk-*.whl"))
120+
if len(wheels) == 0:
121+
raise Exception("no wheel found")
122+
for wheel in wheels:
120123
# fmt: off
121124
tankerci.run(
122125
"poetry", "run",

tankersdk/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
EncryptionOptions,
1919
OidcIdTokenVerification,
2020
OidcIdTokenVerificationMethod,
21+
Padding,
2122
PassphraseVerification,
2223
PassphraseVerificationMethod,
2324
PhoneNumberVerification,

tankersdk/tanker.py

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
import os
33
import warnings
44
import weakref
5-
from enum import Enum
6-
from typing import Any, List, Optional, cast
5+
from enum import Enum, IntEnum
6+
from typing import Any, List, Optional, Union, cast
77

88
import typing_extensions
99
from _tanker import ffi
1010
from _tanker import lib as tankerlib
1111

1212
from .error import Error as TankerError
13+
from .error import InvalidArgument
1314
from .ffi_helpers import CCharList, CData, FFIHelpers, OptionalStrList
1415
from .version import __version__
1516

@@ -44,6 +45,13 @@ class Status(Enum):
4445
IDENTITY_VERIFICATION_NEEDED = 3
4546

4647

48+
class Padding(IntEnum):
49+
"""Special constants for the padding_step encryption option"""
50+
51+
AUTO = 0
52+
OFF = 1
53+
54+
4755
class VerificationMethodType(Enum):
4856
"""Types of available methods for identity verification"""
4957

@@ -240,11 +248,24 @@ def __init__(
240248
share_with_users: Optional[List[str]] = None,
241249
share_with_groups: Optional[List[str]] = None,
242250
share_with_self: bool = True,
251+
padding_step: Optional[Union[int, Padding]] = None,
243252
):
244253
self.share_with_users = share_with_users
245254
self.share_with_groups = share_with_groups
246255
self.share_with_self = share_with_self
247256

257+
if not (
258+
padding_step is None
259+
or type(padding_step) is Padding # noqa: W503
260+
or (padding_step >= 2 and type(padding_step) is int) # noqa: W503
261+
):
262+
raise InvalidArgument(
263+
f"Invalid padding step. Got: `{padding_step}`, use "
264+
+ "Padding.{OFF|AUTO} or an integer >= 2 instead." # noqa: W503
265+
)
266+
267+
self.padding_step = padding_step
268+
248269

249270
class CEncryptionOptions:
250271
"""Wraps the tanker_encrypt_options_t C type"""
@@ -255,19 +276,22 @@ def __init__(
255276
share_with_users: OptionalStrList = None,
256277
share_with_groups: OptionalStrList = None,
257278
share_with_self: bool = True,
279+
padding_step: Optional[int] = None,
258280
) -> None:
259281
self.user_list = CCharList(share_with_users, ffi, tankerlib)
260282
self.group_list = CCharList(share_with_groups, ffi, tankerlib)
283+
self.padding_step = ffi.cast("uint32_t", padding_step or Padding.AUTO)
261284

262285
self._c_data = ffi.new(
263286
"tanker_encrypt_options_t *",
264287
{
265-
"version": 3,
288+
"version": 4,
266289
"share_with_users": self.user_list.data,
267290
"nb_users": self.user_list.size,
268291
"share_with_groups": self.group_list.data,
269292
"nb_groups": self.group_list.size,
270293
"share_with_self": share_with_self,
294+
"padding_step": self.padding_step,
271295
},
272296
)
273297

@@ -554,7 +578,9 @@ async def encrypt(self, clear_data: bytes) -> bytes:
554578
"""Encrypt `clear_data` with the session"""
555579
c_clear_buffer = ffihelpers.bytes_to_c_buffer(clear_data) # type: CData
556580
clear_size = len(c_clear_buffer)
557-
size = tankerlib.tanker_encryption_session_encrypted_size(clear_size)
581+
size = tankerlib.tanker_encryption_session_encrypted_size(
582+
self.c_session, clear_size
583+
)
558584
c_encrypted_buffer = ffi.new("uint8_t[%i]" % size)
559585
c_future = tankerlib.tanker_encryption_session_encrypt(
560586
self.c_session, c_encrypted_buffer, c_clear_buffer, clear_size
@@ -785,12 +811,15 @@ async def encrypt(
785811
share_with_users=options.share_with_users,
786812
share_with_groups=options.share_with_groups,
787813
share_with_self=options.share_with_self,
814+
padding_step=options.padding_step,
788815
)
789816
else:
790817
c_encrypt_options = CEncryptionOptions()
791818
c_clear_buffer = ffihelpers.bytes_to_c_buffer(clear_data) # type: CData
792819
clear_size = len(c_clear_buffer)
793-
size = tankerlib.tanker_encrypted_size(clear_size)
820+
size = tankerlib.tanker_encrypted_size(
821+
clear_size, c_encrypt_options.padding_step
822+
)
794823
c_encrypted_buffer = ffi.new("uint8_t[%i]" % size)
795824
c_future = tankerlib.tanker_encrypt(
796825
self.c_tanker,
@@ -835,6 +864,7 @@ async def encrypt_stream(
835864
share_with_users=options.share_with_users,
836865
share_with_groups=options.share_with_groups,
837866
share_with_self=options.share_with_self,
867+
padding_step=options.padding_step,
838868
)
839869
else:
840870
c_encrypt_options = CEncryptionOptions()
@@ -1085,6 +1115,7 @@ async def create_encryption_session(
10851115
share_with_users=options.share_with_users,
10861116
share_with_groups=options.share_with_groups,
10871117
share_with_self=options.share_with_self,
1118+
padding_step=options.padding_step,
10881119
)
10891120
else:
10901121
c_encrypt_options = CEncryptionOptions()

test/test_tanker.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
EmailVerificationMethod,
2222
EncryptionOptions,
2323
OidcIdTokenVerification,
24+
Padding,
2425
PassphraseVerification,
2526
PhoneNumberVerification,
2627
PhoneNumberVerificationMethod,
@@ -405,6 +406,23 @@ async def test_empty_message(self, tmp_path: Path, app: Dict[str, str]) -> None:
405406
assert len(result) == 0
406407
await alice.session.stop()
407408

409+
@pytest.mark.asyncio
410+
async def test_encrypt_with_padding(
411+
self, tmp_path: Path, app: Dict[str, str]
412+
) -> None:
413+
alice = await create_user_session(tmp_path, app)
414+
chunk_size = 1024**2
415+
message = bytearray(
416+
3 * chunk_size + 2
417+
) # three big chunks plus a little something
418+
input_stream = InMemoryAsyncStream(message)
419+
async with await alice.session.encrypt_stream(
420+
input_stream, EncryptionOptions(padding_step=500)
421+
) as encrypted_stream:
422+
encrypted_message = await encrypted_stream.read()
423+
assert len(encrypted_message) == 3146248
424+
await alice.session.stop()
425+
408426

409427
@pytest.mark.asyncio
410428
async def test_encrypt_decrypt(tmp_path: Path, app: Dict[str, str]) -> None:
@@ -426,6 +444,79 @@ async def test_encrypt_decrypt_empty(tmp_path: Path, app: Dict[str, str]) -> Non
426444
await alice.session.stop()
427445

428446

447+
SIMPLE_ENCRYPTION_OVERHEAD = 17
448+
449+
450+
SIMPLE_PADDED_ENCRYPTION_OVERHEAD = SIMPLE_ENCRYPTION_OVERHEAD + 1
451+
452+
453+
@pytest.mark.asyncio
454+
async def test_auto_padding_is_default(tmp_path: Path, app: Dict[str, str]) -> None:
455+
alice = await create_user_session(tmp_path, app)
456+
message = b"my clear data is clear!"
457+
length_with_padme = 24
458+
encrypted_data = await alice.session.encrypt(message)
459+
assert len(encrypted_data) - SIMPLE_PADDED_ENCRYPTION_OVERHEAD == length_with_padme
460+
clear_data = await alice.session.decrypt(encrypted_data)
461+
assert clear_data == message
462+
await alice.session.stop()
463+
464+
465+
@pytest.mark.asyncio
466+
async def test_padding_opt_auto(tmp_path: Path, app: Dict[str, str]) -> None:
467+
alice = await create_user_session(tmp_path, app)
468+
message = b"my clear data is clear!"
469+
length_with_padme = 24
470+
encrypted_data = await alice.session.encrypt(
471+
message, EncryptionOptions(padding_step=Padding.AUTO)
472+
)
473+
assert len(encrypted_data) - SIMPLE_PADDED_ENCRYPTION_OVERHEAD == length_with_padme
474+
clear_data = await alice.session.decrypt(encrypted_data)
475+
assert clear_data == message
476+
await alice.session.stop()
477+
478+
479+
@pytest.mark.asyncio
480+
async def test_padding_opt_disable(tmp_path: Path, app: Dict[str, str]) -> None:
481+
alice = await create_user_session(tmp_path, app)
482+
message = b"I love you"
483+
encrypted_data = await alice.session.encrypt(
484+
message, EncryptionOptions(padding_step=Padding.OFF)
485+
)
486+
assert len(encrypted_data) == len(message) + SIMPLE_ENCRYPTION_OVERHEAD
487+
clear_data = await alice.session.decrypt(encrypted_data)
488+
assert clear_data == message
489+
await alice.session.stop()
490+
491+
492+
@pytest.mark.asyncio
493+
async def test_padding_opt_enable(tmp_path: Path, app: Dict[str, str]) -> None:
494+
alice = await create_user_session(tmp_path, app)
495+
message = b"I love you"
496+
step = 13
497+
encrypted_data = await alice.session.encrypt(
498+
message, EncryptionOptions(padding_step=step)
499+
)
500+
assert (len(encrypted_data) - SIMPLE_PADDED_ENCRYPTION_OVERHEAD) % step == 0
501+
clear_data = await alice.session.decrypt(encrypted_data)
502+
assert clear_data == message
503+
await alice.session.stop()
504+
505+
506+
def test_padding_opt_error(tmp_path: Path, app: Dict[str, str]) -> None:
507+
with pytest.raises(error.InvalidArgument):
508+
EncryptionOptions(padding_step=0)
509+
510+
with pytest.raises(error.InvalidArgument):
511+
EncryptionOptions(padding_step=1)
512+
513+
with pytest.raises(error.InvalidArgument):
514+
EncryptionOptions(padding_step=-1)
515+
516+
with pytest.raises(error.InvalidArgument):
517+
EncryptionOptions(padding_step=2.42) # type: ignore
518+
519+
429520
@pytest.mark.asyncio
430521
async def test_share_during_encrypt(tmp_path: Path, app: Dict[str, str]) -> None:
431522
alice = await create_user_session(tmp_path, app)
@@ -593,6 +684,81 @@ async def test_share_with_encryption_session_without_self(
593684
await bob.session.stop()
594685

595686

687+
ENCRYPTION_SESSION_OVERHEAD = 57
688+
689+
690+
ENCRYPTION_SESSION_PADDED_OVERHEAD = ENCRYPTION_SESSION_OVERHEAD + 1
691+
692+
693+
@pytest.mark.asyncio
694+
async def test_encryption_session_auto_padding_by_default(
695+
tmp_path: Path, app: Dict[str, str]
696+
) -> None:
697+
alice = await create_user_session(tmp_path, app)
698+
message = b"my clear data is clear!"
699+
length_with_padme = 24
700+
enc_session = await alice.session.create_encryption_session()
701+
encrypted = await enc_session.encrypt(message)
702+
assert len(encrypted) - ENCRYPTION_SESSION_PADDED_OVERHEAD == length_with_padme
703+
704+
decrypted = await alice.session.decrypt(encrypted)
705+
assert decrypted == message
706+
await alice.session.stop()
707+
708+
709+
@pytest.mark.asyncio
710+
async def test_encryption_session_auto_padding(
711+
tmp_path: Path, app: Dict[str, str]
712+
) -> None:
713+
alice = await create_user_session(tmp_path, app)
714+
message = b"my clear data is clear!"
715+
length_with_padme = 24
716+
enc_session = await alice.session.create_encryption_session(
717+
EncryptionOptions(padding_step=Padding.AUTO)
718+
)
719+
encrypted = await enc_session.encrypt(message)
720+
assert len(encrypted) - ENCRYPTION_SESSION_PADDED_OVERHEAD == length_with_padme
721+
722+
decrypted = await alice.session.decrypt(encrypted)
723+
assert decrypted == message
724+
await alice.session.stop()
725+
726+
727+
@pytest.mark.asyncio
728+
async def test_encryption_session_no_padding(
729+
tmp_path: Path, app: Dict[str, str]
730+
) -> None:
731+
alice = await create_user_session(tmp_path, app)
732+
message = b"Ceci n'est pas un test"
733+
enc_session = await alice.session.create_encryption_session(
734+
EncryptionOptions(padding_step=Padding.OFF)
735+
)
736+
encrypted = await enc_session.encrypt(message)
737+
assert len(encrypted) - ENCRYPTION_SESSION_OVERHEAD == len(message)
738+
739+
decrypted = await alice.session.decrypt(encrypted)
740+
assert decrypted == message
741+
await alice.session.stop()
742+
743+
744+
@pytest.mark.asyncio
745+
async def test_encryption_session_padding_step(
746+
tmp_path: Path, app: Dict[str, str]
747+
) -> None:
748+
alice = await create_user_session(tmp_path, app)
749+
message = b"Ceci n'est pas un test"
750+
step = 13
751+
enc_session = await alice.session.create_encryption_session(
752+
EncryptionOptions(padding_step=step)
753+
)
754+
encrypted = await enc_session.encrypt(message)
755+
assert (len(encrypted) - ENCRYPTION_SESSION_PADDED_OVERHEAD) % step == 0
756+
757+
decrypted = await alice.session.decrypt(encrypted)
758+
assert decrypted == message
759+
await alice.session.stop()
760+
761+
596762
@pytest.mark.asyncio
597763
async def test_encryption_session_streams(tmp_path: Path, app: Dict[str, str]) -> None:
598764
alice = await create_user_session(tmp_path, app)

0 commit comments

Comments
 (0)