summaryrefslogtreecommitdiffhomepage
path: root/CmdLine.hs
blob: 0f89fe440a1909dc7dd4beba81d55543c73f0a9d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
{- Copyright 2016 Joey Hess <id@joeyh.name>
 -
 - Licensed under the GNU AGPL version 3 or higher.
 -}

module CmdLine where

import Types
import Types.Storage
import Types.Server (HostName, Port)
import Types.Cost (Seconds(..))
import Storage.Local
import Storage.Network
import Tunables
import qualified Gpg
import Options.Applicative
import Data.Monoid
import qualified Data.ByteString.UTF8 as BU8
import qualified Data.Text as T
import System.Directory

data CmdLine = CmdLine
	{ mode :: Maybe Mode
	, secretkeysource :: Maybe SecretKeySource
	, localstoragedirectory :: Maybe LocalStorageDirectory
	, gui :: Bool
	, testMode :: Bool
	, customShareParams :: Maybe ShareParams
	, name :: Maybe Name
	, othername :: Maybe Name
	, preferredStorage :: [Maybe LocalStorageDirectory -> IO (Maybe Storage)]
	, serverConfig :: ServerConfig
	, chaffMaxDelay :: Maybe Seconds
	}

data Mode = Backup | Restore | UploadQueued | AutoStart | Server | BackupServer FilePath | RestoreServer FilePath | Chaff HostName | CheckServers | Benchmark | Test
	deriving (Show)

data ServerConfig = ServerConfig
	{ serverPort :: Port
	, serverAddress :: String
	, monthsToFillHalfDisk :: Integer
	, serverMotd :: Maybe T.Text
	}

parse :: Parser CmdLine
parse = CmdLine
	<$> optional parseMode
	<*> optional (gpgswitch <|> fileswitch)
	<*> optional localstoragedirectoryopt
	<*> guiswitch
	<*> testmodeswitch
	<*> optional parseShareParams
	<*> optional nameopt
	<*> optional othernameopt
	<*> many (addstoragedirectory <|> addserver)
	<*> parseServerConfig
	<*> optional chaffmaxdelayopt
  where
	gpgswitch = GpgKey . KeyId . T.pack <$> strOption
		( long "gpgkeyid"
		<> metavar "KEYID"
		<> help "Specify keyid of gpg key to back up or restore. (When this option is used to back up a key, it must also be used at restore time.)"
		)
	fileswitch = KeyFile <$> strOption
		( long "keyfile"
		<> metavar "FILE"
		<> help "Specify secret key file to back up or restore. (The same filename must be used to restore a key as was used to back it up.)"
		)
	localstoragedirectoryopt = LocalStorageDirectory <$> option str
		( long "store-directory"
		<> metavar "DIR"
		<> help "Where to store data locally. For the client, data is stored here before it is uploaded to the server. For the server, this is where it stores its data. (default: ~/.keysafe/objects/)"
		)
	testmodeswitch = switch
		( long "testmode"
		<> help "Avoid using expensive cryptographic operations to secure data. Use for testing only, not with real secret keys."
		)
	guiswitch = switch
		( long "gui"
		<> help "Use GUI interface for interaction. Default is to use readline interface when run in a terminal, and GUI otherwise."
		)
	nameopt = option nameOption
		( long "name"
		<> metavar "N"
		<> help "Specify name used for key backup/restore, avoiding the usual prompt."
		)
	othernameopt = option nameOption
		( long "othername"
		<> metavar "N"
		<> help "Specify other name used for key backup/restore, avoiding the usual prompt."
		)
	chaffmaxdelayopt = option secondsOption
		( long "chaff-max-delay"
		<> metavar "SECONDS"
		<> help "Specify a delay between chaff uploads. Will delay a random amount between 0 and this many seconds."
		)
	addstoragedirectory = (\d _lsd -> localStorageOverride d) 
		<$> strOption
			( long "add-storage-directory"
			<> metavar "DIR"
			<> help "Add the directory to the list of locations keysafe will use for backup/restore of keys. Keysafe will use the directory first, before any of its built-in servers."
			)
	addserver = (\(h, p) lsd -> networkStorageOverride lsd h p) 
		<$> option hostPortOption
			( long "add-server"
			<> metavar "HOST[:PORT]"
			<> help "Add the server to the server list which keysafe will use for backup/restore of keys. Keysafe will use the server first before any of its built-in servers."
			)

