Skip to content

Commit

Permalink
Secure Cell passphrase API: PyThemis (#596)
Browse files Browse the repository at this point in the history
* SCell passphrase API in PyThemis

Add support for passphrase API by extending SCellSeal construction API.
It is possible to add via Python's __new__ special method which may
return an instance of a diffrent class (typically, more appropriate
subclass). We introduce a SCellSealPassphrase subclass with the same API
but different implementation of "encrypt" and "decrypt" methods.

To make things easier we also add a SecureCellError to provide more
contexts in exceptions without making error messages too long.

Initially a different syntax was planned:

    SCellSeal.with_passphrase('a secret')

    SCellSeal.with_key(binary_master_key)

It had a noble idea of maintaining similarity with other languages which
do not have named arguments. However, after updating tests and examples
this syntax came off as unnatural and non-Pythonic.

* Tests for passphrase API

Those are mostly straighforward. Also, update existing tests for master
key API and ensure that positional arguments to Secure Cell constuctors
are interpreted as keys, not passphrases.

Another change is update of tests to use a proper master key instead of
a fixed password with master key API.

* Integration tool for passphrase API

* Update Secure Cell code examples

The main showcase has been reworked completely to demonstrate API.
Other examples got cosmetic updates to avoid using 'passwords' when in
fact they work with master keys.

* Optional "encoding=" argument for passphrases

Instead of forcing the users to do '.encode(...)' calls themselves,
adhere to standard Python practice of accepting an "encoding" argument
with specified default encoding.

* Use "six" library for Python 2/3 compatibility

Instead of writing type hacks, use a compatibility library. This should
make it easier to convert PyThemis into Python 3-only when we decide to
drop Python 2 compatibility. We will need to replace all "six" calls
with native equivalents.

* Raise warnings for possible misuse of master key API

Raise warnings instead of exceptions when we suspect that master key API
is misused with strings. Since we're not introducing a new API, it would
be rude to break Existing Code with unannounced exceptions. Howerver, we
cannot pass on the possible misuse either.

Master key API should not be used with strings. Currently, if you pass a
Unicode string, it will be encoded in some internal encoding (usually
UTF-8, but this may not be the case on Windows). Master key API should
not be used with human-readable strings due to security concerns.

Produce a warning when we see master key API to be used with types that
we previously allowed but no longer recommend. Provide suggestions on
what API should be used instead.

This warns the user of possible misuse (if they pay attention to
warnings) and does not break production code, which will keep the old
behavior of misusing strings as master keys.
  • Loading branch information
ilammy authored Mar 18, 2020
1 parent 9e94976 commit 1949e65
Show file tree
Hide file tree
Showing 9 changed files with 363 additions and 44 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,20 @@ _Code:_

- Fixed compatibility issues on 32-bit platforms ([#555](https://github.com/cossacklabs/themis/pull/555)).
- New function `skeygen.GenerateSymmetricKey()` can be used to generate symmetric keys for Secure Cell ([#561](https://github.com/cossacklabs/themis/pull/561)).
- PyThemis now supports _passphrase API_ of Secure Cell in Seal mode ([#596](https://github.com/cossacklabs/themis/pull/596)).

```python
from pythemis.scell import SCellSeal

cell = SCellSeal(passphrase='my passphrase')

encrypted = cell.encrypt(b'message data')
decrypted = cell.decrypt(encrypted)
```

You can safely and securely use human-readable passphrases as strings with this new API.

Existing master key API (`SCellSeal(key=...)`) does not support passphrases. You should use it with symmetric encryption keys, such as generated by `GenerateSymmetricKey()` ([#561](https://github.com/cossacklabs/themis/pull/561)).

- **Ruby**

Expand Down
76 changes: 43 additions & 33 deletions docs/examples/python/scell_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,58 +5,68 @@
from pythemis.scell import SCellContextImprint

message = "i'm plain text message"
passwrd = b"pass"
context = b"somecontext"
master_key = base64.b64decode("bm8sIHRoaXMgaXMgbm90IGEgdmFsaWQgbWFzdGVyIGtleQ==")
passphrase = b"secret passphrase"


print("running secure cell in seal mode...")
print("# Secure Cell in Seal mode\n")

scell = SCellSeal(passwrd)
print("## Master key API\n")

print("encrypting...")
encrypted_message = scell.encrypt(message.encode('utf-8'))
encrypted_message_string = base64.b64encode(encrypted_message)
print(encrypted_message_string)
scellMK = SCellSeal(key=master_key)

print("decrypting from binary... --> ")
decrypted_message = scell.decrypt(encrypted_message).decode('utf-8')
print(decrypted_message)
encrypted_message = scellMK.encrypt(message.encode('utf-8'))
print("Encrypted: " + base64.b64encode(encrypted_message).decode('ascii'))

decrypted_message = scellMK.decrypt(encrypted_message).decode('utf-8')
print("Decrypted: " + decrypted_message)

print("decrypting from string... --> ")
# check https://themis.cossacklabs.com/data-simulator/cell/
encrypted_message_string = "AAEBQAwAAAAQAAAADQAAACoEM9MbJzEu2RDuRoGzcQgN4jchys0q+LLcsbfUDV3M2eg/FhygH1ns"
# Visit https://docs.cossacklabs.com/simulator/data-cell/
print("")
encrypted_message_string = "AAEBQAwAAAAQAAAAEQAAAC0fCd2mOIxlDUORXz8+qCKuHCXcDii4bMF8OjOCOqsKEdV4+Ga2xTHPMupFvg=="
decrypted_message_from_string = base64.b64decode(encrypted_message_string)
decrypted_message = scell.decrypt(decrypted_message_from_string).decode('utf-8')
print(decrypted_message)
decrypted_message = scellMK.decrypt(decrypted_message_from_string).decode('utf-8')
print("Decrypted (simulator): " + decrypted_message)

print("")

print("----------------------")
print("running secure cell in token protect mode...")

scellTP = SCellTokenProtect(passwrd)
print("## Passphrase API\n")

print("encrypting...")
encrypted_message, additional_auth_data = scellTP.encrypt(message.encode('utf-8'))
encrypted_message_string = base64.b64encode(encrypted_message)
print(encrypted_message_string)
scellPW = SCellSeal(passphrase=passphrase)

print("decrypting from binary... --> ")
decrypted_message = scellTP.decrypt(encrypted_message, additional_auth_data).decode('utf-8')
print(decrypted_message)
encrypted_message = scellPW.encrypt(message.encode('utf-8'))
print("Encrypted: " + base64.b64encode(encrypted_message).decode('ascii'))

decrypted_message = scellPW.decrypt(encrypted_message).decode('utf-8')
print("Decrypted: " + decrypted_message)

print("----------------------")
print("running secure cell in context imprint mode...")
print("")


scellCI = SCellContextImprint(passwrd)
print("# Secure Cell in Token Protect mode\n")

scellTP = SCellTokenProtect(key=master_key)

encrypted_message, auth_token = scellTP.encrypt(message.encode('utf-8'))
print("Encrypted: " + base64.b64encode(encrypted_message).decode('ascii'))
print("Auth token: " + base64.b64encode(auth_token).decode('ascii'))

decrypted_message = scellTP.decrypt(encrypted_message, auth_token).decode('utf-8')
print("Decrypted: " + decrypted_message)

print("")


print("# Secure Cell in Context Imprint mode\n")

scellCI = SCellContextImprint(key=master_key)

print("encrypting...")
encrypted_message = scellCI.encrypt(message.encode('utf-8'), context)
encrypted_message_string = base64.b64encode(encrypted_message)
print(encrypted_message_string)
print("Encrypted: " + base64.b64encode(encrypted_message).decode('ascii'))

print("decrypting from binary... --> ")
decrypted_message = scellCI.decrypt(encrypted_message, context).decode('utf-8')
print(decrypted_message)
print("Decrypted: " + decrypted_message)

print("")
6 changes: 3 additions & 3 deletions docs/examples/python/scell_test_postgres.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import psycopg2.extras
from pythemis import scell

password = b"password"
master_key = base64.b64decode(b'c2NlbGxfeG1sX2Zvcm1hdC1wcmVzZXJ2aW5nX2VuY3J5cHRpb24ucHk=')

CREATE_SCELL_DATA_TABLE_SQL = ("CREATE TABLE IF NOT EXISTS scell_data ("
"id serial PRIMARY KEY, num bytea, data bytea);")
Expand All @@ -44,7 +44,7 @@ def init_table(connection):


def add_record(connection, field1, field2):
encryptor = scell.SCellTokenProtect(password)
encryptor = scell.SCellTokenProtect(master_key)
# encrypt field1
encrypted_field1, field1_auth_data = encryptor.encrypt(
field1.encode('utf-8'))
Expand Down Expand Up @@ -72,7 +72,7 @@ def add_record(connection, field1, field2):

def get_record(connection, id):
# retrieve record from db by id
dec = scell.SCellTokenProtect(password)
dec = scell.SCellTokenProtect(master_key)
with connection.cursor() as cursor:
cursor.execute(
"SELECT * FROM scell_data "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import base64
from pythemis import scell

password = b"password"
master_key = base64.b64decode(b'c2NlbGxfeG1sX2Zvcm1hdC1wcmVzZXJ2aW5nX2VuY3J5cHRpb24ucHk=')


def encrypt_children(node, context):
Expand Down Expand Up @@ -52,7 +52,7 @@ def decrypt_children(node, context):
# encoding file 'example_data/test.xml' and save result to encoded_data.xml
tree = ET.parse('example_data/test.xml')
root = tree.getroot()
encryptor = scell.SCellSeal(password)
encryptor = scell.SCellSeal(master_key)
encrypt_children(root, "")
tree.write("encoded_data.xml")

Expand Down
1 change: 1 addition & 0 deletions src/wrappers/themis/python/pythemis/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
class THEMIS_CODES(IntEnum):
NETWORK_ERROR = 2222
BUFFER_TOO_SMALL = 14
INVALID_PARAMETER = 12
FAIL = 11
SUCCESS = 0
SEND_AS_IS = 1
Expand Down
127 changes: 127 additions & 0 deletions src/wrappers/themis/python/pythemis/scell.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#

import six
import warnings

from ctypes import cdll, c_int, byref, create_string_buffer, string_at
from ctypes.util import find_library

Expand All @@ -24,11 +27,43 @@
themis = cdll.LoadLibrary(find_library('themis'))


class SecureCellError(ThemisError):
def __init__(self, message, error_code=THEMIS_CODES.INVALID_PARAMETER):
message = 'Secure Cell: ' + message
super(SecureCellError, self).__init__(error_code, message)


class SCellSeal(object):
def __new__(cls, key=None, passphrase=None, **kwargs):
"""
Make a new Secure Cell in Seal mode.
You must specify either key= or passphrase= keyword argument.
:type key: bytes
:type passphrase: Union(str, bytes)
"""
if key is None and passphrase is None:
raise SecureCellError('missing key or passphrase')
if key is not None and passphrase is not None:
raise SecureCellError('key and passphrase cannot be specified at the same time')
if key is not None:
return object.__new__(SCellSeal)
if passphrase is not None:
return object.__new__(SCellSealPassphrase)

def __init__(self, key):
"""
Make a new Secure Cell in Seal mode with a master key.
:type key: bytes
"""
if not key:
raise ThemisError(THEMIS_CODES.FAIL,
"Secure Cell (Seal) failed creating")
if not isinstance(key, six.binary_type):
warnings.warn('master key should be "bytes", '
'consider using "passphrase=" API with strings')
self.key = key

def encrypt(self, message, context=None):
Expand Down Expand Up @@ -70,11 +105,100 @@ def decrypt(self, message, context=None):
return string_at(decrypted_message, decrypted_message_length.value)


class SCellSealPassphrase(SCellSeal):
def __new__(cls, passphrase, **kwargs):
"""
Make a new Secure Cell in Seal mode with a passphrase.
:type passphrase: Union(str, bytes)
"""
return object.__new__(SCellSealPassphrase)

def __init__(self, passphrase, encoding='utf-8'):
"""
Make a new Secure Cell in Seal mode with a passphrase.
:type passphrase: Union(str, bytes)
"""
if not passphrase:
raise SecureCellError('passphrase cannot be empty')
if isinstance(passphrase, six.text_type):
passphrase = passphrase.encode(encoding)
elif isinstance(passphrase, six.binary_type):
pass
else:
raise SecureCellError('passphrase must be either "unicode" or "bytes"')
self.passphrase = passphrase

def encrypt(self, message, context=None):
"""
Encrypt given message with optional context.
:type message: bytes
:type context: bytes
:returns bytes
"""
context_length = len(context) if context else 0
encrypted_message_length = c_int(0)

res = themis.themis_secure_cell_encrypt_seal_with_passphrase(
self.passphrase, len(self.passphrase),
context, context_length,
message, len(message),
None, byref(encrypted_message_length))
if res != THEMIS_CODES.BUFFER_TOO_SMALL:
raise SecureCellError("encryption failed", error_code=res)

encrypted_message = create_string_buffer(encrypted_message_length.value)
res = themis.themis_secure_cell_encrypt_seal_with_passphrase(
self.passphrase, len(self.passphrase),
context, context_length,
message, len(message),
encrypted_message, byref(encrypted_message_length))
if res != THEMIS_CODES.SUCCESS:
raise SecureCellError("encryption failed", error_code=res)

return string_at(encrypted_message, encrypted_message_length.value)

def decrypt(self, message, context=None):
"""
Decrypt given message with optional context.
:type message: bytes
:type context: bytes
:returns bytes
"""
context_length = len(context) if context else 0
decrypted_message_length = c_int(0)

res = themis.themis_secure_cell_decrypt_seal_with_passphrase(
self.passphrase, len(self.passphrase),
context, context_length,
message, len(message),
None, byref(decrypted_message_length))
if res != THEMIS_CODES.BUFFER_TOO_SMALL:
raise SecureCellError("decryption failed", error_code=res)

decrypted_message = create_string_buffer(decrypted_message_length.value)
res = themis.themis_secure_cell_decrypt_seal_with_passphrase(
self.passphrase, len(self.passphrase),
context, context_length,
message, len(message),
decrypted_message, byref(decrypted_message_length))
if res != THEMIS_CODES.SUCCESS:
raise SecureCellError("decryption failed", error_code=res)

return string_at(decrypted_message, decrypted_message_length.value)


class SCellTokenProtect(object):
def __init__(self, key):
if not key:
raise ThemisError(THEMIS_CODES.FAIL,
"Secure Cell (Token Protect) failed creating")
if not isinstance(key, six.binary_type):
warnings.warn('master key should be "bytes", '
'consider using skeygen.GenerateSymmetricKey()')
self.key = key

def encrypt(self, message, context=None):
Expand Down Expand Up @@ -124,6 +248,9 @@ def __init__(self, key):
if not key:
raise ThemisError(THEMIS_CODES.FAIL,
"Secure Cell (Context Imprint) failed creating")
if not isinstance(key, six.binary_type):
warnings.warn('master key should be "bytes", '
'consider using skeygen.GenerateSymmetricKey()')
self.key = key

def encrypt(self, message, context):
Expand Down
2 changes: 1 addition & 1 deletion src/wrappers/themis/python/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from setuptools import setup


install_requires = []
install_requires = ['six']
if sys.version_info < (3, 4):
install_requires.append('enum34')

Expand Down
Loading

0 comments on commit 1949e65

Please sign in to comment.