diff options
Diffstat (limited to 'lib/PacketTranscoder.hs')
-rw-r--r-- | lib/PacketTranscoder.hs | 306 |
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 #-} | ||
4 | module PacketTranscoder where | ||
5 | |||
6 | import GHC.Stack | ||
7 | import Control.Monad | ||
8 | import Data.IORef | ||
9 | import Data.List | ||
10 | import Data.Maybe | ||
11 | import Data.OpenPGP | ||
12 | import Data.OpenPGP.Util | ||
13 | import GnuPGAgent | ||
14 | import qualified Data.ByteString as S | ||
15 | import qualified Data.ByteString.Char8 as S8 | ||
16 | import Data.Map as Map (Map) | ||
17 | import qualified Data.Map as Map | ||
18 | import qualified Data.Traversable as Traversable | ||
19 | import System.IO ( stderr) | ||
20 | import System.Posix.IO ( fdToHandle ) | ||
21 | import Text.Show.Pretty as PP ( ppShow ) | ||
22 | import Types | ||
23 | import ControlMaybe (handleIO_) | ||
24 | |||
25 | -- | Merge two representations of the same key, prefering secret version | ||
26 | -- because they have more information. | ||
27 | mergeKeyPacket :: String -> MappedPacket -> MappedPacket -> MappedPacket | ||
28 | mergeKeyPacket 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. | ||
37 | keyCompare :: String -> Packet -> Packet -> Ordering | ||
38 | keyCompare what (SecretKeyPacket {}) (PublicKeyPacket {}) = LT | ||
39 | keyCompare what (PublicKeyPacket {}) (SecretKeyPacket {}) = GT | ||
40 | keyCompare what a b | keykey a==keykey b = EQ | ||
41 | keyCompare 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 | |||
48 | resolveInputFile :: InputFileContext -> InputFile -> [FilePath] | ||
49 | resolveInputFile 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 | |||
56 | resolveForReport :: Maybe InputFileContext -> InputFile -> FilePath | ||
57 | resolveForReport mctx (Pipe fdr fdw) = resolveForReport mctx (ArgFile str) | ||
58 | where str = case (fdr,fdw) of | ||
59 | (0,1) -> "-" | ||
60 | _ -> "&pipe" ++ show (fdr,fdw) | ||
61 | resolveForReport mctx (FileDesc fd) = resolveForReport mctx (ArgFile str) | ||
62 | where str = "&" ++ show fd | ||
63 | resolveForReport mctx f = concat $ resolveInputFile ctx f | ||
64 | where ctx = fromMaybe (InputFileContext "&secret" "&public") mctx | ||
65 | |||
66 | readInputFileS :: InputFileContext -> InputFile -> IO S.ByteString | ||
67 | readInputFileS ctx (Pipe fd _) = fdToHandle fd >>= S.hGetContents | ||
68 | readInputFileS ctx (FileDesc fd) = fdToHandle fd >>= S.hGetContents | ||
69 | readInputFileS 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). | ||
78 | cachedContents :: Maybe S.ByteString -> InputFileContext -> InputFile -> IO (IO S.ByteString) | ||
79 | cachedContents 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 | |||
98 | makeMemoizingDecrypter :: KeyRingOperation -> InputFileContext | ||
99 | -> (Maybe MappedPacket, Map.Map KeyKey (OriginMapped Query)) | ||
100 | -> IO PacketTranscoder | ||
101 | makeMemoizingDecrypter 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 | |||
244 | keyQueries :: Maybe String -> Map InputFile (StreamInfo,Message) -> (Maybe MappedPacket, Map KeyKey (OriginMapped Query)) | ||
245 | keyQueries 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 | ||