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
- decrypt
- encrypt
- fingerprint
- generate-signature
- key-pair
- parse-public-key
- parse-public-key-ring
- parse-secret-key
- parse-secret-key-ring
- pgp-signature
- read-sig-file
- sign
- verify
- write-sig-file
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]
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"
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?
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
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])
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
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
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
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
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
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?
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
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