parseMode :: Parser Mode
parseMode = 
	flag' Backup
		( long "backup"
		<> help "Store a secret key in keysafe."
		)
	<|> flag' Restore
		( long "restore"
		<> help "Retrieve a secret key from keysafe."
		)
	<|> flag' UploadQueued
		( long "uploadqueued"
		<> help "Upload any data to servers that was queued by a previous --backup run."
		)
	<|> flag' AutoStart
		( long "autostart"
		<> help "Run automatically on login by desktop autostart file."
		)
	<|> flag' Server
		( long "server"
		<> help "Run as a keysafe server, accepting objects and storing them to ~/.keysafe/objects/local/"
		)
	<|> BackupServer <$> strOption
		( long "backup-server"
		<> metavar "BACKUPDIR"
		<> help "Run on a server, populates the directory with a gpg encrypted backup of all objects stored in the --store-directory. This is designed to be rsynced offsite (with --delete) to back up a keysafe server with minimal information leakage."
		)
	<|> RestoreServer <$> strOption
		( long "restore-server"
		<> metavar "BACKUPDIR"
		<> help "Restore all objects present in the gpg-encrypted backups in the specified directory."
		)
	<|> Chaff <$> strOption
		( long "chaff"
		<> metavar "HOSTNAME"
		<> help "Upload random data to a keysafe server."
		)
	<|> flag' CheckServers
		( long "check-servers"
		<> help "Tries to connect to each server in the server list. Displays the server's MOTD, and the amount of data stored on it. Prints message to stderr and exits nonzero if any of the servers are not accessible."
		)
	<|> flag' Benchmark
		( long "benchmark"
		<> help "Benchmark speed of keysafe's cryptographic primitives."
		)
	<|> flag' Test
		( long "test"
		<> help "Run test suite."
		)

parseShareParams :: Parser ShareParams
parseShareParams = ShareParams <$> totalobjects <*> neededobjects
  where
	totalobjects = option auto 
		( long "totalshares"
		<> metavar "M"
		<> help ("Configure the number of shares to split encrypted secret key into. "
			++ showdefault totalObjects ++ neededboth)
		)
	neededobjects = option auto 
		( long "neededshares"
		<> metavar "N"
		<> help ("Configure the number of shares needed to restore. " 
			++ showdefault neededObjects ++ neededboth)
		)
	showdefault f = "(default: " ++ show (f (shareParams defaultTunables)) ++ ")"
	neededboth = " (When this option is used to back up a key, it must also be provided at restore time.)"
	

parseServerConfig :: Parser ServerConfig
parseServerConfig = ServerConfig
	<$> option auto
		( long "port"
		<> metavar "P"
		<> value 4242
		<> showDefault
		<> help "Port for server to listen on."
		)
	<*> option str
		( long "address"
		<> metavar "A"
		<> value "127.0.0.1"
		<> showDefault
		<> help "Address for server to bind to. (Use \"*\" to bind to all addresses.)"
		)
	<*> option auto
		( long "months-to-fill-half-disk"
		<> metavar "N"
		<> value 12
		<> showDefault
		<> help "Server rate-limits requests and requires proof of work, to avoid too many objects being stored. This is an lower bound on how long it could possibly take for half of the current disk space to be filled."
		)
	<*> optional (T.pack <$> strOption 
		( long "motd"
		<> metavar "MESSAGE"
		<> help "The server's Message Of The Day."
		))

get :: IO CmdLine
get = execParser opts
  where
	opts = info (helper <*> parse)
		( fullDesc
		<> header "keysafe - securely back up secret keys"
		)

-- | When a mode is not specified on the command line,
-- default to backing up if a secret key exists, and otherwise restoring.
selectMode :: CmdLine -> IO Mode
selectMode cmdline = case mode cmdline of
	Just m -> return m
	Nothing -> case secretkeysource cmdline of
		Just (KeyFile f) -> present <$> doesFileExist f
		_ -> present . not . null <$> Gpg.listSecretKeys
  where
	present True = Backup
	present False = Restore

customizeShareParams :: CmdLine -> Tunables -> Tunables
customizeShareParams cmdline t = case customShareParams cmdline of
	Nothing -> t
	Just ps -> t { shareParams = ps }

secondsOption :: ReadM Seconds
secondsOption = Seconds . toRational <$> (auto :: ReadM Double)

nameOption :: ReadM Name
nameOption = Name . BU8.fromString <$> auto

hostPortOption :: ReadM (HostName, Port)
hostPortOption = eitherReader $ \s ->
	case break (== ':') s of
		([], []) -> Left "need a hostname"
		(h, ':':ps) -> case reads ps of
			[(p, "")] -> Right (h, p)
			_ -> Left $ "unable to parse port \"" ++ ps ++ "\""
		(h, _) -> Right (h, 4242)