summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cokiki.hs22
-rw-r--r--kiki.cabal2
-rw-r--r--kiki.hs124
-rw-r--r--lib/Kiki.hs169
-rw-r--r--lib/LengthPrefixedBE.hs (renamed from LengthPrefixedBE.hs)0
-rw-r--r--lib/SSHKey.hs (renamed from SSHKey.hs)0
6 files changed, 180 insertions, 137 deletions
diff --git a/cokiki.hs b/cokiki.hs
index daf2be5..899608b 100644
--- a/cokiki.hs
+++ b/cokiki.hs
@@ -23,6 +23,7 @@ usage = unlines
23 , " strongswan" 23 , " strongswan"
24 ] 24 ]
25 25
26ㄧchroot :: Args (FilePath -> FilePath)
26ㄧchroot = pure (\r a -> slash r a) <*> arg "--chroot" <|> pure id 27ㄧchroot = pure (\r a -> slash r a) <*> arg "--chroot" <|> pure id
27 where 28 where
28 slash :: String -> String -> String 29 slash :: String -> String -> String
@@ -38,11 +39,11 @@ main = do
38 | uid==0 = action 39 | uid==0 = action
39 | otherwise = hPutStrLn stderr "operation requires root." 40 | otherwise = hPutStrLn stderr "operation requires root."
40 let sel = case cmd of 41 let sel = case cmd of
41 ["ssh-client"] -> pure (sshClient uid) <*> ㄧchroot 42 ["ssh-client"] -> pure (sshClient uid) <*> ㄧchroot <*> Kiki.ㄧhomedir
42 ["ssh-server"] -> pure (whenRoot sshServer) 43 ["ssh-server"] -> pure (whenRoot sshServer)
43 ["strongswan"] -> pure (whenRoot strongswan) 44 ["strongswan"] -> pure (whenRoot strongswan)
44 _ -> pure $ hPutStr stderr usage 45 _ -> pure $ hPutStr stderr usage
45 spec = fancy [("--chroot",1)] [] "" 46 spec = fancy [("--chroot",1),("--passphrase-fd",1),("--homedir",1)] [] ""
46 case runArgs (parseInvocation spec args) sel of 47 case runArgs (parseInvocation spec args) sel of
47 Left e -> hPutStrLn stderr $ usageErrorMessage e 48 Left e -> hPutStrLn stderr $ usageErrorMessage e
48 Right io -> io 49 Right io -> io
@@ -51,7 +52,7 @@ maybeReadFile :: FilePath -> IO (Maybe L.ByteString)
51maybeReadFile path = do 52maybeReadFile path = do
52 doesFileExist path >>= bool (return Nothing) (Just <$> L.readFile path) 53 doesFileExist path >>= bool (return Nothing) (Just <$> L.readFile path)
53 54
54sshClient uid root = do 55sshClient uid root cmn = do
55 -- /etc/ssh/ssh_config <-- 'GlobalKnownHostsFile /var/cache/kiki/ssh_known_hosts' 56 -- /etc/ssh/ssh_config <-- 'GlobalKnownHostsFile /var/cache/kiki/ssh_known_hosts'
56 sshconfig <- parseSshConfig . fromMaybe "" <$> maybeReadFile (root "/etc/ssh/ssh_config") 57 sshconfig <- parseSshConfig . fromMaybe "" <$> maybeReadFile (root "/etc/ssh/ssh_config")
57 let (ps,qs) = sshSplitAtDirective "GlobalKnownHostsFile" sshconfig 58 let (ps,qs) = sshSplitAtDirective "GlobalKnownHostsFile" sshconfig
@@ -64,20 +65,21 @@ sshClient uid root = do
64 d:ds | elem "/var/cache/kiki/ssh_known_hosts" d 65 d:ds | elem "/var/cache/kiki/ssh_known_hosts" d
65 -> do hPutStrLn stderr "ssh-client already configured." 66 -> do hPutStrLn stderr "ssh-client already configured."
66 return Nothing 67 return Nothing
67 d:ds -> do hPutStrLn stderr "modifying GlobalKnownHostsFile not implemented" 68 d:ds -> do hPutStrLn stderr "modifying GlobalKnownHostsFile directive"
68 return Nothing 69 let hs = " " : "/var/cache/kiki/ssh_known_hosts" : drop 1 d
69 [] -> do 70 stmt = take 1 d ++ hs
71 return $ Just (ps ++ stmt : ds)
72 [] -> do -- Unconfigured add fresh directive.
70 let stmt = L8.unwords ["GlobalKnownHostsFile" 73 let stmt = L8.unwords ["GlobalKnownHostsFile"
71 , "/var/cache/kiki/ssh_known_hosts" 74 , "/var/cache/kiki/ssh_known_hosts"
72 , "/etc/ssh/ssh_known_hosts" 75 , "/etc/ssh/ssh_known_hosts"
73 , "/etc/ssh/ssh_known_hosts2" 76 , "/etc/ssh/ssh_known_hosts2"
74 ] 77 ]
75 return $ Just (sshconfig ++ parseSshConfig stmt) 78 return $ Just (sshconfig ++ parseSshConfig stmt)
76 -- sshconfig' `deepseq` return () -- force lazy input 79 sshconfig' `deepseq` return () -- force lazy input
77 maybe (return ()) (myWriteFile (root "/etc/ssh/ssh_config") . unparseSshConfig) sshconfig' 80 maybe (return ()) (myWriteFile (root "/etc/ssh/ssh_config") . unparseSshConfig) sshconfig'
78 -- /root/.gnupg/... <-- contains known hosts from /etc/ssh/ssh_known_hosts 81 -- /root/.gnupg/... <-- contains known hosts from /etc/ssh/ssh_known_hosts
79 82 Kiki.refresh root cmn
80 -- Kiki.refresh
81 -- /var/cache/kiki/ssh_known_hosts <-- contains known hosts from /root/.gnupg/... 83 -- /var/cache/kiki/ssh_known_hosts <-- contains known hosts from /root/.gnupg/...
82 84
83sshServer = do 85sshServer = do
@@ -87,7 +89,7 @@ sshServer = do
87strongswan = do 89strongswan = do
88 -- etc/ipsec.conf <-- 'include /var/cache/kiki/ipsec.conf' 90 -- etc/ipsec.conf <-- 'include /var/cache/kiki/ipsec.conf'
89 -- /root/.gnupg/... <-- contains newly-generated ipsec subkey 91 -- /root/.gnupg/... <-- contains newly-generated ipsec subkey
90 Kiki.refresh 92 Kiki.refresh id (Kiki.CommonArgsParsed Nothing Nothing)
91 -- /var/cache/kiki/ipsec.conf <-- contains configurations for each remote ipsec host 93 -- /var/cache/kiki/ipsec.conf <-- contains configurations for each remote ipsec host
92 -- /var/cache/kiki/ipsec.conf <-- contains '%default' configuration for this local host 94 -- /var/cache/kiki/ipsec.conf <-- contains '%default' configuration for this local host
93 -- /var/cache/kiki/ipsec.d/certs <-- contains relevant certs 95 -- /var/cache/kiki/ipsec.d/certs <-- contains relevant certs
diff --git a/kiki.cabal b/kiki.cabal
index 5ed7a4c..b64b87e 100644
--- a/kiki.cabal
+++ b/kiki.cabal
@@ -78,6 +78,8 @@ library
78 CryptoCoins, 78 CryptoCoins,
79 ProcessUtils, 79 ProcessUtils,
80 Hosts, 80 Hosts,
81 SSHKey,
82 LengthPrefixedBE,
81 CommandLine, 83 CommandLine,
82 Numeric.Interval, 84 Numeric.Interval,
83 Numeric.Interval.Bounded, 85 Numeric.Interval.Bounded,
diff --git a/kiki.hs b/kiki.hs
index 0284ff9..842e697 100644
--- a/kiki.hs
+++ b/kiki.hs
@@ -66,14 +66,9 @@ import qualified SSHKey as SSH
66import Text.Printf 66import Text.Printf
67import qualified DNSKey as DNS 67import qualified DNSKey as DNS
68import Data.Time.Clock.POSIX ( posixSecondsToUTCTime ) 68import Data.Time.Clock.POSIX ( posixSecondsToUTCTime )
69import Kiki
69import Debug.Trace 70import Debug.Trace
70 71
71#if !MIN_VERSION_base(4,8,0)
72sortOn :: Ord b => (a -> b) -> [a] -> [a]
73sortOn f =
74 map snd . sortBy (comparing fst) . map (\x -> let y = f x in y `seq` (y, x))
75#endif
76
77-- {-# ANN module ("HLint: ignore Eta reduce"::String) #-} 72-- {-# ANN module ("HLint: ignore Eta reduce"::String) #-}
78-- {-# ANN module ("HLint: ignore Use camelCase"::String) #-} 73-- {-# ANN module ("HLint: ignore Use camelCase"::String) #-}
79 74
@@ -134,8 +129,6 @@ sortOn f =
134 - 129 -
135 -} 130 -}
136 131
137warn str = hPutStrLn stderr str
138
139 132
140isCertificationSig (CertificationSignature {}) = True 133isCertificationSig (CertificationSignature {}) = True
141isCertificationSig _ = True 134isCertificationSig _ = True
@@ -307,21 +300,6 @@ show_whose_key input_key db =
307 300
308show_dns keyspec wkgrip db = either warn putStrLn $ show_pem' keyspec wkgrip db dnsPresentationFromPacket 301show_dns keyspec wkgrip db = either warn putStrLn $ show_pem' keyspec wkgrip db dnsPresentationFromPacket
309 302
310show_pem keyspec wkgrip db = either warn putStrLn $ show_pem' keyspec wkgrip db pemFromPacket
311
312show_pem' keyspec wkgrip db keyfmt = do
313 let s = parseSpec wkgrip keyspec
314 flip (maybe . Left $ keyspec ++ ": not found")
315 (selectPublicKey s db)
316 keyfmt
317
318pemFromPacket k = do
319 let rsa = pkcs8 . fromJust $ rsaKeyFromPacket k
320 der = encodeASN1 DER (toASN1 rsa [])
321 qq = Base64.encode (L.unpack der)
322 return $
323 writePEM "PUBLIC KEY" qq -- ("TODO "++show keyspec)
324
325dnsPresentationFromPacket k = do 303dnsPresentationFromPacket k = do
326 let RSAKey (MPI n) (MPI e) = fromJust $ rsaKeyFromPacket k 304 let RSAKey (MPI n) (MPI e) = fromJust $ rsaKeyFromPacket k
327 dnskey = DNS.RSA n e 305 dnskey = DNS.RSA n e
@@ -341,20 +319,6 @@ dnsPresentationFromPacket k = do
341 ,qq 319 ,qq
342 ] 320 ]
343 321
344show_ssh keyspec wkgrip db = either warn putStrLn $ show_ssh' keyspec wkgrip db
345
346show_ssh' keyspec wkgrip db = do
347 let s = parseSpec wkgrip keyspec
348 flip (maybe . Left $ keyspec ++ ": not found")
349 (selectPublicKey s db)
350 $ return . sshblobFromPacket
351
352sshblobFromPacket k = blob
353 where
354 Just (RSAKey (MPI n) (MPI e)) = rsaKeyFromPacket k
355 bs = SSH.keyblob (n,e)
356 blob = Char8.unpack bs
357
358show_id keyspec wkgrip db = do 322show_id keyspec wkgrip db = do
359 let s = parseSpec "" keyspec 323 let s = parseSpec "" keyspec
360 let ps = do 324 let ps = do
@@ -1029,8 +993,6 @@ processArgs sargspec polyVariadicArgs defaultPoly args_raw = (sargs,margs)
1029 else error . unlines $ [ "unrecognized option "++k 993 else error . unlines $ [ "unrecognized option "++k
1030 , "Use --help for usage." ] 994 , "Use --help for usage." ]
1031 995
1032data CommonArgsParsed = CommonArgsParsed { cap_homespec :: Maybe String, cap_passfd :: Maybe InputFile }
1033
1034parseCommonArgs margs = CommonArgsParsed { cap_homespec = homespec, cap_passfd = passfd } 996parseCommonArgs margs = CommonArgsParsed { cap_homespec = homespec, cap_passfd = passfd }
1035 where 997 where
1036 passphrase_fd = concat <$> Map.lookup "--passphrase-fd" margs 998 passphrase_fd = concat <$> Map.lookup "--passphrase-fd" margs
@@ -1658,55 +1620,6 @@ kiki "tar" args = do
1658 ["-A":_] -> putStrLn "unimplemented." -- import tar file? 1620 ["-A":_] -> putStrLn "unimplemented." -- import tar file?
1659 _ -> kiki "tar" ["--help"] 1621 _ -> kiki "tar" ["--help"]
1660 1622
1661refreshCache rt rootdir = do
1662
1663 let mkpath pth = fromMaybe "" rootdir ++ "/var/cache/kiki/"++pth
1664
1665 write f bs = do
1666 createDirectoryIfMissing True $ takeDirectory f
1667 writeFile f bs
1668
1669 let oname' = do wk <- rtWorkingKey rt
1670 -- XXX unnecessary signature check
1671 onionNameForContact (keykey wk) (rtKeyDB rt)
1672 bUnprivileged = False -- TODO
1673 if (oname' == Nothing) && (not bUnprivileged) then error "Missing tor key" else do
1674 let oname = fromMaybe "" oname'
1675 -- sshcpathpub0 = fromMaybe "" rootdir ++ osHomeDir </> ".ssh" </> "id_rsa.pub"
1676 -- sshspathpub0 = fromMaybe "" rootdir ++ "/etc/ssh/ssh_host_rsa_key.pub"
1677 -- contactipsec0 = fromMaybe "" rootdir ++ "/etc/ipsec.d/certs/%(onion).pem"
1678
1679 -- Finally, export public keys if they do not exist.
1680 flip (maybe $ warn "missing working key?") (rtGrip rt) $ \grip -> do
1681 either warn (write $ mkpath "root/.ssh/id_rsa.pub")
1682 $ show_ssh' "ssh-client" grip (rtKeyDB rt)
1683 either warn (write $ mkpath "ssh_host_rsa_key.pub")
1684 $ show_ssh' "ssh-server" grip (rtKeyDB rt)
1685 either warn (write $ mkpath "ipsec.d/certs/" ++ oname++".pem")
1686 $ show_pem' "ipsec" grip (rtKeyDB rt) pemFromPacket
1687
1688 let cs = filter notme (Map.elems $ rtKeyDB rt)
1689 kk = keykey (fromJust $ rtWorkingKey rt)
1690 notme kd = keykey (keyPacket kd) /= kk
1691
1692 installConctact kd = do
1693 -- The getHostnames command requires a valid cross-signed tor key
1694 -- for each onion name returned in (_,(ns,_)).
1695 let (_,(ns,_)) = getHostnames kd
1696 contactname = fmap Char8.unpack $ listToMaybe ns -- only first onion name.
1697 flip (maybe $ return ()) contactname $ \contactname -> do
1698
1699 let cpath = interp (Map.singleton "onion" contactname) "ipsec.d/certs/%(onion).pem"
1700 their_master = packet $ keyMappedPacket kd
1701 -- We find all cross-certified ipsec keys for the given cross-certified onion name.
1702 ipsecs = sortOn (Down . timestamp)
1703 $ getCrossSignedSubkeys their_master (keySubKeys kd) "ipsec"
1704 forM_ (take 1 ipsecs) $ \k -> do
1705 either warn (write $ mkpath cpath) $ pemFromPacket k
1706
1707 mapM_ installConctact cs
1708
1709
1710tarContent rt spec pubpem knownhosts secpem = ipsecs ++ sshs ++ secrets "root" 1623tarContent rt spec pubpem knownhosts secpem = ipsecs ++ sshs ++ secrets "root"
1711 where 1624 where
1712 ipsecs = do 1625 ipsecs = do
@@ -1811,27 +1724,6 @@ tarC (sargs,margs) = do
1811 hPutStrLn stderr $ "Failed to decrypt "++fingerprint k++"." 1724 hPutStrLn stderr $ "Failed to decrypt "++fingerprint k++"."
1812 return Nothing 1725 return Nothing
1813 1726
1814minimalOp :: CommonArgsParsed -> KeyRingOperation
1815minimalOp cap = op
1816 where
1817 streaminfo = StreamInfo { fill = KF_None
1818 , typ = KeyRingFile
1819 , spill = KF_All
1820 , initializer = NoCreate
1821 , access = AutoAccess
1822 , transforms = []
1823 }
1824 op = KeyRingOperation
1825 { opFiles = Map.fromList $
1826 [ ( HomeSec, streaminfo { access = Sec })
1827 , ( HomePub, streaminfo { access = Pub })
1828 ]
1829 , opPassphrases = do pfile <- maybeToList (cap_passfd cap)
1830 return $ PassphraseSpec Nothing Nothing pfile
1831 , opTransforms = []
1832 , opHome = cap_homespec cap
1833 }
1834
1835-- | 1727-- |
1836-- 1728--
1837-- no leading hyphen, returns Right (input string). 1729-- no leading hyphen, returns Right (input string).
@@ -1872,20 +1764,6 @@ commands =
1872 , ( "tar", "import or export system key files in tar format" ) 1764 , ( "tar", "import or export system key files in tar format" )
1873 ] 1765 ]
1874 1766
1875-- |
1876-- interpolate %var patterns in a string.
1877interp vars raw = es >>= interp1
1878 where
1879 gs = groupBy (\_ c -> c/='%') raw
1880 es = dropWhile null $ gobbleEscapes ("":gs)
1881 where gobbleEscapes :: [String] -> [String]
1882 gobbleEscapes (a:"%":b:bs) = (a++b) : gobbleEscapes bs
1883 gobbleEscapes (g:gs) = g : gobbleEscapes gs
1884 gobbleEscapes [] = []
1885 interp1 ('%':'(':str) = fromMaybe "" (Map.lookup key vars) ++ drop 1 rest
1886 where (key,rest) = break (==')') str
1887 interp1 plain = plain
1888
1889main = do 1767main = do
1890 dotlock_init 1768 dotlock_init
1891 args_raw <- getArgs 1769 args_raw <- getArgs
diff --git a/lib/Kiki.hs b/lib/Kiki.hs
index 783b6ed..575cf26 100644
--- a/lib/Kiki.hs
+++ b/lib/Kiki.hs
@@ -1,8 +1,169 @@
1{-# LANGUAGE CPP #-}
1module Kiki where 2module Kiki where
2 3
4import Control.Monad
5import Control.Applicative
6import Data.List
7import Data.Maybe
8import Data.Ord
9import System.Directory
10import System.FilePath.Posix
11import System.IO
12import Data.OpenPGP
13import Data.OpenPGP.Util
14import qualified Data.Map.Strict as Map
15import qualified Codec.Binary.Base64 as Base64
16import Data.ASN1.BinaryEncoding
17import Data.ASN1.Encoding
18import Data.ASN1.Types
19import qualified Data.ByteString.Lazy as L
20import qualified Data.ByteString.Lazy.Char8 as Char8
21
22import CommandLine
23import qualified SSHKey as SSH
24import KeyRing
25
3-- | 26-- |
4-- Regenerate /var/cache/kiki 27-- Regenerate /var/cache/kiki
5refresh :: IO () 28refresh :: (FilePath -> FilePath) -> CommonArgsParsed -> IO ()
6refresh = do 29refresh root homepass = do
7 -- TODO 30 let homepass' = homepass { cap_homespec = fmap root (cap_homespec homepass) }
8 return () 31 KikiResult r report <- runKeyRing $ minimalOp homepass'
32 let mroot = case root "" of
33 "/" -> Nothing
34 "" -> Nothing
35 pth -> Just pth
36 case r of
37 KikiSuccess rt -> refreshCache rt mroot
38 _ -> return () -- XXX: silent fail?
39
40data CommonArgsParsed = CommonArgsParsed { cap_homespec :: Maybe String, cap_passfd :: Maybe InputFile }
41
42
43minimalOp :: CommonArgsParsed -> KeyRingOperation
44minimalOp cap = op
45 where
46 streaminfo = StreamInfo { fill = KF_None
47 , typ = KeyRingFile
48 , spill = KF_All
49 , initializer = NoCreate
50 , access = AutoAccess
51 , transforms = []
52 }
53 op = KeyRingOperation
54 { opFiles = Map.fromList $
55 [ ( HomeSec, streaminfo { access = Sec })
56 , ( HomePub, streaminfo { access = Pub })
57 ]
58 , opPassphrases = do pfile <- maybeToList (cap_passfd cap)
59 return $ PassphraseSpec Nothing Nothing pfile
60 , opTransforms = []
61 , opHome = cap_homespec cap
62 }
63
64
65refreshCache :: KeyRingRuntime -> Maybe FilePath -> IO ()
66refreshCache rt rootdir = do
67
68 let mkpath pth = fromMaybe "" rootdir ++ "/var/cache/kiki/"++pth
69
70 write f bs = do
71 createDirectoryIfMissing True $ takeDirectory f
72 writeFile f bs
73
74 let oname' = do wk <- rtWorkingKey rt
75 -- XXX unnecessary signature check
76 onionNameForContact (keykey wk) (rtKeyDB rt)
77 bUnprivileged = False -- TODO
78 if (oname' == Nothing) && (not bUnprivileged) then error "Missing tor key" else do
79 let oname = fromMaybe "" oname'
80 -- sshcpathpub0 = fromMaybe "" rootdir ++ osHomeDir </> ".ssh" </> "id_rsa.pub"
81 -- sshspathpub0 = fromMaybe "" rootdir ++ "/etc/ssh/ssh_host_rsa_key.pub"
82 -- contactipsec0 = fromMaybe "" rootdir ++ "/etc/ipsec.d/certs/%(onion).pem"
83
84 -- Finally, export public keys if they do not exist.
85 flip (maybe $ warn "missing working key?") (rtGrip rt) $ \grip -> do
86 either warn (write $ mkpath "root/.ssh/id_rsa.pub")
87 $ show_ssh' "ssh-client" grip (rtKeyDB rt)
88 either warn (write $ mkpath "ssh_host_rsa_key.pub")
89 $ show_ssh' "ssh-server" grip (rtKeyDB rt)
90 either warn (write $ mkpath "ipsec.d/certs/" ++ oname++".pem")
91 $ show_pem' "ipsec" grip (rtKeyDB rt) pemFromPacket
92
93 let cs = filter notme (Map.elems $ rtKeyDB rt)
94 kk = keykey (fromJust $ rtWorkingKey rt)
95 notme kd = keykey (keyPacket kd) /= kk
96
97 installConctact kd = do
98 -- The getHostnames command requires a valid cross-signed tor key
99 -- for each onion name returned in (_,(ns,_)).
100 let (_,(ns,_)) = getHostnames kd
101 contactname = fmap Char8.unpack $ listToMaybe ns -- only first onion name.
102 flip (maybe $ return ()) contactname $ \contactname -> do
103
104 let cpath = interp (Map.singleton "onion" contactname) "ipsec.d/certs/%(onion).pem"
105 their_master = packet $ keyMappedPacket kd
106 -- We find all cross-certified ipsec keys for the given cross-certified onion name.
107 ipsecs = sortOn (Down . timestamp)
108 $ getCrossSignedSubkeys their_master (keySubKeys kd) "ipsec"
109 forM_ (take 1 ipsecs) $ \k -> do
110 either warn (write $ mkpath cpath) $ pemFromPacket k
111
112 mapM_ installConctact cs
113
114
115#if !MIN_VERSION_base(4,8,0)
116sortOn :: Ord b => (a -> b) -> [a] -> [a]
117sortOn f =
118 map snd . sortBy (comparing fst) . map (\x -> let y = f x in y `seq` (y, x))
119#endif
120
121pemFromPacket k = do
122 let rsa = pkcs8 . fromJust $ rsaKeyFromPacket k
123 der = encodeASN1 DER (toASN1 rsa [])
124 qq = Base64.encode (L.unpack der)
125 return $
126 writePEM "PUBLIC KEY" qq -- ("TODO "++show keyspec)
127
128show_pem keyspec wkgrip db = either warn putStrLn $ show_pem' keyspec wkgrip db pemFromPacket
129
130show_pem' keyspec wkgrip db keyfmt = do
131 let s = parseSpec wkgrip keyspec
132 flip (maybe . Left $ keyspec ++ ": not found")
133 (selectPublicKey s db)
134 keyfmt
135
136warn str = hPutStrLn stderr str
137
138show_ssh keyspec wkgrip db = either warn putStrLn $ show_ssh' keyspec wkgrip db
139
140show_ssh' keyspec wkgrip db = do
141 let s = parseSpec wkgrip keyspec
142 flip (maybe . Left $ keyspec ++ ": not found")
143 (selectPublicKey s db)
144 $ return . sshblobFromPacket
145
146-- |
147-- interpolate %var patterns in a string.
148interp vars raw = es >>= interp1
149 where
150 gs = groupBy (\_ c -> c/='%') raw
151 es = dropWhile null $ gobbleEscapes ("":gs)
152 where gobbleEscapes :: [String] -> [String]
153 gobbleEscapes (a:"%":b:bs) = (a++b) : gobbleEscapes bs
154 gobbleEscapes (g:gs) = g : gobbleEscapes gs
155 gobbleEscapes [] = []
156 interp1 ('%':'(':str) = fromMaybe "" (Map.lookup key vars) ++ drop 1 rest
157 where (key,rest) = break (==')') str
158 interp1 plain = plain
159
160sshblobFromPacket k = blob
161 where
162 Just (RSAKey (MPI n) (MPI e)) = rsaKeyFromPacket k
163 bs = SSH.keyblob (n,e)
164 blob = Char8.unpack bs
165
166ㄧhomedir = Kiki.CommonArgsParsed
167 <$> optional (arg "--homedir")
168 <*> optional (FileDesc <$> read <$> arg "--passphrase-fd")
169
diff --git a/LengthPrefixedBE.hs b/lib/LengthPrefixedBE.hs
index 0ccd0e2..0ccd0e2 100644
--- a/LengthPrefixedBE.hs
+++ b/lib/LengthPrefixedBE.hs
diff --git a/SSHKey.hs b/lib/SSHKey.hs
index 488f55f..488f55f 100644
--- a/SSHKey.hs
+++ b/lib/SSHKey.hs