{-# LANGUAGE ViewPatterns #-} {-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} module Kiki ( module Kiki , setVerifyFlag ) where import Control.Applicative import Control.Exception import Control.Monad import Data.ASN1.BinaryEncoding import Data.ASN1.Encoding import Data.ASN1.Types import Data.Binary import Data.Bool import Data.Char import Data.Functor import Data.List import Data.Maybe import Data.OpenPGP import Data.OpenPGP.Util import Data.Ord import qualified Data.Traversable as T (mapM) import System.Directory import System.FilePath.Posix as FilePath import System.IO import System.IO.Error import System.IO.Temp import System.Posix.Files import System.Posix.Types (FileMode) import System.Posix.IO as Posix (createPipe) import System.Posix.User #if defined(VERSION_memory) import Data.ByteArray.Encoding import qualified Data.ByteString.Char8 as S8 #elif defined(VERSION_dataenc) import qualified Codec.Binary.Base64 as Base64 #endif import qualified Data.ByteString.Lazy as L import qualified Data.ByteString.Lazy.Char8 as Char8 import qualified Data.Map.Strict as Map import Network.Socket import ProcessUtils import qualified SSHKey as SSH import CommandLine import DotLock import GnuPGAgent (Query (..)) import KeyRing hiding (pemFromPacket) withAgent :: [PassphraseSpec] -> [PassphraseSpec] withAgent [] = [PassphraseAgent] withAgent ps = ps ciphername :: SymmetricAlgorithm -> String ciphername Unencrypted = "-" ciphername TripleDES = "3des" ciphername (SymmetricAlgorithm w8) = "cipher-"++show w8 ciphername c = map toLower $ show c cipherFromString :: String -> SymmetricAlgorithm cipherFromString "clear" = Unencrypted cipherFromString "unencrypted" = Unencrypted cipherFromString s = case filter ( (== s) . ciphername) ciphers of x:_ -> x -- _ | all isHexDigit s -> unhex s _ -> error $ "known ciphers: "++unwords (map ciphername ciphers) {- where #if defined(VERSION_memory) unhex hx = case convertFromBase Base16 (S8.pack hx) of Left e -> do -- Useful for debugging but insecure generally ;) -- putStrLn $ "convertFromBase error for input "++show hx++": "++show e return Nothing Right bs -> return $ Just $ S8.unpack bs #elif defined(VERSION_dataenc) unhex hx = maybe (return () {- putStrLn $ "dataenc error for input "++show hx -}) return $ fmap (map $ chr . fromIntegral) $ Base16.decode hx #endif -} ciphers :: [SymmetricAlgorithm] ciphers = takeWhile notFallback $ map toEnum $ [0..4]++[7..] where notFallback (SymmetricAlgorithm _) = False notFallback _ = True -- | -- Regenerate /var/cache/kiki refresh :: (FilePath -> FilePath) -> CommonArgsParsed -> IO () refresh root homepass = do let homepass' = homepass { cap_homespec = fmap root (cap_homespec homepass) } KikiResult r report <- runKeyRing $ minimalOp homepass' let mroot = case root "" of "/" -> Nothing "" -> Nothing pth -> Just pth case r of KikiSuccess rt -> refreshCache rt mroot _ -> return () -- XXX: silent fail? data CommonArgsParsed = CommonArgsParsed { cap_homespec :: Maybe String, cap_passfd :: Maybe InputFile } streaminfo :: StreamInfo streaminfo = StreamInfo { fill = KF_None , spill = KF_None , typ = KeyRingFile , initializer = NoCreate , access = AutoAccess , transforms = [] } minimalOp :: CommonArgsParsed -> KeyRingOperation minimalOp cap = op where streaminfo = StreamInfo { fill = KF_None , typ = KeyRingFile , spill = KF_All , initializer = NoCreate , access = AutoAccess , transforms = [] } op = KeyRingOperation { opFiles = Map.fromList $ [ ( HomeSec, streaminfo { access = Sec }) , ( HomePub, streaminfo { access = Pub }) ] , opPassphrases = withAgent $ do pfile <- maybeToList (cap_passfd cap) return $ PassphraseSpec Nothing Nothing pfile , opTransforms = [] , opHome = cap_homespec cap } run :: [String] -> Args (IO ()) -> IO () run args x = case runArgs (parseInvocation (uncurry fancy kikiOptions "") args) x of Left e -> hPutStrLn stderr $ usageErrorMessage e Right io -> io outputReport :: [(FilePath, KikiReportAction)] -> IO () outputReport report = do forM_ report $ \(fname,act) -> do putStrLn $ fname ++ ": " ++ reportString act spawn :: CommonArgsParsed -> SymmetricAlgorithm -> FilePath -> IO () spawn cmn cipher path = do putStrLn "TODO: implement keygen.sh replacement.sh" mkdirFor :: FilePath -> IO () mkdirFor path = do let dir = takeDirectory path createDirectoryIfMissing True dir -- | Useful default KeyRingFile StreamInfo. strm :: StreamInfo strm = StreamInfo { typ = KeyRingFile , fill = KF_None , spill = KF_All , access = AutoAccess , initializer = NoCreate , transforms = [] } -- | Convenience constructor for StreamInfo buildStreamInfo :: KeyFilter -> FileType -> StreamInfo buildStreamInfo rtyp ftyp = strm { typ = ftyp , fill = rtyp } -- | Convenience constuctor for Streaminfo generating a tagged subkey. peminfo :: Int -- ^ bits -> String -- ^ subkey tag. -> StreamInfo peminfo bits usage = StreamInfo { typ = PEMFile , fill = KF_None -- KF_Match usage , spill = KF_Match usage , access = Sec , initializer = Internal (GenRSA $ bits `div` 8) , transforms = [] } importAndRefresh :: (FilePath -> FilePath) -> CommonArgsParsed -> SymmetricAlgorithm -> IO () importAndRefresh root cmn cipher = do let rootdir = do guard (root "x" /= "x") Just $ root "" me <- getEffectiveUserID let noChrootArg = rootdir == Nothing bUnprivileged = (me/=0) && noChrootArg bool id (error "--chroot requires an argument") (rootdir==Just "") $ do let homespec = mplus (slash <$> rootdir <*> cap_homespec cmn) (fmap (++"/root/.gnupg") rootdir) passfd = cap_passfd cmn (home,secring,pubring,mbwk) <- unconditionally $ getHomeDir homespec osHomeDir <- if bUnprivileged then getHomeDirectory else return "/root" old_umask <- setFileCreationMask 0o077 -- Keyring files need to be created with proper mask. gotsec <- doesFileExist secring (torgen,pwds) <- bool id (const $ return (Generate 0 $ GenRSA $ 1024 `div` 8, [])) gotsec $ do -- We have no secring.gpg (and thus no master key). -- Since 'runKeyRing' cannot currently cope with this situation, we will -- generate a master-key and very minimal secring.gpg file. master_un <- generateKey (GenRSA $ 4096 `div` 8 ) <&> \k -> MappedPacket (k { is_subkey = False }) -- Set as master-key. Map.empty -- Packet occurs in no files. -- The user may desire the master key is encrypted on disk but this -- requires a password prompt. In order to have a decent prompt, it'd -- be nice if we could display the .onion hostname for the key. -- Therefore, we generate the tor key early. tor_un <- generateKey (GenRSA $ 1024 `div` 8 ) -- However, we'll postpone writing the tor key to the keyring and -- instead have the later all-in-one call to runKeyRing take care of -- it. That interface does not currently provide a way to accept -- in-memory input, so we'll create a fifo stream for it to read the -- key in PEM format. (read_tor,write_tor) <- Posix.createPipe rs <- writeKeyToFile (streaminfo { typ = PEMFile , access = Sec , spill = KF_Match "tor" , fill = KF_All }) (FileDesc write_tor) tor_un -- -- Currently disabled: show warnings and errors from the PEM generation. -- outputReport $ map (first show) rs let ctx = InputFileContext secring pubring -- Here we encrypt the master-key if neccessary. If no --passphrase-fd option -- was used, the user will receive prompts from gpg-agent. (master0,transcoder) <- do let main_passwds = withAgent $ do pfd <- maybeToList passfd return $ PassphraseSpec Nothing Nothing pfd uidentry = Map.singleton (keykey $ packet master_un) $ master_un { packet = Query (packet master_un) (torUIDFromKey tor_un) Nothing } cipher's2k <- do IteratedSaltedS2K _ salt _ <- randomS2K SHA1 return $ (cipher {- AES128 -}, IteratedSaltedS2K SHA1 salt (15 * 2^19)) transcoder <- makeMemoizingDecrypter main_passwds ctx (Just master_un, uidentry) master <- transcoder cipher's2k master_un return (master,transcoder) -- Finally, we write-out the secring.gpg file. case master0 of KikiSuccess master -> do mkdirFor secring writeInputFileL ctx HomeSec $ encode $ Message [master] putStrLn "Wrote master key" return (FileDesc read_tor, [PassphraseMemoizer transcoder]) er -> do hPutStrLn stderr ("warning: " ++ errorString er) hPutStrLn stderr "warning: keys will not be encrypted."; mkdirFor secring writeInputFileL ctx HomeSec $ encode $ Message [packet master_un] putStrLn "Wrote master key" return (FileDesc read_tor, []) -- If the public ring does not exist, then creating an empty file is -- sufficient to satisfy 'runKeyRing'. However, as we've already generated -- a key above, GnuPG will not like the unsynced state we are leaving these -- files. It's important at this point that, 'runKeyRing' actually occurs -- to fix things up. gotpub <- doesFileExist pubring when (not gotpub) $ do mkdirFor pubring writeInputFileL (InputFileContext secring pubring) HomePub ( encode $ Message [] ) setFileCreationMask old_umask -- We're done creating keyring files, so restore umask. let sshcpath = fromMaybe "" rootdir ++ osHomeDir ++ ".ssh/id_rsa" sshspath = fromMaybe "" rootdir ++ "/etc/ssh/ssh_host_rsa_key" op = KeyRingOperation { opFiles = Map.fromList $ [ ( HomeSec, buildStreamInfo KF_All KeyRingFile ) , ( HomePub, (buildStreamInfo KF_All KeyRingFile) { access = Pub } ) , ( torgen , case torgen of FileDesc _ -> StreamInfo { typ = PEMFile , fill = KF_Match "tor" , spill = KF_Match "tor" , access = Sec , initializer = NoCreate , transforms = [] } _ -> strm { spill = KF_Match "tor" }) , ( Generate 1 (GenRSA (2048 `div` 8)), strm { spill = KF_Match "ipsec" }) , ( ArgFile sshcpath, (peminfo 2048 "ssh-client") ) , ( ArgFile sshspath, (peminfo 2048 "ssh-server") ) , ( Generate 2 (GenRSA (4096 `div` 8)), strm { spill = KF_Match "encrypt" }) , ( Generate 3 (GenRSA (4096 `div` 8)), strm { spill = KF_Match "sign" }) ] , opPassphrases = withAgent $ pwds ++ do pfd <- maybeToList passfd return $ PassphraseSpec Nothing Nothing pfd , opHome = homespec , opTransforms = [] } nop = KeyRingOperation { opFiles = Map.fromList -- It's too late for a true no-op, -- so we'll sync the keyrings at least. [ ( HomeSec, buildStreamInfo KF_All KeyRingFile ) , ( HomePub, (buildStreamInfo KF_All KeyRingFile) { access = Pub } ) ] , opPassphrases = withAgent $ do pfd <- maybeToList passfd return $ PassphraseSpec Nothing Nothing pfd , opHome=homespec, opTransforms = [] } -- Run the all-in-one operation that generates or imports all subkeys. KikiResult rt report <- runKeyRing (if bUnprivileged then nop else op) outputReport report rt <- case rt of BadPassphrase -> error "Operation requires correct passphrase. (Hint: Use --passphrase-fd=0 to input it on stdin.)" _ -> unconditionally $ return rt -- Finally, we update /var/cache/kiki. when (not bUnprivileged) $ refreshCache rt rootdir -- Installs the cert file for the peer to the filesystem, and returns an -- ipsec.conf snippet configuring the peer and referencing the installed cert -- file. installIpsecPeerCertificate :: FileWriter -> (L.ByteString, SockAddr, KeyData) -> IO Char8.ByteString installIpsecPeerCertificate fw (contactname,addr,kd) = Char8.concat <$> do forM (take 1 ipsecs) $ \k -> do flip (either warn') (pemFromPacket k :: Either String String) $ \pem -> do case sshs of (sshkey:_) -> do theirHostname <- sshKeyToHostname sshkey write fw (ipsecCertPath theirHostname) pem return $ strongswanPeerConfiguration addr contactname theirHostname _ -> error "fuck." where warn' x = warn x >> return Char8.empty their_master = packet $ keyMappedPacket kd :: Packet -- We find all cross-certified ipsec keys for the given cross-certified onion name. ipsecs :: [Packet] ipsecs = sortOn (Down . timestamp) $ getSubkeys CrossSigned their_master (keySubKeys kd) "ipsec" -- ++ getSubkeys CrossSigned their_master (keySubKeys kd) "strongswan" sshs :: [Packet] sshs = sortOn (Down . timestamp) $ getSubkeys CrossSigned their_master (keySubKeys kd) "ssh-server" ipsecPath :: String -> Char8.ByteString -> String ipsecPath theDirName theBaseName = "ipsec.d/" ++ theDirName ++ "/" ++ Char8.unpack theBaseName ++ ".pem" ipsecKeyPath :: Char8.ByteString -> FilePath ipsecKeyPath = ipsecPath "private" ipsecCertPath :: Char8.ByteString -> FilePath ipsecCertPath = ipsecPath "certs" makeFileWriter :: (FilePath -> FilePath) -> (IO ()) -> FileWriter makeFileWriter p c = FileWriter { pathMaker = p , fileWriterCommit = c , write = write' writeFile , writeL = write' L.writeFile , writeL077 = \f bs -> do old_umask <- setFileCreationMask 0o077 write' L.writeFile f bs setFileCreationMask old_umask } where write' wr (p -> f) bs = do createDirectoryIfMissing True $ takeDirectory f wr f bs data FileWriter = FileWriter { pathMaker :: FilePath -> FilePath, fileWriterCommit :: IO (), write :: FilePath -> String -> IO (), writeL :: FilePath -> Char8.ByteString -> IO (), writeL077 :: FilePath -> Char8.ByteString -> IO FileMode } getMkPathAndCommit :: FilePath -> IO (FileWriter) getMkPathAndCommit destdir = do let cachedir = takeDirectory destdir unslash ('/':xs) = xs unslash xs = xs timeout = -1 -- TODO: set milisecond timeout on dotlock createDirectoryIfMissing True cachedir tmpdir <- createTempDirectory cachedir (takeBaseName destdir ++ ".") createSymbolicLink (makeRelative cachedir tmpdir) (tmpdir ++ ".link") lock <- dotlock_create destdir 0 T.mapM (flip dotlock_take timeout) lock let mkpath pth = tmpdir unslash (makeRelative destdir pth) commit = do oldcommit <- (Just <$> readSymbolicLink destdir) `catch` \e -> do when (not $ isDoesNotExistError e) $ warn (show e) return Nothing -- Note: Files not written to are considered deleted, -- otherwise call readyReadBeforeWrite on them. rename (tmpdir ++ ".link") destdir er <- T.mapM dotlock_release lock void $ T.mapM removeDirectoryRecursive (FilePath.combine cachedir <$> oldcommit) -- Present transaction is Write only (or Write-Before-Read) which is fine. -- If ever Read-Before-Write is required, uncomment and use: -- let readyReadBeforeWrite pth = do -- let copyIt = do -- createDirectoryIfMissing True (takeDirectory (mkpath pth)) -- copyFile (destdir unslash (makeRelative destdir pth) (mkpath pth) -- doesFileExist (mkpath pth) >>= flip when copyIt -- return (mkpath pth) return $ makeFileWriter mkpath commit generateHostsFile :: FileWriter -> KeyRingRuntime -> IO () generateHostsFile fw rt = do let hostspath = pathMaker fw "hosts" op = KeyRingOperation { opFiles = Map.fromList $ [ ( HomePub, streaminfo { typ=KeyRingFile, spill=KF_All, access=Pub } ) , ( ArgFile hostspath, streaminfo { typ=Hosts, spill=KF_None, fill=KF_All, access=Pub } ) ] , opPassphrases = [] , opHome = Just $ takeDirectory (rtPubring rt) , opTransforms = [] } KikiResult _ report <- runKeyRing op outputReport report names :: KeyRingRuntime -> Maybe Hostnames names rt = do wk <- rtWorkingKey rt -- XXX unnecessary signature check return $ getHostnames (rtKeyDB rt Map.! keykey wk) getssh :: (Char8.ByteString, SockAddr, KeyData) -> Char8.ByteString getssh (contactname,_addr,kd) = do let their_master = packet $ keyMappedPacket kd sshs :: [Packet] sshs = sortOn (Down . timestamp) $ getSubkeys CrossSigned their_master (keySubKeys kd) "ssh-server" blobs = mapMaybe sshblobFromPacketL sshs taggedblobs = map (\b -> contactname <> " " <> b) blobs Char8.unlines taggedblobs installIpsecConf :: FileWriter -> SockAddr -> Char8.ByteString -> [(Char8.ByteString, SockAddr, KeyData)] -> IO () installIpsecConf fw wkaddr (certBasename) cs = do snippets <- mapM (installIpsecPeerCertificate fw) cs writeL fw "ipsec.conf" . Char8.unlines $ [ "conn %default" , " ikelifetime=60m" , " keylife=20m" , " rekeymargin=3m" , " keyingtries=%forever" , " keyexchange=ikev2" , " dpddelay=10s" , " dpdaction=restart" , " left=%defaultroute" , " leftsubnet=" <> Char8.pack (showA wkaddr) <> "/128" , " leftauth=pubkey" , " leftid=" <> Char8.pack (showA wkaddr) , " leftrsasigkey=" <> certBasename , " leftikeport=4500" , " rightikeport=4500" , " right=%any" , " rightauth=pubkey" , " type=tunnel" , " auto=route" , "" ] ++ filter (not . Char8.null) snippets refreshCache :: KeyRingRuntime -> Maybe FilePath -> IO () refreshCache rt rootdir = do fw <- getMkPathAndCommit (fromMaybe "" rootdir ++ "/var/cache/kiki/config") generateHostsFile fw rt fromMaybe (error "No working key.") $ do Hostnames wkaddr onames _ _ <- names rt Just $ do let oname = Char8.concat $ take 1 onames bUnprivileged = False -- TODO if (oname == "") && (not bUnprivileged) then error "Missing tor key" else do -- sshcpathpub0 = fromMaybe "" rootdir ++ osHomeDir ".ssh" "id_rsa.pub" -- sshspathpub0 = fromMaybe "" rootdir ++ "/etc/ssh/ssh_host_rsa_key.pub" -- contactipsec0 = fromMaybe "" rootdir ++ "/etc/ipsec.d/certs/%(onion).pem" flip (maybe $ warn "missing working key?") (rtWorkingKey rt) $ \wk -> do let grip = fingerprint wk exportOp = passphrases <> pemSecrets <> minimalOp (CommonArgsParsed (Just $ takeDirectory $ rtPubring rt) Nothing) where passphrases = mempty { opPassphrases = [PassphraseMemoizer (rtPassphrases rt)] } pemSecrets = mempty { opFiles = Map.fromList [ send "ipsec" (ipsecKeyPath oname) "missing ipsec key?" , send "ssh-client" ("root/.ssh/id_rsa") "missing ssh-client key?" , send "ssh-server" ("ssh_host_rsa_key") "missing ssh host key?" , send "tor" ("tor/private_key") "missing tor key?" ] } send usage path warning = ( ArgFile (pathMaker fw path), StreamInfo { typ = PEMFile , fill = KF_Match usage , spill = KF_None , access = Sec , initializer = WarnMissing warning , transforms = [] }) KikiResult rt' report <- runKeyRing exportOp outputReport report -- outputReport $ map (first $ resolveForReport ctx) $ filter ((/=ExportedSubkey) . snd) report rt'' <- rethrowKikiErrors rt' writePublicKeyFiles rt'' fw grip oname wkaddr rethrowKikiErrors :: KikiCondition a -> IO a rethrowKikiErrors BadPassphrase = error "Operation requires correct passphrase. (Hint: Use --passphrase-fd=0 to input it on stdin.)" rethrowKikiErrors rt = unconditionally $ return rt writePublicKeyFiles :: KeyRingRuntime -> FileWriter -> String -> Char8.ByteString -> SockAddr -> IO () writePublicKeyFiles rt fw grip oname wkaddr = do -- Finally, export public keys if they do not exist. either warn (write fw "root/.ssh/id_rsa.pub") $ show_ssh' "ssh-client" grip (rtKeyDB rt) either warn (write fw "ssh_host_rsa_key.pub") $ show_ssh' "ssh-server" grip (rtKeyDB rt) either warn (write fw $ ipsecCertPath oname) $ show_pem' "ipsec" grip (rtKeyDB rt) pemFromPacket let onionkeys = mapMaybe namedContact $ Map.elems $ rtKeyDB rt cs = filter (\(_,_,kd) -> notme kd) onionkeys kk = keykey (fromJust $ rtWorkingKey rt) notme kd = keykey (keyPacket kd) /= kk namedContact kd = do -- The getHostnames command requires a valid cross-signed tor key -- for each onion name returned in (_,(ns,_)). let Hostnames addr ns _ _ = getHostnames kd fmap (\n -> (n,addr, kd)) $ listToMaybe ns -- only first onion name. known_hosts = L.concat $ map getssh onionkeys writeL fw "ssh_known_hosts" known_hosts installIpsecConf fw wkaddr (Char8.pack $ takeBaseName $ ipsecCertPath oname) cs fileWriterCommit fw sshKeyToHostname :: Packet -> IO Char8.ByteString sshKeyToHostname sshkey = do (_, (sout, _serr)) <- runExternal shellScript (Just $ sshblobFromPacket sshkey) return $ Char8.fromChunks [sout] where shellScript = "f=$(mktemp) && cat > \"$f\" && ssh-keygen -r _ -f \"$f\" |" ++ " (read _ _ _ _ _ hash _ && echo -n $hash.ssh.cryptonomic.net); r=$?; rm -f \"$f\"; exit $r" strongswanPeerConfiguration :: SockAddr -> Char8.ByteString -> Char8.ByteString -> Char8.ByteString strongswanPeerConfiguration addr oname rightip = Char8.unlines [ "conn " <> oname , " right=" <> rightip , " rightsubnet=" <> p (showA addr) <> "/128" , " rightauth=pubkey" , " rightid=" <> p (showA addr) , " rightrsasigkey=" <> oname <> ".pem" ] where p = Char8.pack -- conn hiotuxliwisbp6mi.onion -- right=%hiotuxliwisbp6mi.onion.ipv4 -- rightsubnet=fdcc:76c8:cb34:74e6:2aa3:cb39:abc8:d403/128 -- rightauth=pubkey -- rightid=fdcc:76c8:cb34:74e6:2aa3:cb39:abc8:d403 -- rightrsasigkey=hiotuxliwisbp6mi.onion.pem showA :: SockAddr -> String showA addr = if null bracket then pre else drop 1 pre where (pre,bracket) = break (==']') (show addr) #if !MIN_VERSION_base(4,8,0) sortOn :: Ord b => (a -> b) -> [a] -> [a] sortOn f = map snd . sortBy (comparing fst) . map (\x -> let y = f x in y `seq` (y, x)) #endif pemFromPacket :: Monad m => Packet -> m String pemFromPacket k = do let rsa = pkcs8 . fromJust $ rsaKeyFromPacket k der = encodeASN1 DER (toASN1 rsa []) #if defined(VERSION_memory) qq = S8.unpack $ convertToBase Base64 (L.toStrict der) #elif defined(VERSION_dataenc) qq = Base64.encode (L.unpack der) #endif return $ writePEM PemPublicKey qq -- ("TODO "++show keyspec) show_pem :: String -> String -> KeyDB -> IO () show_pem keyspec wkgrip db = either warn putStrLn $ show_pem' keyspec wkgrip db pemFromPacket show_pem' :: String -> String -> KeyDB -> (Packet -> Either String b) -> Either String b show_pem' keyspec wkgrip db keyfmt = do let s = parseSpec wkgrip keyspec flip (maybe . Left $ keyspec ++ ": not found") (selectPublicKey s db) keyfmt warn :: String -> IO () warn str = hPutStrLn stderr str show_ssh :: String -> String -> KeyDB -> IO () show_ssh keyspec wkgrip db = either warn putStrLn $ show_ssh' keyspec wkgrip db show_ssh' :: String -> String -> KeyDB -> Either String String show_ssh' keyspec wkgrip db = do let s = parseSpec wkgrip keyspec flip (maybe . Left $ keyspec ++ ": not found") (selectPublicKey s db) $ return . sshblobFromPacket -- | -- interpolate %var patterns in a string. interp :: Map.Map String String -> String -> String interp vars raw = es >>= interp1 where gs = groupBy (\_ c -> c/='%') raw es = dropWhile null $ gobbleEscapes ("":gs) where gobbleEscapes :: [String] -> [String] gobbleEscapes (a:"%":b:bs) = (a++b) : gobbleEscapes bs gobbleEscapes (g:gs) = g : gobbleEscapes gs gobbleEscapes [] = [] interp1 ('%':'(':str) = fromMaybe "" (Map.lookup key vars) ++ drop 1 rest where (key,rest) = break (==')') str interp1 plain = plain sshblobFromPacket :: Packet -> String sshblobFromPacket k = Char8.unpack $ fromJust $ sshblobFromPacketL k sshblobFromPacketL :: Packet -> Maybe Char8.ByteString sshblobFromPacketL k = do RSAKey (MPI n) (MPI e) <- rsaKeyFromPacket k return $ SSH.keyblob (n,e) {- replaceSshServerKeys root cmn = do let homepass' = cmn { cap_homespec = fmap root (cap_homespec cmn) } replaceSSH op = op { opFiles = files } where files = Map.adjust delssh HomeSec $ Map.adjust delssh HomePub $ Map.insert (ArgFile $ root "/etc/ssh/ssh_host_rsa_key") strm $ opFiles op strm = streaminfo { typ = PEMFile, spill = KF_Match "ssh-server", access = Sec } delssh strm = strm { transforms = DeleteSubkeyByUsage "ssh-server" : transforms strm , fill = KF_All } KikiResult r report <- runKeyRing $ minimalOp homepass' case r of KikiSuccess rt -> Kiki.refreshCache rt $ case root "" of "/" -> Nothing "" -> Nothing pth -> Just pth err -> hPutStrLn stderr $ errorString err -} slash :: String -> String -> String slash "/" ('/':xs) = '/':xs slash "" ('/':xs) = '/':xs slash "" xs = '/':xs slash (y:ys) xs = y:slash ys xs dashdashChroot :: Args (FilePath -> FilePath) dashdashChroot = pure (\r a -> slash r a) <*> arg "--chroot" <|> pure id dashdashHomedir :: Args CommonArgsParsed dashdashHomedir = CommonArgsParsed <$> optional (arg "--homedir") <*> optional (FileDesc <$> read <$> arg "--passphrase-fd") dashdashCipher :: Args SymmetricAlgorithm dashdashCipher = fromMaybe CAST5 <$> optional (cipherFromString <$> arg "--cipher") kikiOptions :: ( [(String,Int)], [String] ) kikiOptions = ( ss, ps ) where ss = [("--chroot",1),("--passphrase-fd",1),("--homedir",1),("--cipher",1)] ps = []