RSA Cryptography In Clojure
I recently found myself needing to do some public/private key cryptography using RSA in Clojure. Fortunately there is pretty good library support for doing this kind of thing in Java, but it still took me a while to get all of the interop working. Additionally, I needed to be able to serialize and de-serialize keys in a couple of formats (.pem and .der, specifically), so we’ll look at setting this up as well.
Generating a Keypair
Keys are generated based on the desired length and algorithm. To generate a key we have to do a little bit of Java ceremony around requesting a
We can use this to generate a Private Key, and from that Private Key retrieve the Public Key if needed.
(defn kp-generator [length] (doto (java.security.KeyPairGenerator/getInstance "RSA") (.initialize length))) (defn generate-keypair [length] (assert (>= length 512) "RSA Key must be at least 512 bits long.") (.generateKeyPair (kp-generator length))) (def keypair (generate-keypair 512)) (def public-key (.getPublic keypair))
Encrypting, Decrypting, and Encoding Messages
The Java crypto methods we’re using generally return a Byte Array of their encrypted data. For my use-case I wanted to encode these in Base64, which is easy in Java 8 thanks to the built-in Base64 module (For earlier versions, check out javax.xml.bind.DatatypeConverter).
(defn decode64 [str] (.decode (java.util.Base64/getDecoder) str)) (defn encode64 [bytes] (.encodeToString (java.util.Base64/getEncoder) bytes))
Now we can use the keys we generated to encrypt and decrypt a message. This being public/private key crypto, remember of course that encryption is done using the public key and decryption using the private.
(defn encrypt [message public-key] "Perform RSA public key encryption of the given message string. Returns a Base64-encoded string of the encrypted data." (encode64 (let [cipher (doto (javax.crypto.Cipher/getInstance "RSA/ECB/PKCS1Padding") (.init javax.crypto.Cipher/ENCRYPT_MODE public-key))] (.doFinal cipher (.getBytes message))))) (defn decrypt [message private-key] "Use an RSA private key to decrypt a Base64-encoded string of ciphertext." (let [cipher (doto (javax.crypto.Cipher/getInstance "RSA/ECB/PKCS1Padding") (.init javax.crypto.Cipher/DECRYPT_MODE private-key))] (->> message decode64 (.doFinal cipher) (map char) (apply str))))
Signing and Verifying
The other big asymmetric crypto operation is to sign using a private key and verify using a public key. This is pretty easy with a bit of Java interop as well.
(defn sign "RSA private key signing of a message. Takes message as string" [message private-key] (encode64 (let [msg-data (.getBytes message) sig (doto (java.security.Signature/getInstance "SHA256withRSA") (.initSign private-key (java.security.SecureRandom.)) (.update msg-data))] (.sign sig)))) (defn verify [encoded-sig message public-key] "RSA public key verification of a Base64-encoded signature and an assumed source message. Returns true/false if signature is valid." (let [msg-data (.getBytes message) signature (decode64 encoded-sig) sig (doto (java.security.Signature/getInstance "SHA256withRSA") (.initVerify public-key) (.update msg-data))] (.verify sig signature)))
Serializing and Deserializing Keys
Finally for my use-case it was important to be able to serialize and de-serialize keys in a format that would be readable by other systems. I found this part the trickiest to get working due to relatively sparse documentation and some confusion about the various formats and key serialization algorithms, but here it is.
DER Encoding Public Keys
(defn der-string->pub-key [string] "Generate an RSA public key from a DER-encoded Base64 string. Some systems like to line-wrap these at 64 characters, so we have to get rid of any newlines before decoding." (let [non-wrapped (clojure.string/replace string #"\n" "") key-bytes (decode64 non-wrapped) spec (java.security.spec.X509EncodedKeySpec. key-bytes) key-factory (java.security.KeyFactory/getInstance "RSA")] (.generatePublic key-factory spec))) (defn public-key->der-string [key] "Generate DER-formatted string for a public key." (-> key .getEncoded encode64 (clojure.string/replace #"\n" "")))
DER Encoding Private Keys
(defn der-string->private-key [string] (.generatePrivate (java.security.KeyFactory/getInstance "RSA") (java.security.spec.PKCS8EncodedKeySpec. (decode64 (.getBytes string))))) (defn private-key->der-string [pk] (-> pk .getEncoded java.security.spec.PKCS8EncodedKeySpec. .getEncoded encode64))
PEM-encoding is another common format for serializing cryptographic keys. I was able to get everything so far working using just pieces from Java’s standard library, but after much experimentation could never get it to read PEM-encoded keys reliably. So I ended up reaching for Bouncy Castle, one of the go-to crypto Java crypto libraries.
BC supports a sizeable menu of different signing, hashing, and encryption algorithms. Fortunately for me reading and writing PEM keys was tucked in among them.
To pull in BouncyCastle I used this
project.clj configuration for leiningen:
(defproject block-chain "0.2.0" :dependencies [[org.clojure/clojure "1.8.0"] [org.bouncycastle/bcpkix-jdk15on "1.53"]])
Then used it to decode the keys.
;; Have to do this bit of setup first so the keyparsers ;; can find BouncyCastle (java.security.Security/addProvider (org.bouncycastle.jce.provider.BouncyCastleProvider.)) (defn keydata [reader] (->> reader (org.bouncycastle.openssl.PEMParser.) (.readObject))) (defn pem-string->key-pair [string] "Convert a PEM-formatted private key string to a public/private keypair. Returns java.security.KeyPair." (let [kd (keydata (io/reader (.getBytes string)))] (.getKeyPair (org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter.) kd))) (defn pem-string->pub-key [string] "Convert a PEM-formatted public key string to an RSA public key. Returns sun.security.rsa.RSAPublicKeyImpl" (let [kd (keydata (io/reader (.getBytes string))) kf (java.security.KeyFactory/getInstance "RSA") spec (java.security.spec.X509EncodedKeySpec. (.getEncoded kd))] (.generatePublic kf spec))) (defn format-pem-string [encoded key-type] "Takes a Base64-encoded string of key data and formats it for file-output following openssl's convention of wrapping lines at 64 characters and appending the appropriate header and footer for the specified key type" (let [chunked (->> encoded (partition 64 64 ) (map #(apply str %))) formatted (join "\n" chunked)] (str "-----BEGIN " key-type "-----\n" formatted "\n-----END " key-type "-----\n"))) (defn private-key->pem-string [key] "Convert RSA private keypair to a formatted PEM string for saving in a .pem file. By default these private keys will encode themselves as PKCS#8 data (e.g. when calling (.getEncoded private-key)), so we have to convert it to ASN1, which PEM uses (this seems to also be referred to as PKCS#1). More info here http://stackoverflow.com/questions/7611383/generating-rsa-keys-in-pkcs1-format-in-java" (-> (.getEncoded key) (org.bouncycastle.asn1.pkcs.PrivateKeyInfo/getInstance) (.parsePrivateKey) (.toASN1Primitive) (.getEncoded) (encode64) (format-pem-string "RSA PRIVATE KEY"))) (defn public-key->pem-string [key] "Generate PEM-formatted string for a public key. This is simply a base64 encoding of the key wrapped with the appropriate header and footer." (format-pem-string (encode64 (.getEncoded key)) "PUBLIC KEY"))
One last note about PEM formatting and keys – in some instances a PEM key is simply the same Base64-encoded DER representation of the key wrapped with the “BEGIN KEY” / “END KEY” header and footer. However the PEM format can be used slightly differently by a variety of key types, and because of this it sometimes needs to include additional metadata about what key format is being encoded.
The ins and outs of serializing cryptographic keys can get pretty complex, and there are unfortunately a lot of ways to do things using very similar encoding formats. I’ve managed to cobble together enough for the use-cases I needed here, but if you’d like to understand more, here is a good article that goes into more depth.