Skip to content

Instantly share code, notes, and snippets.

@dschuetz
Created May 31, 2018 12:28
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save dschuetz/2ff54d738041fc888613f925a7708a06 to your computer and use it in GitHub Desktop.
Save dschuetz/2ff54d738041fc888613f925a7708a06 to your computer and use it in GitHub Desktop.
#
# Demonstrating kSecKeyAlgorithmECIESEncryptionCofactorX963SHA256AESGCM
#
# David Schuetz (@DarthNull)
# May 2018
#
# see also: https://darthnull.org/security/2018/05/31/secure-enclave-ecies
#
###############################################################
# The only bits you'll have to mess with:
###############################################################
message = 'The Magic Words are still Squeamish Ossifrage'
bob_pem = '''
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHiG0sllsW2K9uX/Ey1nxJsv4u/1z
28JgocZcuFcmE/BuKXZ1w5CB35VxrYqF6RKUucnaauk4VfjSAfYr6gC+GA==
-----END PUBLIC KEY-----'''
###############################################################
# "kSecKeyAlgorithmECIESEncryptionCofactorX963SHA256AESGCM"
#
# This is the Apple algorithm name for:
#
# * Elliptic Curve Integrated Encryption Scheme
# * Used for Encryption
# * Elliptic Curve Diffie-Hellman Key agreement system with Cofactor
# * X963 Key Derivation Function using SHA256 to "improve" the resultant key
# * AES-GCM using the final key to encrypt the provided plaintext
# The script is specifically written to encrypt messages for a macOS demo
# app using Secure Enclave encryption on TouchBar-enabled MacBook Pros.
# But theoretically, it should work with anything using *exactly this*
# set of algorithm choices.
#
# The demo app I'm targeting is here:
# https://github.com/agens-no/EllipticCurveKeyPair
# and I'm using the pyca/cryptography "hazmat" (love the name) libraries
# at cryptography.io.
# Unfortunately, the Apple documentation for this is fairly spotty. While
# searching through the opensource.apple.com repositories, I eventually
# hit upon this comment:
#
# @constant kSecKeyAlgorithmECIESEncryptionCofactorX963SHA256AESGCM
# Legacy ECIES encryption or decryption, use
# kSecKeyAlgorithmECIESEncryptionCofactorVariableIVX963SHA256AESGCM
# in new code.
# Encryption is done using AES-GCM with key negotiated by
# kSecKeyAlgorithmECDHKeyExchangeCofactorX963SHA256. AES Key size
# is 128bit for EC keys <=256bit and 256bit for bigger EC keys.
# Ephemeral public key data is used as sharedInfo for KDF,
# and static public key data is used as authenticationData for
# AES-GCM processing. AES-GCM uses 16 bytes long TAG and
# all-zero 16 byte long IV (initialization vector).
# However, even this seems to be incorrect, as the AES-GCM encryption is not
# using anything for the Additional Authentication Data (AAD).
# It also says to stop using this algorithm, and start using a different one
# instead. From what I've been able to guess, it seems like it's the same
# algorithm, only instead of extracting 128-bits as the symmetric
# encryption key (or 256-bits for larger EC keys), it extracts the key,
# plus an additional 16-bytes to be used as an IV. That should be reasonably
# easy to change, but I haven't tried doing it yet.
# The basic process is this:
#
# * Create a public/private keypair using the demo app (this is "Bob")
# * the private key is stored in the Mac's Secure Enclave
# * click on "encryption" to display the public key in PEM format
#
# * Load Bob's public key into the app below (we're, predictably, "Alice")
#
# * In the demo app, just "Encrypt" the provided sample text
# * (we're doing this to put it in the mode to then decrypt something)
#
# * Set the message in this script and run it
#
# * Copy the final Base64-encrypted text into the macOS demo app
# (you may need to hit backspace to put the cursor at the end of the data)
#
# * hit "Decrypt" and, hopefully, you'll see your message
# What is this doing?
#
# * Create an ephemeral public/private pair
# * by looking at Bob's public key, I saw it was using the prime256v1 curve
# * this is also called SECP256R1
# * create a public / private keypair for Alice, unique to this message
#
# * Using Bob's public and Alice's private keys, use ECDH to create a
# unique shared key
# * the "Cofactor" that gets applied is, I believe, conveniently "1" for
# this curve. So I don't yet know for certain whether it's actually
# being properly generated by this library, or if I'm just lucky enough
# that it's working by default.
#
# * Now, use the X963 KDF, using SHA-256 as the chosen hash, and using
# Alice's (ephemeral) public key data as "Shared Information," to
# generate the symmetric encryption key.
# * We extract the first 16 bytes (128-bits) because our EC key is
# less than 256 bits. If it were >= 256 bits, we'd extract 32 bytes
# for a full 256-bit AES symmetric key.
# * If this had been a VariableIV variant, then we'd extract another
# 128 bits to use as an IV. Instead we'll use 16 bytes of 0 as IV.
#
# * Encrypt the message using AES-GCM, with the key output by the KDF.
# * The Apple comment/doc says to use Bob's public key data as
# authenticationData for the AES-GCM, but in testing, it looks like
# this breaks it. Looking at the Apple source for
# SecKeyECIESEncryptAESGCMCopyResult
# in
# OSX/sec/Security/SecKeyAdaptors.c
# it looks like no AAD (Additional Authentication Data) is being
# passed to the underlying ccgm_one_shot(ccases_gcm...) call.
# When I tried adding Bob's public bytes as implied by the docs,
# the derived GCM tag naturally changed, and so the decryption broke...
# so for now, I'm going with what I see, rather than what I've read.
#
# * Build the final output string:
# * ECIES documentation seems to specify:
#
# Ephemeral_Public_key + Tag + CT
#
# as the final message format, but I think that's built around a
# tag built with HMAC (which might've been itself keyed with
# additional data out of the KDF...I'm no longer certain of anything
# I read while wrestling with this....)
# * However, since the AES-GCM function automatically appends the
# GCM tag to the end of the ciphertext, it looks like Apple's simply
# expecting:
#
# Ephemeral_Public_key + CT (+tag)
#
# So we glom those two elements together.
#
# * Print the final message encoded as a Base64 string.
#
# Also, the "Public Key Bytes" as used for KDF Shared Info, and also as
# prepended to the CT in the final message....looks like it's basically the
# "real" bitstream data in the DER format key. For example:
# $ cat bob_pem | openssl ec -pubin -noout -text
# read EC key
# Private-Key: (256 bit)
# pub:
# 04:1e:21:b4:b2:59:6c:5b:62:bd:b9:7f:c4:cb:59:
# f1:26:cb:f8:bb:fd:73:db:c2:60:a1:c6:5c:b8:57:
# 26:13:f0:6e:29:76:75:c3:90:81:df:95:71:ad:8a:
# 85:e9:12:94:b9:c9:da:6a:e9:38:55:f8:d2:01:f6:
# 2b:ea:00:be:18
# ASN1 OID: prime256v1
# NIST CURVE: P-256
# The data after "pub:" is the raw bits. If instead, you dump it using
# asn1parse, you'll see there's nothing after that. And if you dump it
# to hex, you'll see it's the last 65 bytes of the key.
#
# In practice, it's probably never going to be as easy to extract these
# bits as simply taking a [-65:] slice in python, but it's working for
# me, so I'm declaring victory and moving on.
#
# (also, how annoying is it that the openssl output doesn't group those
# displayed bytes into 16-byte lines? urgh.)
#
# So, on to the script!
#
import binascii, base64 # for making things look nice to us humans
#
# lots of stuff that we'll use to do the actual work
#
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat, load_pem_public_key
from cryptography.hazmat.primitives.kdf.x963kdf import X963KDF
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.hmac import HMAC
backend = default_backend()
#
# First, load up bob's public key
#
bob_public = load_pem_public_key(bob_pem, backend)
bob_pub_bytes = bob_public.public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo)[-65:]
print "Bob's public key (PEM format): \n%s\n" % bob_pem
print "Bob's public key bytes: %s\n" % binascii.b2a_hex(bob_pub_bytes)
#
# Now, generate Alice's ephemeral privae key just for this message
#
alice_priv = ec.generate_private_key(ec.SECP256R1(), backend)
alice_pub_bytes = alice_priv.public_key().public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo)[-65:]
print "Alice's public key bytes: %s\n" % binascii.b2a_hex(alice_pub_bytes)
#
# use ECDH to generate a shared key using Alice's private and Bob's public keys
#
shared_key = alice_priv.exchange(ec.ECDH(), bob_public)
print "ECDH Shared Key: %s\n" % binascii.b2a_hex(shared_key)
#
# Use the ANSI x9.63 Key Derivation Function to derive the final
# encryption key from the ECDH-built key.
#
# * Use the SHA-256 hash when deriving the key,
# * Use Alice's (ephemeral) public key data as Shared Info, and
# * Extract 16 bytes (enough for a 128-bit key
xkdf = X963KDF(
algorithm=hashes.SHA256(),
length=16,
sharedinfo=alice_pub_bytes,
backend=backend
)
key_enc = xkdf.derive(shared_key)
print 'Final AES Encryption Key: %s\n' % binascii.b2a_hex(key_enc)
iv = binascii.a2b_hex('00000000000000000000000000000000')
print 'Initialization Vector: %s\n' % binascii.b2a_hex(iv)
#
# ENCRYPT THE MESSAGE!
#
C = AESGCM(key_enc)
ct = C.encrypt(iv, message, "")
# bob_pub_bytes was not used as AAD, contrary to expectations
print 'Ciphertext: %s\n' % binascii.b2a_hex(ct)
print "Final message: (Alice's public key bytes + CipherText (incl. GCM tag at end):\n"
final_ct = alice_pub_bytes + ct
print binascii.b2a_hex(final_ct)
print "\nFinal message, Base-64 Encoded, to drop back into the demo app:\n"
print base64.b64encode(final_ct)
@shockrock
Copy link

This is so great, thanks for all your effort in this and the article! Just curious, did you ever (try to) get encryption/decryption to work with just openssl (cli)? It works great for signing and verifying but not encryption.

@EasyAndComplex
Copy link

EasyAndComplex commented Feb 12, 2024

I believe it wont work any more, since it not updated, I have tried and it doesn't work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment