blob: 2ce0fface29d7cf18e892f7f8e791e65fbe6bc76 [file] [log] [blame]
#!/usr/bin/env python
# Copyright 2016 The Fuchsia Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import base64
from cryptography.fernet import Fernet
from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA
import os
import sys
import file_util
# The names of the files in the cache directory where the analyzer's public
# and private key will be stored.
ANALYZER_PUBLIC_KEY_FILE = "analyzer_public_key.pem"
ANALYZER_PRIVATE_KEY_FILE = "analyzer_private_key.pem"
class CryptoHelper(object):
""" An object to assist in performing hybrid encryption/decription in
the Cobalt prototype pipeline. Data is encrypted by the randomizers using
symmetric, authenticated, non-determinsitic encryption, and then the
symmetric key is encrypted using the public key of the analyzer. The
sealed envelope consisting of the pair (ciphertext, encrypted_key) is sent to
the shuffler which shuffles the sealed envelopes while being unable to
decrypt the data. The shuffler forwards the shuffled data to the analyzer
which decrypts the symmetric key using its private key and then uses
the symmetric key to decrypt the paylod.
A new symmetric key is generated for each encryption. The analyzer's public/
private key pair is generated once per installation
and cached in the |cache| directory. If either key file is deleted then
both keys will be regenerated.
The public key encryption uses the RSA encryption protocol according to
PKCS#1 OAEP.
The symmetric authenticated encryption uses AES 128 in CBC mode with HMAC,
as implemented by the Fernet library from cryptography.io. Fernet uses
encrypt-then-mac authenticated encryption.
See https://github.com/fernet/spec/blob/master/Spec.md.
Example:
# In a randomizer:
ch = CryptoHelper()
ciphertext1, encrypted_key1 = ch.encryptForSendingToAnalyzer("Hello world!")
ciphertext2, encrypted_key2 = ch.encryptForSendingToAnalyzer("Goodbye world!")
# Later in an analyzer:
ch = CryptoHelper()
# prints "Hello world!"
print(ch.decryptOnAnalyzer(ciphertext1, encrypted_key1))
# prints "Goodbye world!"
print(ch.decryptOnAnalyzer(ciphertext2, encrypted_key2))
The ciphertexts and encrypted keys are base64 encoded so they may be
conveniently included in a human-readable file.
"""
def __init__(self):
public_key_file = os.path.join(file_util.CACHE_DIR,
ANALYZER_PUBLIC_KEY_FILE)
private_key_file = os.path.join(file_util.CACHE_DIR,
ANALYZER_PRIVATE_KEY_FILE)
if (not os.path.exists(public_key_file)
or not os.path.exists(private_key_file)):
try:
os.remove(public_key_file)
except:
pass
try:
os.remove(private_key_file)
except:
pass
self._generateAnalyzerKeyPair()
self._analyzer_public_key_cipher = PKCS1_OAEP.new(self._readKey(
public_key_file))
self._analyzer_private_key_cipher = PKCS1_OAEP.new(self._readKey(
private_key_file))
def _generateAnalyzerKeyPair(self):
""" Generates a public/private key pair for the analyzer and writes
the keys into two files in the |cache| directory.
"""
new_key = RSA.generate(2048)
with file_util.openFileForWriting(ANALYZER_PUBLIC_KEY_FILE,
file_util.CACHE_DIR) as f1:
f1.write(new_key.publickey().exportKey("PEM"))
with file_util.openFileForWriting(ANALYZER_PRIVATE_KEY_FILE,
file_util.CACHE_DIR) as f2:
f2.write(new_key.exportKey("PEM"))
def _readKey(self, file_name):
""" Reads and returns a key from the specified file.
Args:
file_name {string} The full path of a key file to read.
Returns:
An RSA key object based on the data read from the file.
"""
with open(file_name) as f:
return RSA.importKey(f.read())
def encryptForSendingToAnalyzer(self, plaintext):
"""Encrypts plaintext for sending to the analyzer.
A new symmetric key is generated and then the plaintext is encrypted
into a ciphertext using this key. Then the symmetric key is encrypted
using the analyzer's public key. The pair consisting of the ciphertext
and the encrypted symmetric key is returned.
This function should be invoked from a randomizer.
Args:
plaintext {string} The plain text to encrypt.
Returns:
{pair of string} (ciphertext, encrypted_key) Both values are base64
encoded.
"""
symmetric_key = Fernet.generate_key()
symmetric_encryption = Fernet(symmetric_key)
ciphertext = symmetric_encryption.encrypt(plaintext)
encrypted_key = base64.b64encode(self._analyzer_public_key_cipher.encrypt(
symmetric_key))
return (ciphertext, encrypted_key)
def decryptOnAnalyzer(self, ciphtertext, encrypted_key):
""" Decrypts the ciphertext on the analyzer.
First the encrypted_key is decrypted using the analyzer's private key.
Then the ciphertext is decrypted using the decrypted symmetric key.
This function should be invoked from an analyzer.
Args:
ciphertext: {string} Ciphertext to decrypt
encrypted_key: {string} The encryption of the symmetric key.
Returns:
{string} The decrypted plaintext.
"""
symmetric_key = self._analyzer_private_key_cipher.decrypt(
base64.b64decode(encrypted_key))
symmetric_encryption = Fernet(symmetric_key)
return symmetric_encryption.decrypt(ciphtertext)
def main():
# This main() function is a manual test of the code in this file.
ch = CryptoHelper()
for i in xrange(10):
plain_text = "message number %i" % i
cipher_text, encrypted_key = ch.encryptForSendingToAnalyzer(plain_text)
print("_______________________")
print "cipher_text=%s"%cipher_text
print("_______________________")
print "encrypted_key=%s"%encrypted_key
print("_______________________")
print(ch.decryptOnAnalyzer(cipher_text, encrypted_key))
ch = CryptoHelper()
for i in xrange(10):
plain_text = "message number %i" % i
cipher_text, encrypted_key = ch.encryptForSendingToAnalyzer(plain_text)
print(ch.decryptOnAnalyzer(cipher_text, encrypted_key))
plain_text = "A very long message " * 100
cipher_text, encrypted_key = ch.encryptForSendingToAnalyzer(plain_text)
ch = CryptoHelper()
print(ch.decryptOnAnalyzer(cipher_text, encrypted_key))
if __name__ == '__main__':
sys.exit(main())