diff options
Diffstat (limited to 'lib/GnuPGAgent.hs')
-rw-r--r-- | lib/GnuPGAgent.hs | 198 |
1 files changed, 198 insertions, 0 deletions
diff --git a/lib/GnuPGAgent.hs b/lib/GnuPGAgent.hs new file mode 100644 index 0000000..7161b92 --- /dev/null +++ b/lib/GnuPGAgent.hs | |||
@@ -0,0 +1,198 @@ | |||
1 | {-# LANGUAGE LambdaCase #-} | ||
2 | {-# LANGUAGE CPP #-} | ||
3 | {-# LANGUAGE PatternGuards #-} | ||
4 | module GnuPGAgent | ||
5 | ( session | ||
6 | , Query(..) | ||
7 | , QueryMode(..) | ||
8 | , getPassphrase | ||
9 | , clearPassphrase | ||
10 | , quit | ||
11 | , key_nbits) where | ||
12 | |||
13 | import Debug.Trace | ||
14 | import Control.Monad | ||
15 | import Data.Char | ||
16 | import Data.OpenPGP | ||
17 | import Data.OpenPGP.Util | ||
18 | import Network.Socket | ||
19 | import System.Directory | ||
20 | import System.Environment | ||
21 | import System.IO | ||
22 | import Text.Printf | ||
23 | #if defined(VERSION_memory) | ||
24 | import qualified Data.ByteString.Char8 as S8 | ||
25 | import Data.ByteArray.Encoding | ||
26 | #elif defined(VERSION_dataenc) | ||
27 | import qualified Codec.Binary.Base16 as Base16 | ||
28 | #endif | ||
29 | import LengthPrefixedBE | ||
30 | import qualified Data.ByteString.Lazy as L | ||
31 | #if defined(VERSION_hourglass) | ||
32 | import Data.Hourglass | ||
33 | #else | ||
34 | import Data.Time.Calendar | ||
35 | import Data.Time.Clock | ||
36 | import Data.Time.Clock.POSIX | ||
37 | #endif | ||
38 | import Data.Word | ||
39 | |||
40 | data GnuPGAgent = GnuPGAgent { agentHandle :: Handle } | ||
41 | |||
42 | session = do | ||
43 | envhomedir Nothing gpgHomeSpec >>= \case | ||
44 | Just gpghome -> do | ||
45 | sock <- socket AF_UNIX Stream defaultProtocol | ||
46 | connect sock (SockAddrUnix (gpghome ++ "/S.gpg-agent")) | ||
47 | agent <- socketToHandle sock ReadWriteMode | ||
48 | hSetBuffering agent LineBuffering | ||
49 | lookupEnv "DISPLAY" >>= \case | ||
50 | Just display -> do hPutStrLn agent ("option putenv DISPLAY="++display) | ||
51 | _ <- hGetLine agent | ||
52 | return () | ||
53 | Nothing -> return () | ||
54 | -- TODO: GPG_TTY | ||
55 | return $ Just $ GnuPGAgent agent | ||
56 | Nothing -> do | ||
57 | hPutStrLn stderr "Unable to find home directory." | ||
58 | return Nothing | ||
59 | |||
60 | percentPlusEscape :: String -> String | ||
61 | percentPlusEscape s = do | ||
62 | c <- s | ||
63 | case c of | ||
64 | ' ' -> "+" | ||
65 | '+' -> "%2B" | ||
66 | '"' -> "%22" | ||
67 | '%' -> "%25" | ||
68 | _ | c < ' ' -> printf "%%%02X" (ord c) | ||
69 | _ -> return c | ||
70 | |||
71 | clearPassphrase agent key = do | ||
72 | let cmd = "clear_passphrase --mode=normal "++fingerprint key | ||
73 | hPutStrLn (agentHandle agent) cmd | ||
74 | |||
75 | data Query = Query | ||
76 | { queryPacket :: Packet | ||
77 | , queryUID :: String | ||
78 | , queryMainKey :: Maybe Packet | ||
79 | } | ||
80 | deriving Show | ||
81 | |||
82 | data QueryMode = AskNot | AskAgain String | AskExisting | AskNew | ||
83 | deriving (Show,Eq,Ord) | ||
84 | |||
85 | getPassphrase :: GnuPGAgent -> QueryMode -> Query -> IO (Maybe String) | ||
86 | getPassphrase agent ask (Query key uid masterkey) = do | ||
87 | let (er0,pr,desc) = prompts key uid masterkey | ||
88 | (er,askopt) = case ask of | ||
89 | AskNot -> (er0,"--no-ask ") | ||
90 | AskAgain ermsg -> (ermsg,"") | ||
91 | AskExisting -> (er0,"") | ||
92 | AskNew -> (er0,"--repeat=1 ") | ||
93 | cmd = "get_passphrase "++askopt++fingerprint key++" "++unwords (map percentPlusEscape [er,pr,desc]) | ||
94 | hPutStrLn stderr $ "gpg-agent <- " ++ cmd | ||
95 | hPutStrLn (agentHandle agent) cmd | ||
96 | r0 <- hGetLine (agentHandle agent) | ||
97 | -- hPutStrLn stderr $ "agent says: " ++ r0 | ||
98 | case takeWhile (/=' ') r0 of | ||
99 | "OK" -> hGetLine (agentHandle agent) >>= unhex . drop 3 | ||
100 | where | ||
101 | #if defined(VERSION_memory) | ||
102 | unhex hx = case convertFromBase Base16 (S8.pack hx) of | ||
103 | Left e -> do | ||
104 | -- Useful for debugging but insecure generally ;) | ||
105 | -- putStrLn $ "convertFromBase error for input "++show hx++": "++show e | ||
106 | return Nothing | ||
107 | Right bs -> return $ Just $ S8.unpack bs | ||
108 | #elif defined(VERSION_dataenc) | ||
109 | unhex hx = maybe (return () {- putStrLn $ "dataenc error for input "++show hx -}) | ||
110 | return | ||
111 | $ fmap (map $ chr . fromIntegral) $ Base16.decode hx | ||
112 | #endif | ||
113 | "ERR" -> return Nothing | ||
114 | |||
115 | quit (GnuPGAgent h) = hClose h | ||
116 | |||
117 | prompts :: Packet -> String -> Maybe Packet -> (String,String,String) | ||
118 | prompts key uid masterkey = ("X","X",atext) | ||
119 | where | ||
120 | atext = printf (concat [ "Please enter the passphrase to unlock the" | ||
121 | , " secret key for the OpenPGP certificate:\n" | ||
122 | , "\"%s\"\n" | ||
123 | , "%d-bit %s key, ID %s,\n" | ||
124 | , "created %s%s.\n"]) | ||
125 | uid | ||
126 | (key_nbits key) algo_name (keystr key) | ||
127 | timestr maink | ||
128 | |||
129 | maink | ||
130 | | Just k <- masterkey = printf " (main key ID %s)" (drop 32 $ fingerprint k) | ||
131 | | otherwise = "" | ||
132 | |||
133 | algo_name = | ||
134 | case key_algorithm key of | ||
135 | a | a `elem` [RSA,RSA_E,RSA_S] -> "RSA" | ||
136 | ELGAMAL -> "ELG" -- also PUBKEY_ALGO_ELGAMAL_E | ||
137 | DSA -> "DSA" | ||
138 | ECDSA -> "ECDSA" | ||
139 | ECC -> "ECDH" | ||
140 | _ -> "?" -- also "EDDSA"; | ||
141 | |||
142 | |||
143 | keystr k = drop 32 $ fingerprint k -- FCD7BFB7 | ||
144 | |||
145 | timestr = timeString $ timestamp key -- 2014-01-04 | ||
146 | |||
147 | data HomeDir = HomeDir { homevar :: String, appdir :: String } | ||
148 | gpgHomeSpec :: HomeDir | ||
149 | gpgHomeSpec = HomeDir | ||
150 | { homevar = "GNUPGHOME" | ||
151 | , appdir = ".gnupg" | ||
152 | } | ||
153 | |||
154 | envhomedir opt home = do | ||
155 | gnupghome <- fmap (mfilter (/="")) $ lookupEnv (homevar home) | ||
156 | homed <- fmap (mfilter (/="") . Just) getHomeDirectory | ||
157 | let homegnupg = (++('/':(appdir home))) <$> homed | ||
158 | let val = (opt `mplus` gnupghome `mplus` homegnupg) | ||
159 | return $ val | ||
160 | |||
161 | |||
162 | timeString :: Word32 -> String | ||
163 | timeString t = printf "%d-%d-%d" year month day | ||
164 | where | ||
165 | #if defined(VERSION_hourglass) | ||
166 | Date year m day = timeFromElapsed (Elapsed (Seconds $ fromIntegral t)) | ||
167 | month = fromEnum m + 1 | ||
168 | #else | ||
169 | (year,month,day) = toGregorian . utctDay $ posixSecondsToUTCTime (realToFrac t) | ||
170 | #endif | ||
171 | |||
172 | key_nbits :: Packet -> Int | ||
173 | key_nbits p@(SecretKeyPacket {}) = _key_nbits (key_algorithm p) (key p) | ||
174 | key_nbits p@(PublicKeyPacket {}) = _key_nbits (key_algorithm p) (key p) | ||
175 | key_nbits _ = 0 | ||
176 | |||
177 | _key_nbits :: KeyAlgorithm -> [(Char,MPI)] -> Int | ||
178 | _key_nbits RSA toks | Just n <- lookup 'n' toks = mpi_nbits n | ||
179 | _key_nbits DSA toks | Just n <- lookup 'p' toks = mpi_nbits n | ||
180 | _key_nbits ECDSA toks | Just n <- lookup 'c' toks = curve_oid_nbits n | ||
181 | _key_nbits ELGAMAL toks | Just n <- lookup 'p' toks = mpi_nbits n | ||
182 | _key_nbits _ _ = 0 | ||
183 | -- unimplemented: RSA_E RSA_S ECC DH / KeyAlgorithm Word8 | ||
184 | |||
185 | curve_oid_nbits :: MPI -> Int | ||
186 | curve_oid_nbits (MPI 0x2a8648ce3d030107 ) = 256 -- SEC p256r1 ( NIST P-256 ) | ||
187 | curve_oid_nbits (MPI 0x2B81040022 ) = 384 -- SEC p384r1 ( NIST P-384 ) | ||
188 | curve_oid_nbits (MPI 0x2B81040023 ) = 521 -- SEC p521r1 ( NIST P-521 ) | ||
189 | curve_oid_nbits (MPI 0x2b8104000a ) = 256 -- SEC p256k1 ( bitcoin curve ) | ||
190 | curve_oid_nbits n = trace ("Unknown curve: "++ show n) 0 | ||
191 | |||
192 | |||
193 | mpi_nbits (MPI n) = 8 * fromIntegral len | ||
194 | where | ||
195 | len = case encode_bigendian n of | ||
196 | b | L.head b == 0x0 -> L.length b - 1 | ||
197 | | otherwise -> L.length b | ||
198 | |||