python-dirbtuves / internet-voting

Internet voting system.
1 stars 2 forks source link

Function to put content to mix-net envelope #6

Open sirex opened 9 years ago

sirex commented 9 years ago

For crypto part use https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/

Here is how this function could look:

envelope = create_envelope(nodes, content)

nodes should be list of tuples tuple has two elements: node's IP address and public key.

content should be any JSON serializable object.

Returned envelope can be deserialized with primary keys of nodes in reverse order.

sirex commented 9 years ago

The task is to read more about crypthogrphy.io.

strazdas commented 9 years ago

RSA can only encrypt data blocks that are shorter than the key length.

Usually, RSA is only used to transfer a symmetric key (at the start of the stream for example) and then the bulk data is encrypted with that key.

Asymmetric encryption isn't efficient enough to transfer a lot of data. http://security.stackexchange.com/a/33445

sirex commented 9 years ago

Padding is a way to take data that may or may not be a multiple of the block size for a cipher and extend it out so that it is. This is required for many block cipher modes as they require the data to be encrypted to be an exact multiple of the block size.

https://cryptography.io/en/latest/hazmat/primitives/padding/

sirex commented 9 years ago

Very well explained what is RSA: http://unix.stackexchange.com/a/12265

It looks like we have to use RSA just to encrypt asymmetric key, and the whole message has to be encrypted using that asymmetric key. See https://cryptography.io/en/latest/fernet/

sirex commented 9 years ago

I just read more about PKI and yes, asymmetric algorithms usually are used only for symmetric key exchange. Then, rest of the encryption is done by symmetric algorithms using that exchanged key.

So your task will bet to encrypt message multiple time with list of given symmetric keys, using Fernet protocol. And the key exchange protocol is a different task.

sirex commented 9 years ago

After thinking more about this, I figured out, that you still need public keys. So the API stays the same:

envelope = create_envelope(nodes, content)

Here nodes is a list of tuples, each tuple has mix-net node id (an int) and public key instance.

When creating multi-layered envelope, for each layer you need a JSON list with two elements:

  1. Fernet key encoded with node's public key.
  2. The message encoded with Fernet key. Message should be JSON list with two elements: 1) The inner envelope. 2) Next node Id (int). Node Id of first layer should be None.

The envelope could look something like this (pseudocode):

envelope = json([
    json([
        node[1].public_key.encrypt(node[0].fernet_key),
        node[1].fernet_key.encrypt(json([
            json([
                node[0].public_key.encrypt(node[0].fernet_key),
                node[0].fernet_key.encrypt(message),
            ]),
            node[0].id,

        ]))
    ]),
    node[1].id,
])

Here node[0] would be CEC.

For decoding envelope this API can be used:

content, next_node_id = decrypt_envelope(primary_key, envelope)

Here is what it should do:

  1. Deserialize envelope using json.loads.
  2. Decrypt first element, using primary_key to get Fernet key.
  3. Decrypt second element using decrypted Fernet key.
  4. Deserialize JSON to get content (inner envelope) and next_node_id (address).

For the serialization, I'm not sure if JSON is a good choice, since we will be dealing with binary data. Maybe we should use MessagePack instead of JSON?

sirex commented 9 years ago

Today decided to look into crypto things, and assembled set of functions, that should cover most of crypto needs. It would be good, if you wood put into some module and use these functions for your task.

import os
import json
import base64
import hashlib
import binascii

from cryptography.fernet import Fernet
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.asymmetric import padding as asymmetric_padding

def generate(size: int=4096):
    return rsa.generate_private_key(
        public_exponent=65537,
        key_size=size,
        backend=default_backend(),
    )

def dump_base64(data: bytes) -> str:
    return base64.encodebytes(data).replace(b'\n', b'').decode('ascii')

def load_base64(data: str) -> bytes:
    return base64.decodebytes(data.encode('ascii'))

def dump_private_key(private_key, password: str=None) -> str:
    if password is None:
        encryption_algorithm = serialization.NoEncryption()
    else:
        encryption_algorithm = serialization.BestAvailableEncryption(password.encode('utf-8'))

    private_key_bytes = private_key.private_bytes(
        encoding=serialization.Encoding.DER,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=encryption_algorithm,
    )

    return dump_base64(private_key_bytes)

