diff --git a/leiningen-core/src/leiningen/core/user.clj b/leiningen-core/src/leiningen/core/user.clj index a6bde804..f3f96eac 100644 --- a/leiningen-core/src/leiningen/core/user.clj +++ b/leiningen-core/src/leiningen/core/user.clj @@ -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)))) diff --git a/src/leiningen/deploy.clj b/src/leiningen/deploy.clj index 810b96f4..b98a8d28 100644 --- a/src/leiningen/deploy.clj +++ b/src/leiningen/deploy.clj @@ -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") diff --git a/test/leiningen/test/deploy.clj b/test/leiningen/test/deploy.clj index b9fccb56..50d5b1bc 100644 --- a/test/leiningen/test/deploy.clj +++ b/test/leiningen/test/deploy.clj @@ -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"}}]))