security.openpgp interface to bouncycastle openpgp

Author: Chris Zheng  (z@caudate.me)
Date: 27 November 2018
Repository: https://github.com/zcaudate/hara
Version: 3.0.2

1    Introduction

hara.security.openpgp is an interface to the bouncy-castle openpgp implementation

1.1    Installation

Add to project.clj dependencies:

[hara/security.openpgp "3.0.2"]

All functions are in the hara.security.openpgp namespace.

 (use (quote hara.security.openpgp))

2    API



crc-24 ^

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 ^

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     (-> (.nextObject obj-factory)
                         (.getEncryptedDataObjects)
                         (iterator-seq)
                         (first))
        key-id       (.getKeyID enc-data)
        bytes        (-> (.getDataStream enc-data
                                         (BcPublicKeyDataDecryptorFactory. private))
                         (JcaPGPObjectFactory.)
                         (.nextObject)
                         (.getDataStream)
                         (binary/bytes))
        bytes        (try (-> (JcaPGPObjectFactory. (binary/input-stream bytes))
                              (.nextObject)
                              (.getDataStream)
                              (binary/bytes))
                          (catch Exception e bytes))]
    bytes))
link
(-> (.getBytes "Hello World") (encrypt {:public +public-key+}) (decrypt {:private +private-key+}) (String.)) => "Hello World"

encrypt ^

encrypts bytes given a public key

v 3.0
(defn encrypt
  [clear {:keys [public]}]
  (let [bstream (ByteArrayOutputStream.)
        ldata   (PGPLiteralDataGenerator.)
        lname   (str (fs/create-tmpfile))
        lstream (doto (.open ldata bstream PGPLiteralData/BINARY lname (count clear) (java.util.Date.))
                  (.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))

fingerprint ^

returns the fingerprint of a public key

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

generate-signature ^

generates a signature given bytes and a keyring

v 3.0
(defn generate-signature
  ([bytes {:keys [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 ^

returns a public and private key pair from a secret key

v 3.0
(defn key-pair
  [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 ^

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
(-> (string/joinl +public-key-string+ "n") (parse-public-key)) ;; #public "E710D59C5346D3C0A1C578AE6753F8E16D35FC24" => org.bouncycastle.openpgp.PGPPublicKey

parse-public-key-ring ^

parses a public key ring from string

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

parse-secret-key ^

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
(-> (string/joinl +secret-key-string+ "n") (parse-secret-key)) ;; #secret "E710D59C5346D3C0A1C578AE6753F8E16D35FC24" => org.bouncycastle.openpgp.PGPSecretKey

parse-secret-key-ring ^

parses a secret key ring from string

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

pgp-signature ^

returns a gpg signature from encoded bytes

v 3.0
(defn pgp-signature
  [bytes]
  (let [make-pgp-signature (object/query-class PGPSignature ["new" [BCPGInputStream] :#])]
    (-> bytes
        (java.io.ByteArrayInputStream.)
        (BCPGInputStream.)
        (make-pgp-signature))))
link
(-> (generate-signature (.getBytes "Hello World") {:public +public-key+ :private +private-key+}) (.getEncoded) (pgp-signature)) => org.bouncycastle.openpgp.PGPSignature

read-sig-file ^

reads bytes from a GPG compatible file

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

sign ^

generates a output gpg signature for an input file

v 3.0
(defn sign
  ([input sig-file {:keys [public private] :as 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" "dev/scratch/project.clj.asc" {:public +public-key+ :private +private-key+}) => bytes?

verify ^

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))]
     (.init sig (BcPGPContentVerifierBuilderProvider.) public)
     (.update sig bytes)
     (.verify sig))))
link
(verify "project.clj" "dev/scratch/project.clj.asc" {:public +public-key+}) => true

write-sig-file ^

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-----"])
       (string/join "n")
       (spit sig-file)))
link
(let [signature (-> (generate-signature (.getBytes "Hello World") {:public +public-key+ :private +private-key+}) (.getEncoded))] (write-sig-file "dev/scratch/project.clj.asc" signature))