allow GPG to be invoked unattended with passphrase (+2 squashed commits)

Squashed commits:
[81bda1b] allow GPG to be invoked unattended with passphrase
[ef54597] allow GPG to be invoked unattended with passphrase
This commit is contained in:
Neil Okamoto 2017-05-29 09:58:19 -07:00
parent 98250defe3
commit ec921f141e
3 changed files with 79 additions and 25 deletions

View file

@ -2,7 +2,6 @@
"Functions exposing user-level configuration."
(:require [clojure.java.io :as io]
[clojure.string :as str]
[clojure.java.shell :as shell]
[leiningen.core.utils :as utils])
(:import (com.hypirion.io Pipe)
(org.apache.commons.io.output TeeOutputStream)
@ -104,9 +103,14 @@
[env]
(into-array String (map (fn [[k v]] (str (name k) "=" v)) env)))
(defn gpg
"Shells out to (gpg-program) with the given arguments"
[& args]
(defn gpg-with-passphrase
"Shells out to (gpg-program) with the given arguments and, if
passphrase is not nil, sends the passphrase on stdin for unattended
operations such as signing artifacts for deployment. When a passphrase
is provided the caller must include the following args
[\"--passphrase-fd\" \"0\" \"--pinentry-mode\" \"loopback\"]
along with whatever other args are needed for the gpg command."
[passphrase & args]
(try
(let [proc-env (as-env-strings (get-english-env))
proc-args (into-array String (concat [(gpg-program)] args))
@ -114,10 +118,15 @@
(.addShutdownHook (Runtime/getRuntime)
(Thread. (fn [] (.destroy proc))))
(with-open [out (.getInputStream proc)
err (.getErrorStream proc)
err-output (ByteArrayOutputStream.)]
(let [pump-err (doto (Pipe. (.getErrorStream proc)
(TeeOutputStream. System/err err-output))
.start)]
(if passphrase
(with-open [in (.getOutputStream proc)]
(io/copy passphrase in)))
(let [pump-err (doto (Pipe. err
(TeeOutputStream. System/err
err-output))
.start)]
(.join pump-err)
(let [exit-code (.waitFor proc)]
{:exit exit-code
@ -126,6 +135,11 @@
(catch java.io.IOException e
{:exit 1 :out "" :err (.getMessage e)})))
(defn gpg
"Shells out to (gpg-program) with the given arguments"
[& args]
(apply gpg-with-passphrase nil args))
(defn gpg-available?
"Verifies (gpg-program) exists"
[]
@ -156,25 +170,37 @@
(re-find re? (:url settings)))]
cred))))
(defn resolve-env-keyword
"Resolve usage of :env and :env/foo in project.clj"
[k v]
(cond (= :env v)
(getenv (str "LEIN_"
(-> (name k)
(str/upper-case)
(str/replace "-" "_"))))
(and (keyword? v) (= "env" (namespace v)))
(getenv (str/upper-case (name v)))
:else nil))
(defn- resolve-gpg-keyword
"Resolve usage of :gpg in project.clj"
[source-settings k v]
(cond (= :gpg v)
(get (match-credentials source-settings (credentials)) k)))
(defn- resolve-credential
"Resolve key-value pair from result into a credential, updating result."
[source-settings result [k v]]
(letfn [(resolve [v]
(cond (= :env v)
(getenv (str "LEIN_" (str/upper-case (name k))))
(and (keyword? v) (= "env" (namespace v)))
(getenv (str/upper-case (name v)))
(= :gpg v)
(get (match-credentials source-settings (credentials)) k)
(coll? v) ;; collection of places to look
(or (resolve-env-keyword k v)
(resolve-gpg-keyword source-settings k v)
(if (coll? v) ;; collection of places to look
(->> (map resolve v)
(remove nil?)
first)
:else v))]
first))
v))]
(if (#{:username :password :passphrase :private-key-file} k)
(assoc result k (resolve v))
(assoc result k v))))

View file

@ -76,14 +76,27 @@
(defn signing-args
"Produce GPG arguments for signing a file."
[file opts]
(let [key-spec (if-let [key (:gpg-key opts)]
["--default-key" key])]
`["--yes" "-ab" ~@key-spec "--" ~file]))
(let [key-args (concat
(if-let [key (:gpg-key opts)]
["--default-key" key])
(if-let [pass (:gpg-passphrase opts)]
["--passphrase-fd" "0"
"--pinentry-mode" "loopback"]))]
`["--yes" "-ab" ~@key-args "--" ~file]))
(defn signing-passphrase
"Produce GPG passphrase if specified in project"
[opts]
(if-let [pp (:gpg-passphrase opts)]
(or (user/resolve-env-keyword :gpg-passphrase pp)
pp)))
(defn sign
"Create a detached signature and return the signature file name."
[file opts]
(let [{:keys [err exit]} (apply user/gpg (signing-args file opts))]
(let [{:keys [err exit]} (apply user/gpg-with-passphrase
(cons (signing-passphrase opts)
(signing-args file opts)))]
(when-not (zero? exit)
(main/abort "Could not sign"
(str file "\n" err (if err "\n")

View file

@ -2,6 +2,7 @@
(:use [clojure.test]
[clojure.java.io :only [file]]
[leiningen.deploy]
[leiningen.core.user :as user]
[leiningen.test.helper :only [delete-file-recursively
tmp-dir sample-project
sample-deploy-project]]))
@ -66,7 +67,21 @@
(is (= (signing-args "foo.jar" nil)
["--yes" "-ab" "--" "foo.jar"]))
(is (= (signing-args "foo.jar" {:gpg-key "123456"})
["--yes" "-ab" "--default-key" "123456" "--" "foo.jar"])))
["--yes" "-ab" "--default-key" "123456" "--" "foo.jar"]))
(is (= (signing-args "foo.jar" {:gpg-key "123456" :gpg-passphrase "abc"})
["--yes" "-ab" "--default-key" "123456"
"--passphrase-fd" "0" "--pinentry-mode" "loopback"
"--" "foo.jar"]))
(is (= (signing-args "foo.jar" {:gpg-passphrase "abc"})
["--yes" "-ab"
"--passphrase-fd" "0" "--pinentry-mode" "loopback"
"--" "foo.jar"]))
(is (= (signing-passphrase {}) nil))
(is (= (signing-passphrase {:gpg-passphrase "abc"}) "abc"))
(with-redefs [user/getenv (fn [v] (if (= v "LEIN_GPG_PASSPHRASE") "abc" nil))]
(is (= (signing-passphrase {:gpg-passphrase :env}) "abc")))
(with-redefs [user/getenv (fn [v] (if (= v "MYPASS") "abc" nil))]
(is (= (signing-passphrase {:gpg-passphrase :env/mypass}) "abc"))))
(testing "Key selection"
(is (= (:gpg-key (signing-opts {:signing {:gpg-key "key-project"}}
["repo" {:signing {:gpg-key "key-repo"}}]))