summaryrefslogtreecommitdiffhomepage
path: root/keysafe.hs
blob: 056003a71e5b838202cfeffba6c189e38c71c4fb (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
{-# LANGUAGE OverloadedStrings #-}

{- Copyright 2016 Joey Hess <id@joeyh.name>
 -
 - Licensed under the GNU AGPL version 3 or higher.
 -}

module Main where

import Types
import Tunables
import qualified CmdLine
import UI
import Encryption
import Entropy
import ExpensiveHash
import Cost
import Shard
import Storage
import Storage.LocalFiles
import qualified Gpg
import Data.Maybe
import Control.Monad
import qualified Data.ByteString as B
import qualified Data.ByteString.UTF8 as BU8
import System.Posix.User (userGecos, getUserEntryForID, getEffectiveUserID)

main :: IO ()
main = do
	cmdline <- CmdLine.get
	ui <- selectUI (CmdLine.gui cmdline)
	let tunables = if CmdLine.testMode cmdline
		then testModeTunables
		else defaultTunables
	mode <- CmdLine.selectMode cmdline
	go mode (CmdLine.secretkeysource cmdline) tunables ui
  where
	go CmdLine.Backup (Just secretkeysource@(GpgKey kid)) tunables ui = do
		ok <- Gpg.knownByKeyServer kid
		unless ok $
			error "Your gpg public key has to be stored on the keyservers before you can back it up by keyid. Either use gpg --send-key to store the public key on the keyservers, or omit the --gpgkeyid option"
		backup ui tunables secretkeysource
			=<< getSecretKey secretkeysource
	go CmdLine.Backup (Just secretkeysource) tunables ui =
		backup ui tunables secretkeysource
			=<< getSecretKey secretkeysource
	go CmdLine.Backup Nothing tunables ui =
		backup ui tunables anyGpgKey =<< pickGpgKeyToBackup ui
	go CmdLine.Restore (Just secretkeydest) _ ui =
		restore ui secretkeydest
	go CmdLine.Restore Nothing _ ui =
		restore ui anyGpgKey
	go CmdLine.Benchmark _ tunables _ =
		benchmarkTunables tunables

getSecretKey :: SecretKeySource -> IO SecretKey
getSecretKey (GpgKey kid) = Gpg.getSecretKey kid
getSecretKey (KeyFile f) = SecretKey <$> B.readFile f

-- | Pick gpg secret key to back up.
--
-- If there is only one gpg secret key,
-- the choice is obvious. Otherwise prompt the user with a list.
pickGpgKeyToBackup :: UI -> IO SecretKey
pickGpgKeyToBackup ui = go =<< Gpg.listSecretKeys
  where
	go [] = do
		showError ui "You have no gpg secret keys to back up."
		error "Aborting on no gpg secret keys."
	go [(_, kid)] = Gpg.getSecretKey kid
	go l = maybe (error "Canceled") Gpg.getSecretKey
		=<< promptKeyId ui "Pick gpg secret key"
			"Pick gpg secret key to back up:" l

-- | Use when the gpg keyid will not be known at restore time.
anyGpgKey :: SecretKeySource
anyGpgKey = GpgKey (KeyId "")

backup :: UI -> Tunables -> SecretKeySource -> SecretKey -> IO ()
backup ui tunables secretkeysource secretkey = do
	username <- userName
	name <- fromMaybe (error "Aborting on no name") 
		<$> promptName ui "Enter a name"
			namedesc username validateName
	password <- fromMaybe (error "Aborting on no password") 
		<$> promptPassword ui "Enter a password"
			passworddesc validatePassword
	kek <- genKeyEncryptionKey tunables name password
	-- TODO: show password strength estimate, and verify password.
	putStrLn "Very rough estimate of cost to brute-force the password:"
	print $ estimateAttack spotAWS $ estimateBruteforceOf kek
		(passwordEntropy password [])
	let esk = encrypt tunables kek secretkey
	let sis = shardIdents tunables name secretkeysource
	shards <- genShards esk tunables
	print =<< mapM (uncurry (storeShard localFiles)) (zip (getIdents sis) shards)
	print =<< obscureShards localFiles
  where
	namedesc = unlines
		[ "To back up your secret key, you will need to enter a name and a password."
		, ""
		, "Make sure to pick a name you will remember at some point in the future,"
		, "perhaps years from now, when you will need to enter it with the same"
		, "spelling and capitalization in order to restore your secret key."
		, ""
		, "(Your own full name is a pretty good choice for the name to enter here.)"
		]
	passworddesc = unlines
		[ "Pick a password that will be used to protect your secret key."
		, ""
		, "It's very important that this password be hard to guess."
		, ""
		, "And, it needs to be one that you will be able to remember years from now"
		, "in order to restore your secret key."
		]

restore :: UI -> SecretKeySource -> IO ()
restore ui secretkeydest = do
	username <- userName
	name <- fromMaybe (error "Aborting on no name") 
		<$> promptName ui "Enter the name of the key to restore"
			namedesc username validateName
	let sis = shardIdents tunables name secretkeydest
	-- we drop 1 to simulate not getting all shards from the servers
	let l = drop 1 $ zip [1..] (getIdents sis)
	shards <- map (\(RetrieveSuccess s) -> s)
		<$> mapM (uncurry (retrieveShard localFiles)) l
	_ <- obscureShards localFiles
	let esk = combineShards tunables shards
	go esk (candidateKeyEncryptionKeys tunables name password)
  where
	go _ [] = error "decryption failed"
	go esk (kek:rest) = case decrypt kek esk of
		Just (SecretKey sk) -> print sk
		Nothing -> go esk rest

	password = Password "correct horse battery staple"
	-- TODO: derive by probing to find objects
	tunables = testModeTunables -- defaultTunables
	namedesc = unlines
		[ "When you backed up the key, you entered a name and a password."
		, "Now it's time to remember what you entered back then."
		, ""
		, "(If you can't remember the name you used, your own full name is the best guess.)"
		]

validateName :: Name -> Maybe Problem
validateName (Name n)
	| B.length n < 6 = Just "The name should be at least 6 letters long."
	| otherwise = Nothing

validatePassword :: Password -> Maybe Problem
validatePassword _ = Nothing

userName :: IO Name
userName = do
	u <- getUserEntryForID =<< getEffectiveUserID
	return $ Name $ BU8.fromString $ takeWhile (/= ',') (userGecos u)