def load_private_key(private_key_data: str, password: str=None):
    return serialization.load_der_private_key(
        load_base64(private_key_data),
        password=password.encode('utf-8'),
        backend=default_backend()
    )

def dump_public_key(public_key) -> str:
    public_key_bytes = public_key.public_bytes(
        encoding=serialization.Encoding.DER,
        format=serialization.PublicFormat.SubjectPublicKeyInfo,
    )
    return dump_base64(public_key_bytes)

def load_public_key(public_key_data: str):
    return serialization.load_der_public_key(
        load_base64(public_key_data),
        backend=default_backend(),
    )

def public_key_fingerprint(public_key) -> str:
    public_key_bytes = public_key.public_bytes(
        encoding=serialization.Encoding.DER,
        format=serialization.PublicFormat.SubjectPublicKeyInfo,
    )
    return hashlib.sha1(public_key_bytes).hexdigest()

def encrypt(public_key, message: str) -> (str, str):
    key = Fernet.generate_key()
    fernet = Fernet(key)
    cipher = fernet.encrypt(message.encode('utf-8'))
    key = public_key.encrypt(key, asymmetric_padding.OAEP(
        mgf=asymmetric_padding.MGF1(algorithm=hashes.SHA1()),
        algorithm=hashes.SHA1(),
        label=None,
    ))
    return dump_base64(key), cipher.decode('ascii')

def decrypt(private_key, key: str, cipher: str) -> str:
    key = private_key.decrypt(load_base64(key), asymmetric_padding.OAEP(
        mgf=asymmetric_padding.MGF1(algorithm=hashes.SHA1()),
        algorithm=hashes.SHA1(),
        label=None,
    ))
    fernet = Fernet(key)
    message = fernet.decrypt(cipher.encode('ascii'))
    return message.decode('utf-8')

def sign(private_key, message: str) -> str:
    signer = private_key.signer(
        asymmetric_padding.PSS(
            mgf=asymmetric_padding.MGF1(hashes.SHA256()),
            salt_length=asymmetric_padding.PSS.MAX_LENGTH
        ),
        hashes.SHA256()
    )
    signer.update(message.encode('utf-8'))
    return dump_base64(signer.finalize())

def verify(public_key, message: str, signature: str) -> bool:
    verifier = public_key.verifier(
        load_base64(signature),
        asymmetric_padding.PSS(
            mgf=asymmetric_padding.MGF1(hashes.SHA256()),
            salt_length=asymmetric_padding.PSS.MAX_LENGTH
        ),
        hashes.SHA256()
    )
    verifier.update(message.encode('utf-8'))

    try:
        verifier.verify()
    except InvalidSignature:
        return False
    else:
        return True

def get_random_bytes(n: int) -> bytes:
    return os.urandom(n)

def apply_hash_function(message: str, salt: bytes) -> str:
    hash_bytes = hashlib.pbkdf2_hmac('sha1', message.encode('utf-8'), salt, 1000000)
    return binascii.hexlify(hash_bytes).decode('utf-8')

def password_to_fernet_key(password: str, salt: bytes) -> bytes:
    key = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 1000000)
    return base64.urlsafe_b64encode(key)

def fernet_encrypt(password: str, salt: bytes, data: str) -> bytes:
    fernet = Fernet(password_to_fernet_key(password, salt))
    return base64.urlsafe_b64decode(fernet.encrypt(data.encode('utf-8')).decode('ascii'))

def fernet_decrypt(password: str, salt: bytes, cipher: str) -> str:
    fernet = Fernet(password_to_fernet_key(password, salt))
    return fernet.decrypt(base64.urlsafe_b64encode(cipher)).decode('utf-8')

def dump_sensitive_data(password: str, data) -> bytes:
    salt = get_random_bytes(32)
    cipher = fernet_encrypt(password, salt, json.dumps(data))
    return salt + cipher

def load_sensitive_data(password: str, cipher: bytes):
    return json.loads(fernet_decrypt(password, cipher[:32], cipher[32:]))