lib.openpgp

OpenPGP key parsing, encryption, signatures, and verification

Work with Bouncy Castle OpenPGP keys and byte-oriented encryption or signature operations through Clojure functions.

1    Overview

The namespace parses armored public and secret key rings, derives usable key pairs, encrypts and decrypts byte arrays, and reads or writes detached signature files.

2    Walkthrough

2.1    Parse keys

(require '[lib.openpgp :as openpgp])

(def public-key
  (openpgp/parse-public-key public-key-text))

(def secret-key
  (openpgp/parse-secret-key secret-key-text))

(def pair (openpgp/key-pair secret-key))
(openpgp/fingerprint public-key)

2.2    Encrypt and decrypt bytes

(let [[public private] pair
      encrypted (openpgp/encrypt
                 (.getBytes "hello")
                 {:public public})]
  (String.
   (openpgp/decrypt encrypted
                    {:private private})))

2.3    Create and verify a detached signature

(let [[public private] pair]
  (openpgp/sign "artifact.jar"
                "artifact.jar.asc"
                {:public public :private private})
  (openpgp/verify "artifact.jar"
                  "artifact.jar.asc"
                  {:public public}))

3    API



crc-24 ^

[input]
Added 3.0

returns the crc24 checksum

v 3.0
(defn crc-24
  ([input]
   (let [crc (CRC24.)
         _   (doseq [i (seq input)]
               (.update crc i))
         val (.getValue crc)
         bytes (-> (biginteger val)
                   (.toByteArray)
                   seq)
         bytes (case (count bytes)
                 4 (rest bytes)
                 3 bytes
                 (nth (iterate #(cons 0 %)
                               bytes)
                      (- 3 (count bytes))))]
     [(->> (byte-array bytes)
           (encode/to-base64)
           (str "="))
      bytes
      val])))
link
(crc-24 (byte-array [100 100 100 100 100 100])) => ["=6Fko" [-24 89 40] 15227176]

decrypt ^

[encrypted {:keys [private]}]
Added 3.0

decrypts the encrypted information

v 3.0
(defn decrypt
  ([encrypted {:keys [private]}]
   (let [obj-factory  (-> (binary/input-stream encrypted)
                          (PGPUtil/getDecoderStream)
                          (PGPObjectFactory. (BcKeyFingerprintCalculator.)))
         enc-data     (-> ^PGPEncryptedDataList (.nextObject obj-factory)
                          (.getEncryptedDataObjects)
                          (iterator-seq)
                          ^PGPPublicKeyEncryptedData (first))
         key-id       (.getKeyID enc-data)
         bytes        (-> (.getDataStream enc-data
                                          (BcPublicKeyDataDecryptorFactory. private))
                          (JcaPGPObjectFactory.)
                          ^PGPLiteralData (.nextObject)
                          (.getDataStream)
                          (binary/bytes))
         bytes        (try (-> (JcaPGPObjectFactory. ^java.io.InputStream (binary/input-stream bytes))
                               ^PGPLiteralData (.nextObject)
                               (.getDataStream)
                               (binary/bytes))
                           (catch Exception e bytes))]
     bytes)))
link
(-> (.getBytes "Hello World") (encrypt {:public +public-key+}) ^bytes (decrypt {:private +private-key+}) (String.)) => "Hello World"

encrypt ^

[clear {:keys [public]}]
Added 3.0

encrypts bytes given a public key

v 3.0
(defn encrypt
  ([^bytes clear {:keys [public]}]
   (let [bstream (ByteArrayOutputStream.)
         ldata   (PGPLiteralDataGenerator.)
         lname   (str (fs/create-tmpfile))
         lstream (.open ldata bstream PGPLiteralData/BINARY
                        lname (count clear) (java.util.Date.))
         lstream (doto ^java.io.OutputStream lstream
                   (.write clear)
                   (.close))
         cpk-builder (-> (BcPGPDataEncryptorBuilder. PGPEncryptedData/CAST5)
                         (.setSecureRandom (SecureRandom.))
                         (.setWithIntegrityPacket true))
         cpk     (doto (PGPEncryptedDataGenerator. cpk-builder)
                   (.addMethod (BcPublicKeyKeyEncryptionMethodGenerator. public)))
         bytes   (.toByteArray bstream)
         ostream (ByteArrayOutputStream.)
         estream (doto (.open cpk ostream (count bytes))
                   (.write bytes)
                   (.close))]
     (.close ostream)
     (.toByteArray ostream))))
link
(->> (encrypt (.getBytes "Hello World") {:public +public-key+}) (encode/to-base64)) ;; ["hQEMA2dT+OFtNfwkAQgAy4YMHQ70vRNgsoSQTFCLeHDMM6k6X+tkfs1xQzG5sdxlqXKxsaeX2z//i6iz" ;; "kfEpo2MuzvecpKb3Eu4WPYGqqHcStHtyAUJYT4BxC2HZf3Nd8v6wpo4C++EdrIEUOY4cgQbpv1xBSd0c" ;; "csoQPq9JPhGjBX3n9I+Nf6QDqjyC8EQixdeTJyb5SW23yDdPQGZd++CmWFxgnoMaQR6fJJpv0iO+1mpz" ;; "QK+1xi4eTeDhdzAP36Mh8Wtj97xS+JWYyHq6MG6BsxofWmpWjZbp6xSnHytj/IWLTS7AOyCyQfOwfM81" ;; "yxI4kFCHOejsGa3N1G6SpzSWkO0f3uXKVLidxdAtudJ6AUFwusdCsWWV1maNhOUre9F5sDawSz3fJea3" ;; "3SF/8t0TGY0YrwL0gN1zr60AL+zykyJJiLB8b9tshXsNbO4BNdmKJ/IAi2JYk7GMIha/JSUtsHMetL2d"] => string?

fingerprint ^

[pub]
Added 3.0

returns the fingerprint of a public key

v 3.0
(defn fingerprint
  ([^PGPPublicKey pub]
   (-> pub
       (.getFingerprint)
       (encode/to-hex)
       (.toUpperCase))))
link
(fingerprint +public-key+) => "E710D59C5346D3C0A1C578AE6753F8E16D35FC24"

generate-signature ^

[bytes {:keys [public private]}]
Added 3.0

generates a signature given bytes and a keyring

v 3.0
(defn ^PGPSignature generate-signature
  ([^bytes bytes {:keys [^PGPPublicKey public private]}]
   (let [sig-gen  (-> (BcPGPContentSignerBuilder.
                       (.getAlgorithm public)
                       PGPUtil/SHA256)
                      (PGPSignatureGenerator.))
         sig-gen  (doto sig-gen
                    (.init PGPSignature/BINARY_DOCUMENT private)
                    (.update bytes))]
     (.generate sig-gen))))
link
(generate-signature (.getBytes "Hello World") {:public +public-key+ :private +private-key+}) ;; #gpg.signature ["iQEcBAABCAAGBQJbw1U8AAoJEGdT+OFtNf... "] => org.bouncycastle.openpgp.PGPSignature

key-pair ^

[secret-key]
Added 3.0

returns a public and private key pair from a secret key

v 3.0
(defn key-pair
  ([^PGPSecretKey secret-key]
   (let [decryptor (-> (JcePBESecretKeyDecryptorBuilder.)
                       (.setProvider "BC")
                       (.build (char-array "")))]
     [(.getPublicKey secret-key)
      (.extractPrivateKey secret-key decryptor)])))
link
(key-pair +secret-key+) ;;[#public "E710D59C5346D3C0A1C578AE6753F8E16D35FC24" #private "7445568256057146404"] => (contains [org.bouncycastle.openpgp.PGPPublicKey org.bouncycastle.openpgp.PGPPrivateKey])

parse-public-key ^

[input] [input id]
Added 3.0

parses a public key from string

v 3.0
(defn parse-public-key
  ([input]
   (->> (parse-public-key-ring input)
        (.getPublicKeys)
        (iterator-seq)
        first))
  ([input id]
   (->> (parse-public-key-ring input)
        (.getPublicKeys)
        (iterator-seq)
        (filter #{id})
        (first))))
link
(-> (common/joinl +public-key-string+ "n") (parse-public-key)) ;; #public "E710D59C5346D3C0A1C578AE6753F8E16D35FC24" => org.bouncycastle.openpgp.PGPPublicKey

parse-public-key-ring ^

[input]
Added 3.0

parses a public key ring from string

v 3.0
(defn ^PGPPublicKeyRing parse-public-key-ring
  ([input]
   (-> (binary/input-stream input)
       (PGPUtil/getDecoderStream)
       (PGPPublicKeyRing. ^BcKeyFingerprintCalculator (BcKeyFingerprintCalculator.)))))
link
(-> (common/joinl +public-key-string+ "n") (parse-public-key-ring)) => org.bouncycastle.openpgp.PGPPublicKeyRing

parse-secret-key ^

[input] [input id]
Added 3.0

parses a secret key from string

v 3.0
(defn parse-secret-key
  ([input]
   (-> (parse-secret-key-ring input)
       (.getSecretKeys)
       (iterator-seq)
       first))
  ([input id]
   (->> (parse-secret-key-ring input)
        (.getSecretKeys)
        (iterator-seq)
        (filter #{id})
        (first))))
link
(-> (common/joinl +secret-key-string+ "n") (parse-secret-key)) ;; #secret "E710D59C5346D3C0A1C578AE6753F8E16D35FC24" => org.bouncycastle.openpgp.PGPSecretKey

parse-secret-key-ring ^

[input]
Added 3.0

parses a secret key ring from string

v 3.0
(defn ^PGPSecretKeyRing parse-secret-key-ring
  ([input]
   (-> (binary/input-stream input)
       (PGPUtil/getDecoderStream)
       (PGPSecretKeyRing. ^BcKeyFingerprintCalculator (BcKeyFingerprintCalculator.)))))
link
(-> (common/joinl +secret-key-string+ "n") (parse-secret-key-ring)) => org.bouncycastle.openpgp.PGPSecretKeyRing

pgp-signature ^

[bytes]
Added 3.0

returns a gpg signature from encoded bytes

v 3.0
(defn ^PGPSignature pgp-signature
  ([bytes]
   (let [input-stream (BCPGInputStream. (java.io.ByteArrayInputStream. bytes)) 
         fingerprint-calc (BcKeyFingerprintCalculator.)
         object-factory (PGPObjectFactory. input-stream fingerprint-calc)
         obj (.nextObject object-factory)]
     (cond (instance? PGPSignature obj)
           obj

           (instance? PGPSignatureList obj)
           (first obj)

           :else
           (throw (ex-info "Expected PGPSignature, got different object type."
                           {:object obj}))))))
link
(-> (generate-signature (.getBytes "Hello World") {:public +public-key+ :private +private-key+}) (.getEncoded) (pgp-signature)) => org.bouncycastle.openpgp.PGPSignature

read-sig-file ^

[sig-file]
Added 3.0

reads bytes from a GPG compatible file

v 3.0
(defn read-sig-file
  ([sig-file]
   (->> (slurp sig-file)
        (clojure.string/split-lines)
        (reverse)
        (drop-while (fn [^String input]
                      (not (and (.startsWith input "=")
                                (= 5 (count input))))))
        (rest)
        (take 6)
        (reverse)
        (clojure.string/join "")
        (encode/from-base64))))
link
(read-sig-file "test-scratch/project.clj.asc") => bytes?

sign ^

[input sig-file {:keys [public private], :as opts}]
Added 3.0

generates a output gpg signature for an input file

v 3.0
(defn sign
  ([input sig-file {:keys [public private] :as opts}]
   (env/prn opts)
   (let [input-bytes (fs/read-all-bytes input)
         sig-bytes  (-> (generate-signature input-bytes opts)
                        (.getEncoded))]
     (write-sig-file sig-file sig-bytes)
     sig-bytes)))
link
(sign "project.clj" "test-scratch/project.clj.asc" {:public +public-key+ :private +private-key+}) => bytes?

verify ^

[input sig-file {:keys [public]}]
Added 3.0

verifies that the signature works

v 3.0
(defn verify
  ([input sig-file {:keys [public]}]
   (let [bytes (fs/read-all-bytes input)
         sig-bytes (read-sig-file sig-file)
         sig (if (zero? (count sig-bytes))
               (throw (Exception. (str "Not a valid signature file: " sig-file)))
               (pgp-signature sig-bytes))]
     (-> (doto ^PGPSignature sig
           (.init (BcPGPContentVerifierBuilderProvider.) ^PGPPublicKey public)
           (.update ^bytes bytes))
         (.verify)))))
link
(verify "project.clj" "test-scratch/project.clj.asc" {:public +public-key+}) => true

write-sig-file ^

[sig-file bytes]
Added 3.0

writes bytes to a GPG compatible file

v 3.0
(defn write-sig-file
  ([sig-file bytes]
   (->> (concat ["-----BEGIN PGP SIGNATURE-----"
                 ""]
                (->> bytes
                     (encode/to-base64)
                     (partition-all 64)
                     (map #(apply str %)))
                [(first (crc-24 bytes))
                 "-----END PGP SIGNATURE-----"])
        (clojure.string/join "n")
        (spit sig-file))))
link
(let [signature (-> (generate-signature (.getBytes "Hello World") {:public +public-key+ :private +private-key+}) (.getEncoded))] (write-sig-file "test-scratch/project.clj.asc" signature)) => nil