summaryrefslogtreecommitdiff
path: root/lib/PacketTranscoder.hs
diff options
context:
space:
mode:
Diffstat (limited to 'lib/PacketTranscoder.hs')
-rw-r--r--lib/PacketTranscoder.hs306
1 files changed, 306 insertions, 0 deletions
diff --git a/lib/PacketTranscoder.hs b/lib/PacketTranscoder.hs
new file mode 100644
index 0000000..f4b4cce
--- /dev/null
+++ b/lib/PacketTranscoder.hs
@@ -0,0 +1,306 @@
1{-# LANGUAGE TupleSections #-}
2{-# LANGUAGE OverloadedStrings #-}
3{-# LANGUAGE PatternGuards #-}
4module PacketTranscoder where
5
6import GHC.Stack
7import Control.Monad
8import Data.IORef
9import Data.List
10import Data.Maybe
11import Data.OpenPGP
12import Data.OpenPGP.Util
13import GnuPGAgent
14import qualified Data.ByteString as S
15import qualified Data.ByteString.Char8 as S8
16import Data.Map as Map (Map)
17import qualified Data.Map as Map
18import qualified Data.Traversable as Traversable
19import System.IO ( stderr)
20import System.Posix.IO ( fdToHandle )
21import Text.Show.Pretty as PP ( ppShow )
22import Types
23import ControlMaybe (handleIO_)
24
25-- | Merge two representations of the same key, prefering secret version
26-- because they have more information.
27mergeKeyPacket :: String -> MappedPacket -> MappedPacket -> MappedPacket
28mergeKeyPacket what key p =
29 key { packet = minimumBy (keyCompare what) [packet key,packet p]
30 , locations = Map.union (locations key) (locations p)
31 }
32
33-- | Compare different versions if the same key pair. Public versions
34-- are considered greater. If the two packets do not represent the same
35-- key or the packets are not keys at all, an error will result that
36-- includes the context provided as the first argument.
37keyCompare :: String -> Packet -> Packet -> Ordering
38keyCompare what (SecretKeyPacket {}) (PublicKeyPacket {}) = LT
39keyCompare what (PublicKeyPacket {}) (SecretKeyPacket {}) = GT
40keyCompare what a b | keykey a==keykey b = EQ
41keyCompare what a b = error $ unlines ["Unable to merge "++what++":"
42 , if isKey a then fingerprint a else ""
43 , PP.ppShow a
44 , if isKey b then fingerprint b else ""
45 , PP.ppShow b
46 ]
47
48resolveInputFile :: InputFileContext -> InputFile -> [FilePath]
49resolveInputFile ctx = resolve
50 where
51 resolve HomeSec = return (homesecPath ctx)
52 resolve HomePub = return (homepubPath ctx)
53 resolve (ArgFile f) = return f
54 resolve _ = []
55
56resolveForReport :: Maybe InputFileContext -> InputFile -> FilePath
57resolveForReport mctx (Pipe fdr fdw) = resolveForReport mctx (ArgFile str)
58 where str = case (fdr,fdw) of
59 (0,1) -> "-"
60 _ -> "&pipe" ++ show (fdr,fdw)
61resolveForReport mctx (FileDesc fd) = resolveForReport mctx (ArgFile str)
62 where str = "&" ++ show fd
63resolveForReport mctx f = concat $ resolveInputFile ctx f
64 where ctx = fromMaybe (InputFileContext "&secret" "&public") mctx
65
66readInputFileS :: InputFileContext -> InputFile -> IO S.ByteString
67readInputFileS ctx (Pipe fd _) = fdToHandle fd >>= S.hGetContents
68readInputFileS ctx (FileDesc fd) = fdToHandle fd >>= S.hGetContents
69readInputFileS ctx inp = do
70 let fname = resolveInputFile ctx inp
71 fmap S.concat $ mapM S.readFile fname
72
73
74
75-- | Reads contents of an 'InputFile' or returns the cached content from a prior call.
76-- An optional prompt is provided and will be printed on stdout only in the case that
77-- the provided 'InputFile' is 'FileDesc' 0 (i.e. stdin).
78cachedContents :: Maybe S.ByteString -> InputFileContext -> InputFile -> IO (IO S.ByteString)
79cachedContents maybePrompt ctx fd = do
80 ref <- newIORef Nothing
81 return $ get maybePrompt ref fd
82 where
83 trimCR bs = fst $ S.spanEnd (\x -> x==10 || x==13) bs
84
85 get maybePrompt ref fd = do
86 pw <- readIORef ref
87 flip (flip maybe return) pw $ do
88 if fd == FileDesc 0 then case maybePrompt of
89 Just prompt -> S.hPutStr stderr prompt
90 Nothing -> return ()
91 else return ()
92 pw <- fmap trimCR $ readInputFileS ctx fd
93 writeIORef ref (Just pw)
94 return pw
95
96
97
98makeMemoizingDecrypter :: KeyRingOperation -> InputFileContext
99 -> (Maybe MappedPacket, Map.Map KeyKey (OriginMapped Query))
100 -> IO PacketTranscoder
101makeMemoizingDecrypter operation ctx (workingkey,keys) = do
102 if null chains then do
103 -- (*) Notice we do not pass ctx to resolveForReport.
104 -- This is because the merge function does not currently use a context
105 -- and the pws map keys must match the MappedPacket locations.
106 -- TODO: Perhaps these should both be of type InputFile rather than
107 -- FilePath?
108 -- pws :: Map.Map FilePath (IO S.ByteString)
109 {-
110 -- This disabled code obtained password sources from StreamInfo records.
111 pws <-
112 Traversable.mapM (cachedContents ctx . fromJust . pwfile . typ)
113 (Map.mapKeys (resolveForReport Nothing) -- see note (*) note above
114 $ Map.filter (isJust . pwfile . typ) $ opFiles operation)
115 -}
116 let prompt = Just "Enter possibly multi-line passphrase (Press CTRL-D when finished):\n"
117 -- List of file-specific password sources.
118 pws2 <-
119 Traversable.mapM (cachedContents prompt ctx)
120 $ Map.fromList $ mapMaybe
121 (\spec -> (,passSpecPassFile spec) `fmap` do
122 guard $ isNothing $ passSpecKeySpec spec
123 passSpecRingFile spec)
124 passspecs
125 -- List of general password sources.
126 defpw <- do
127 Traversable.mapM (cachedContents prompt ctx . passSpecPassFile)
128 $ listToMaybe $ filter (\sp -> isNothing (passSpecRingFile sp)
129 && isNothing (passSpecKeySpec sp))
130 $ passspecs
131 unkeysRef <- newIORef (Map.empty :: Map.Map (KeyKey,SymmetricAlgorithm,S2K) Packet)
132 return $ doDecrypt unkeysRef ({- pws `Map.union` -} pws2) defpw (not $ null agentspec)
133 else let PassphraseMemoizer f = head chains
134 in return f
135 where
136 (chains,passspecs0) = partition isChain $ opPassphrases operation
137 where isChain (PassphraseMemoizer {}) = True
138 isChain _ = False
139 (agentspec,passspecs) = partition isAgent passspecs0
140 where isAgent PassphraseAgent = True
141 isAgent _ = False
142 doDecrypt :: IORef (Map.Map (KeyKey,SymmetricAlgorithm,S2K) Packet)
143 -> Map.Map FilePath (IO S.ByteString)
144 -> Maybe (IO S.ByteString)
145 -> Bool
146 -> (SymmetricAlgorithm,S2K)
147 -> MappedPacket
148 -> IO (KikiCondition Packet)
149 doDecrypt unkeysRef pws defpw agent_requested (dest_alg,dest_s2k) mp0 = do
150 unkeys <- readIORef unkeysRef
151 let (mp,qry) = fromMaybe (mp0,Query (packet mp0) "anonymous2" Nothing)
152 $ mplus (do k <- Map.lookup kk keys
153 return (mergeKeyPacket "decrypt" mp0 (fmap queryPacket k), packet k))
154 (do guard $ is_subkey (packet mp0)
155 working <- fmap packet workingkey
156 q <- fmap packet $ Map.lookup (keykey working) keys
157 return (mp0, Query (packet mp0) (queryUID q) (Just working)))
158
159 dest_s2k' | dest_alg==Unencrypted = S2K 100 ""
160 | otherwise = dest_s2k
161
162 wk = packet mp0
163 kk = keykey wk
164 fs = Map.keys $ locations mp
165
166 decryptIt [] = return BadPassphrase
167 decryptIt (getpw:getpws) = tries 1 getpw (decryptIt getpws)
168 where
169 tries count getpw recurse = do
170 -- TODO: This function should use mergeKeyPacket to
171 -- combine the packet with it's unspilled version before
172 -- attempting to decrypt it. Note: We are uninterested
173 -- in the 'locations' field, so this would effectively
174 -- allow you to run 'decryptIt' on an unencrypted public key
175 -- to obtain it's secret key.
176 handleIO_ (decryptIt []) $ do
177 (pw,wants_retry) <- getpw (count,qry)
178 let wkun = fromMaybe wk $ do
179 guard $ symmetric_algorithm (packet mp) /= Unencrypted
180 decryptSecretKey pw (packet mp)
181
182 retryOrFail
183 | Just clear <- wants_retry = if count < 4
184 then tries (count+1) getpw recurse
185 else clear >> recurse
186 | otherwise = recurse
187
188 case symmetric_algorithm wkun of
189
190 Unencrypted -> do
191 writeIORef unkeysRef (Map.insert (kk,Unencrypted,S2K 100 "") wkun unkeys)
192 ek <- case dest_alg of
193 Unencrypted -> return $ Just wkun
194 _ -> encryptSecretKey pw dest_s2k' dest_alg wkun
195
196 case ek of
197 Nothing -> retryOrFail
198 Just wken -> do
199 modifyIORef unkeysRef (Map.insert (kk,dest_alg,dest_s2k') wken)
200 return $ KikiSuccess wken
201
202 _ -> retryOrFail
203
204 getpws = (map (const . fmap (,Nothing)) $ mapMaybe (`Map.lookup` pws) fs ++ maybeToList defpw) ++ [ agentpw | agent_requested ]
205
206 agentpw (count,qry) = do
207 s <- session
208 fromMaybe (return ("",Nothing)) $ do
209 s <- s
210 Just $ do
211 let (firsttime,maink) | Just k <- (queryMainKey qry) = (2,k)
212 | otherwise = (1,error "bug in makeMemoizingDecrypter")
213
214 alg = symmetric_algorithm (queryPacket qry)
215
216 ask | count<firsttime = AskNot
217 | count>firsttime = AskAgain "Bad passphrase"
218 | count==firsttime = initial_ask
219 where
220 initial_ask | Unencrypted <- alg = AskNew
221 | otherwise = AskExisting
222
223 actual_qry | count<firsttime = qry { queryPacket = maink, queryMainKey = Nothing }
224 | otherwise = qry
225
226 let clear | count > firsttime = clearPassphrase s (queryPacket qry)
227 | otherwise = return ()
228 clear
229 let sanitizeQry qry = (fingerprint $ queryPacket qry, queryUID qry, fmap fingerprint $ queryMainKey qry)
230 -- putStrLn $ "(count,firsttime,ask,qry,actual_qry)="++show (count,firsttime,ask,sanitizeQry qry, sanitizeQry actual_qry)
231 mbpw <- getPassphrase s ask actual_qry
232 quit s
233 return ( maybe "" S8.pack mbpw, guard (ask /= AskNew) >> Just clear )
234
235 calls <- currentCallStack
236 putStrLn $ concat [fingerprint wk," ", show (symmetric_algorithm wk,s2k wk)," --> ",show (dest_alg,dest_s2k)]
237 mapM_ putStrLn calls
238 if symmetric_algorithm wk == dest_alg && s2k wk == dest_s2k'
239 then return (KikiSuccess wk)
240 else maybe (decryptIt getpws)
241 (return . KikiSuccess)
242 $ Map.lookup (kk,dest_alg,dest_s2k') unkeys
243
244keyQueries :: Maybe String -> Map InputFile (StreamInfo,Message) -> (Maybe MappedPacket, Map KeyKey (OriginMapped Query))
245keyQueries grip ringPackets = (mwk, fmap makeQuery keys)
246 where
247 makeQuery (maink,mp,us) = mp { packet = q }
248 where q = Query { queryPacket = packet mp
249 , queryUID = concat $ take 1 $ Map.keys $ Map.union us (getUIDS maink)
250 , queryMainKey = if is_subkey (packet mp)
251 then maink `mplus` fmap packet mwk
252 else Nothing
253 }
254
255 getUIDS maink = fromMaybe Map.empty $ do
256 k <- maink
257 (_,_,mus) <- Map.lookup (keykey k) keys
258 return mus
259
260 -- | mwk
261 -- first master key matching the provided grip
262 -- (the m is for "MappedPacket", wk for working key)
263 mwk :: Maybe MappedPacket
264 mwk = listToMaybe $ do
265 fp <- maybeToList grip
266 let matchfp mp
267 | not (is_subkey p) && matchpr fp p == fp = Just mp
268 | otherwise = Nothing
269 where p = packet mp
270 Map.elems $ Map.mapMaybe matchfp $ fmap (\(_,p,_) -> p) $ keys
271
272 keys = Map.foldl slurpkeys Map.empty
273 $ Map.mapWithKey filterSecrets ringPackets
274 where
275 filterSecrets :: InputFile -> (a,Message) -> [[MappedPacket]]
276 filterSecrets f (_,Message ps) = keygroups
277 -- filter (isSecretKey . packet) mps
278 where
279 mps = zipWith (mappedPacketWithHint fname) ps [1..]
280 fname = resolveForReport Nothing f -- (Just ctx) f
281 keygroups = dropWhile (not . isSecretKey . packet . head)
282 $ groupBy (const $ not . isSecretKey . packet) mps
283 slurpkeys :: (Map KeyKey (Maybe Packet,MappedPacket,Map String [Packet]))
284 -> [[MappedPacket]]
285 -> (Map KeyKey (Maybe Packet, MappedPacket,Map String [Packet]))
286 slurpkeys m pss = Map.unionWith combineKeyKey m m2
287 where
288
289 m2 :: Map.Map KeyKey (Maybe Packet, MappedPacket, (Map.Map String [Packet]))
290 m2 = Map.fromList
291 $ drop 1
292 $ scanl' build ([],(Nothing,error "bug in PacketTranscoder (1)", error "bug in PacketTranscoder (2)")) pss
293 where
294 build (_,(main0,_,_)) ps = (kk,(main,kp,uidmap ps'))
295 where
296 main | is_subkey (packet kp) = main0
297 | otherwise = Just $ packet kp
298 (kpkt,ps') = splitAt 1 ps
299 kp = head kpkt
300 kk = keykey . packet $ kp
301 combineKeyKey (master1,mp,um) (master2,mp2,um2) = (master1 `mplus` master2,mp,Map.unionWith (++) um um2)
302 uidmap ps = um2
303 where
304 ugs = dropWhile (not . isUserID . packet .head) $ groupBy (const $ not . isUserID . packet) ps
305 um2 = Map.fromList
306 $ map (\(MappedPacket (UserIDPacket s) _:sigs)->(s,takeWhile isSignaturePacket $ map packet sigs)) ugs