|
| 1 | +-- SPDX-FileCopyrightText: 2022 Serokell <https://serokell.io> |
| 2 | +-- |
| 3 | +-- SPDX-License-Identifier: MPL-2.0 |
| 4 | + |
| 5 | +module Backend.Pass |
| 6 | + ( PassBackend ) where |
| 7 | +import Backend |
| 8 | +import BackendName |
| 9 | +import Coffer.Path |
| 10 | +import Coffer.Path qualified as P |
| 11 | +import Control.Exception (IOException) |
| 12 | +import Control.Lens |
| 13 | +import Data.ByteString.Lazy (ByteString) |
| 14 | +import Data.ByteString.Lazy qualified as BS |
| 15 | +import Data.Maybe |
| 16 | +import Data.Text (Text) |
| 17 | +import Data.Text qualified as T |
| 18 | +import Data.Text.Encoding (encodeUtf8) |
| 19 | +import Data.Text.Encoding qualified as T |
| 20 | +import Effect.Fs |
| 21 | +import Entry (Entry) |
| 22 | +import Entry qualified as E |
| 23 | +import Entry.Pass |
| 24 | +import Error |
| 25 | +import Fmt (build, fmt) |
| 26 | +import Polysemy |
| 27 | +import Polysemy.Error |
| 28 | +import System.Directory qualified as D |
| 29 | +import System.FilePath (makeRelative) |
| 30 | +import System.IO.Error (isDoesNotExistError) |
| 31 | +import System.Process.Typed |
| 32 | +import Toml (TomlCodec) |
| 33 | +import Toml qualified |
| 34 | + |
| 35 | +data PassBackend = |
| 36 | + PassBackend |
| 37 | + { pbName :: BackendName |
| 38 | + , pbStoreDir :: FilePath |
| 39 | + , pbPassExe :: Maybe FilePath |
| 40 | + } |
| 41 | + deriving stock (Show) |
| 42 | + |
| 43 | +passCodec :: TomlCodec PassBackend |
| 44 | +passCodec = |
| 45 | + PassBackend |
| 46 | + <$> backendNameCodec "name" Toml..= pbName |
| 47 | + <*> Toml.string "store_dir" Toml..= pbStoreDir |
| 48 | + <*> Toml.dimatch fPathToT tToFPath (Toml.text "pass_exe") Toml..= pbPassExe |
| 49 | + where tToFPath = Just . T.unpack |
| 50 | + fPathToT :: Maybe String -> Maybe Text |
| 51 | + fPathToT a = a <&> T.pack |
| 52 | + |
| 53 | + |
| 54 | +verifyPassStore |
| 55 | + :: Member (Error CofferError) r |
| 56 | + => Member (Embed IO) r |
| 57 | + => FilePath |
| 58 | + -> Sem r () |
| 59 | +verifyPassStore storeDir = |
| 60 | + res >>= \case |
| 61 | + Left e -> throw $ OtherError (show e & T.pack) |
| 62 | + Right (Just _) -> pure () |
| 63 | + Right Nothing -> throw . OtherError $ |
| 64 | + "You must first initialize the password store at: " <> T.pack storeDir |
| 65 | + where |
| 66 | + res = runError @FsError . runFsInIO $ do |
| 67 | + nodeExists (stringToPath $ storeDir <> "/.gpg-id") |
| 68 | + |
| 69 | + |
| 70 | +wrapper |
| 71 | + :: Effects r |
| 72 | + => PassBackend |
| 73 | + -> [String] |
| 74 | + -> Maybe (StreamSpec 'STInput ()) |
| 75 | + -> Sem r (ExitCode, ByteString, ByteString) |
| 76 | +wrapper backend args input = do |
| 77 | + let passExe = pbPassExe backend |
| 78 | + let storeDir = pbStoreDir backend |
| 79 | + verifyPassStore storeDir |
| 80 | + |
| 81 | + proc (fromMaybe "pass" passExe) args |
| 82 | + & case input of |
| 83 | + Just a -> setStdin a |
| 84 | + Nothing -> setStdin nullStream |
| 85 | + & setEnv [("PASSWORD_STORE_DIR", storeDir)] |
| 86 | + & readProcess |
| 87 | + |
| 88 | + |
| 89 | + |
| 90 | +pbWriteSecret |
| 91 | + :: Effects r => PassBackend -> Entry -> Sem r () |
| 92 | +pbWriteSecret backend entry = do |
| 93 | + let input = |
| 94 | + entry ^. re E.entry . re passTextPrism |
| 95 | + & encodeUtf8 |
| 96 | + & BS.fromStrict |
| 97 | + |
| 98 | + (exitCode, _stdout, stderr) <- |
| 99 | + wrapper |
| 100 | + backend |
| 101 | + [ "insert" |
| 102 | + , "-mf" |
| 103 | + , entry ^. E.path & P.entryPathAsPath & build & fmt |
| 104 | + ] |
| 105 | + (Just $ byteStringInput input) |
| 106 | + |
| 107 | + case exitCode of |
| 108 | + ExitSuccess -> pure () |
| 109 | + ExitFailure _i -> throw $ OtherError (T.decodeUtf8 $ BS.toStrict stderr) |
| 110 | + |
| 111 | + |
| 112 | +pbReadSecret |
| 113 | + :: Effects r => PassBackend -> EntryPath -> Sem r (Maybe Entry) |
| 114 | +pbReadSecret backend path = do |
| 115 | + (exitCode, stdout, stderr) <- |
| 116 | + wrapper |
| 117 | + backend |
| 118 | + [ "show" |
| 119 | + , path & P.entryPathAsPath & build & fmt |
| 120 | + ] |
| 121 | + Nothing |
| 122 | + |
| 123 | + case exitCode of |
| 124 | + ExitSuccess -> |
| 125 | + pure $ T.decodeUtf8 (BS.toStrict stdout) ^? passTextPrism . E.entry |
| 126 | + ExitFailure 1 -> |
| 127 | + pure Nothing |
| 128 | + ExitFailure _e -> |
| 129 | + throw $ OtherError (T.decodeUtf8 $ BS.toStrict stderr) |
| 130 | + |
| 131 | +pbListSecrets |
| 132 | + :: Effects r => PassBackend -> Path -> Sem r (Maybe [Text]) |
| 133 | +pbListSecrets backend path = do |
| 134 | + let storeDir = pbStoreDir backend |
| 135 | + verifyPassStore storeDir |
| 136 | + |
| 137 | + let fpath = storeDir <> (path & build & fmt) |
| 138 | + contents <- runError (fromException @IOException $ D.listDirectory fpath) |
| 139 | + >>= (\case Left e -> |
| 140 | + if | isDoesNotExistError e -> pure Nothing |
| 141 | + | True -> throw $ OtherError (T.pack $ show e) |
| 142 | + Right v -> pure $ Just v) |
| 143 | + <&> \a -> a <&> map (makeRelative fpath) |
| 144 | + |
| 145 | + pure $ contents <&> map (T.dropEnd 4 . T.pack) |
| 146 | + |
| 147 | +pbDeleteSecret |
| 148 | + :: Effects r => PassBackend -> EntryPath -> Sem r () |
| 149 | +pbDeleteSecret backend path = do |
| 150 | + (exitCode, _stdout, stderr) <- |
| 151 | + wrapper |
| 152 | + backend |
| 153 | + [ "rm" |
| 154 | + , "-f" |
| 155 | + , path & P.entryPathAsPath & build & fmt |
| 156 | + ] |
| 157 | + Nothing |
| 158 | + |
| 159 | + case exitCode of |
| 160 | + ExitSuccess -> pure () |
| 161 | + ExitFailure _e -> throw $ OtherError (T.decodeUtf8 $ BS.toStrict stderr) |
| 162 | + |
| 163 | + |
| 164 | +instance Backend PassBackend where |
| 165 | + _name kvBackend = pbName kvBackend |
| 166 | + _codec = passCodec |
| 167 | + _writeSecret = pbWriteSecret |
| 168 | + _readSecret = pbReadSecret |
| 169 | + _listSecrets = pbListSecrets |
| 170 | + _deleteSecret = pbDeleteSecret |
0 commit comments