summaryrefslogtreecommitdiff
path: root/lib/GnuPGAgent.hs
diff options
context:
space:
mode:
Diffstat (limited to 'lib/GnuPGAgent.hs')
-rw-r--r--lib/GnuPGAgent.hs198
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 #-}
4module GnuPGAgent
5 ( session
6 , Query(..)
7 , QueryMode(..)
8 , getPassphrase
9 , clearPassphrase
10 , quit
11 , key_nbits) where
12
13import Debug.Trace
14import Control.Monad
15import Data.Char
16import Data.OpenPGP
17import Data.OpenPGP.Util
18import Network.Socket
19import System.Directory
20import System.Environment
21import System.IO
22import Text.Printf
23#if defined(VERSION_memory)
24import qualified Data.ByteString.Char8 as S8
25import Data.ByteArray.Encoding
26#elif defined(VERSION_dataenc)
27import qualified Codec.Binary.Base16 as Base16
28#endif
29import LengthPrefixedBE
30import qualified Data.ByteString.Lazy as L
31#if defined(VERSION_hourglass)
32import Data.Hourglass
33#else
34import Data.Time.Calendar
35import Data.Time.Clock
36import Data.Time.Clock.POSIX
37#endif
38import Data.Word
39
40data GnuPGAgent = GnuPGAgent { agentHandle :: Handle }
41
42session = 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
60percentPlusEscape :: String -> String
61percentPlusEscape 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
71clearPassphrase agent key = do
72 let cmd = "clear_passphrase --mode=normal "++fingerprint key
73 hPutStrLn (agentHandle agent) cmd
74
75data Query = Query
76 { queryPacket :: Packet
77 , queryUID :: String
78 , queryMainKey :: Maybe Packet
79 }
80 deriving Show
81
82data QueryMode = AskNot | AskAgain String | AskExisting | AskNew
83 deriving (Show,Eq,Ord)
84
85getPassphrase :: GnuPGAgent -> QueryMode -> Query -> IO (Maybe String)
86getPassphrase 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
115quit (GnuPGAgent h) = hClose h
116
117prompts :: Packet -> String -> Maybe Packet -> (String,String,String)
118prompts 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
147data HomeDir = HomeDir { homevar :: String, appdir :: String }
148gpgHomeSpec :: HomeDir
149gpgHomeSpec = HomeDir
150 { homevar = "GNUPGHOME"
151 , appdir = ".gnupg"
152 }
153
154envhomedir 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
162timeString :: Word32 -> String
163timeString 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
172key_nbits :: Packet -> Int
173key_nbits p@(SecretKeyPacket {}) = _key_nbits (key_algorithm p) (key p)
174key_nbits p@(PublicKeyPacket {}) = _key_nbits (key_algorithm p) (key p)
175key_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
185curve_oid_nbits :: MPI -> Int
186curve_oid_nbits (MPI 0x2a8648ce3d030107 ) = 256 -- SEC p256r1 ( NIST P-256 )
187curve_oid_nbits (MPI 0x2B81040022 ) = 384 -- SEC p384r1 ( NIST P-384 )
188curve_oid_nbits (MPI 0x2B81040023 ) = 521 -- SEC p521r1 ( NIST P-521 )
189curve_oid_nbits (MPI 0x2b8104000a ) = 256 -- SEC p256k1 ( bitcoin curve )
190curve_oid_nbits n = trace ("Unknown curve: "++ show n) 0
191
192
193mpi_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