keysafe 0.20160922 → 0.20160927
raw patch · 20 files changed
+389/−157 lines, 20 filesdep +MonadRandomdep +random-shuffle
Dependencies added: MonadRandom, random-shuffle
Files
- Benchmark.hs +15/−14
- CHANGELOG +23/−0
- CmdLine.hs +51/−18
- Crypto/Argon2.hs +4/−4
- Crypto/Argon2/FFI.hsc +6/−6
- HTTP/Client.hs +1/−1
- HTTP/Server.hs +13/−5
- INSTALL +10/−3
- Makefile +3/−3
- Output.hs +33/−0
- Servers.hs +30/−12
- Storage.hs +58/−15
- TODO +6/−2
- Tests.hs +7/−8
- Types/Cost.hs +1/−1
- Types/Server.hs +7/−2
- UI/NonInteractive.hs +7/−9
- UI/Readline.hs +34/−39
- keysafe.cabal +4/−1
- keysafe.hs +76/−14
Benchmark.hs view
@@ -8,6 +8,7 @@ module Benchmark where import Types+import Output import Tunables import ExpensiveHash import HTTP.ProofOfWork@@ -86,33 +87,33 @@ benchmarkTunables :: Tunables -> IO () benchmarkTunables tunables = do- putStrLn "/proc/cpuinfo:"- putStrLn =<< readFile "/proc/cpuinfo"+ say "/proc/cpuinfo:"+ say =<< readFile "/proc/cpuinfo" - putStrLn "Benchmarking 1000 rounds of proof of work hash..."- print =<< benchmarkExpensiveHash 1000 (proofOfWorkHashTunable 0)+ say "Benchmarking 1000 rounds of proof of work hash..."+ display =<< benchmarkExpensiveHash 1000 (proofOfWorkHashTunable 0) - putStrLn "Benchmarking 60 rounds of 1 second proofs of work..."- print =<< benchmarkPoW 60 (Seconds 1)+ say "Benchmarking 60 rounds of 1 second proofs of work..."+ display =<< benchmarkPoW 60 (Seconds 1) - putStrLn "Benchmarking 10 rounds of 8 second proofs of work..."- print =<< benchmarkPoW 10 (Seconds 8)+ say "Benchmarking 10 rounds of 8 second proofs of work..."+ display =<< benchmarkPoW 10 (Seconds 8) -- Rather than run all 256 rounds of this hash, which would -- probably take on the order of 1 hour, run only 16, and scale -- the expected cost accordingly. let normalrounds = 256 * randomSaltBytes (keyEncryptionKeyTunable tunables)- putStrLn $ "Benchmarking 16/" ++ show normalrounds ++ " rounds of key encryption key hash..."+ say $ "Benchmarking 16/" ++ show normalrounds ++ " rounds of key encryption key hash..." r <- benchmarkExpensiveHash' 16 (keyEncryptionKeyHash $ keyEncryptionKeyTunable tunables) (mapCost (/ (fromIntegral normalrounds / 16)) $ randomSaltBytesBruteForceCost $ keyEncryptionKeyTunable tunables)- print r- putStrLn $ "Estimated time for " ++ show normalrounds ++ " rounds of key encryption key hash..."- print $ BenchmarkResult+ display r+ say $ "Estimated time for " ++ show normalrounds ++ " rounds of key encryption key hash..."+ display $ BenchmarkResult { expectedBenchmark = mapCost (* 16) (expectedBenchmark r) , actualBenchmark = mapCost (* 16) (actualBenchmark r) } - putStrLn "Benchmarking 1 round of name generation hash..."- print =<< benchmarkExpensiveHash 1+ say "Benchmarking 1 round of name generation hash..."+ display =<< benchmarkExpensiveHash 1 (nameGenerationHash $ nameGenerationTunable tunables)
CHANGELOG view
@@ -1,3 +1,26 @@+keysafe (0.20160927) unstable; urgency=medium++ * Makefile: Avoid rebuilding on make install, so that sudo make install works.+ * Added --chaff-max-delay option for slower chaffing.+ * Fix embedded copy of Argon2 to not use Word64, fixing build on 32 bit+ systems.+ * Randomize the server list.+ * Don't upload more than neededshares-1 shares to Alternate servers+ without asking the user if they want to do this potentially dangerous+ action.+ * Added a second keysafe server to the server list. It's provided+ by Marek Isalski at Faelix. Currently located in UK, but planned move+ to CH. Currently at Alternate level until verification is complete.+ * Server: --motd can be used to provide a Message Of The Day.+ * Added --check-servers mode, which is useful both at the command line+ to see what servers keysafe knows about, and as a cron job.+ * Server: Round number of objects down to the nearest thousand, to avoid+ leaking too much data about when objects are uploaded to servers.+ * Filter out escape sequences and any other unusual characters when+ writing all messages to the console.++ -- Joey Hess <id@joeyh.name> Tue, 27 Sep 2016 20:25:35 -0400+ keysafe (0.20160922) unstable; urgency=medium * Keysafe now knows about 3 servers, although only 1 is currently in
CmdLine.hs view
@@ -8,6 +8,7 @@ import Types import Types.Storage import Types.Server (HostName)+import Types.Cost (Seconds(..)) import Tunables import qualified Gpg import Options.Applicative@@ -27,15 +28,17 @@ , name :: Maybe Name , othername :: Maybe Name , serverConfig :: ServerConfig+ , chaffMaxDelay :: Maybe Seconds } -data Mode = Backup | Restore | UploadQueued | AutoStart | Server | BackupServer FilePath | RestoreServer FilePath | Chaff HostName | Benchmark | Test+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@@ -43,13 +46,14 @@ <$> optional parseMode <*> optional (gpgswitch <|> fileswitch) <*> localstorageswitch- <*> localstoragedirectoryopt+ <*> optional localstoragedirectoryopt <*> guiswitch <*> testmodeswitch- <*> optional (ShareParams <$> totalobjects <*> neededobjects)- <*> nameopt- <*> othernameopt+ <*> optional parseShareParams+ <*> optional nameopt+ <*> optional othernameopt <*> parseServerConfig+ <*> optional chaffmaxdelayopt where gpgswitch = GpgKey . KeyId . T.pack <$> strOption ( long "gpgkeyid"@@ -65,7 +69,7 @@ ( long "store-local" <> help "Store data locally. (The default is to store data in the cloud.)" )- localstoragedirectoryopt = optional $ LocalStorageDirectory <$> option str+ localstoragedirectoryopt = LocalStorageDirectory <$> option str ( long "store-directory" <> metavar "DIR" <> help "Where to store data locally. (default: ~/.keysafe/objects/)"@@ -78,26 +82,21 @@ ( long "gui" <> help "Use GUI interface for interaction. Default is to use readline interface when run in a terminal, and GUI otherwise." )- totalobjects = option auto - ( long "totalshares"- <> metavar "M"- <> help ("Configure the number of shares to split encrypted secret key into. (default: " ++ show (totalObjects (shareParams defaultTunables)) ++ ") (When this option is used to back up a key, it must also be provided at restore time.)")- )- neededobjects = option auto - ( long "neededshares"- <> metavar "N"- <> help ("Configure the number of shares needed to restore. (default: " ++ show (neededObjects (shareParams defaultTunables)) ++ ") (When this option is used to back up a key, it must also be provided at restore time.)")- )- nameopt = optional $ Name . BU8.fromString <$> strOption+ nameopt = option nameOption ( long "name" <> metavar "N" <> help "Specify name used for key backup/restore, avoiding the usual prompt." )- othernameopt = optional $ Name . BU8.fromString <$> strOption+ 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."+ ) parseMode :: Parser Mode parseMode = @@ -136,6 +135,10 @@ <> 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."@@ -145,6 +148,25 @@ <> 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@@ -168,6 +190,11 @@ <> 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@@ -193,3 +220,9 @@ 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
Crypto/Argon2.hs view
@@ -152,9 +152,9 @@ -- will be throw. data Argon2Exception = -- | The length of the supplied password is outside the range supported by @libargon2@.- Argon2PasswordLengthOutOfRange !Word64 -- ^ The erroneous length.+ Argon2PasswordLengthOutOfRange !CSize -- ^ The erroneous length. | -- | The length of the supplied salt is outside the range supported by @libargon2@.- Argon2SaltLengthOutOfRange !Word64 -- ^ The erroneous length.+ Argon2SaltLengthOutOfRange !CSize -- ^ The erroneous length. | -- | Either too much or too little memory was requested via 'hashMemory'. Argon2MemoryUseOutOfRange !Word32 -- ^ The erroneous 'hashMemory' value. | -- | Either too few or too many iterations were requested via 'hashIterations'.@@ -167,7 +167,7 @@ instance Exception Argon2Exception -type Argon2Encoded = Word32 -> Word32 -> Word32 -> CString -> Word64 -> CString -> Word64 -> Word64 -> CString -> Word64 -> IO Int32+type Argon2Encoded = Word32 -> Word32 -> Word32 -> CString -> CSize -> CString -> CSize -> CSize -> CString -> CSize -> IO Int32 hashEncoded' :: HashOptions -> BS.ByteString@@ -202,7 +202,7 @@ fmap T.decodeUtf8 (BS.packCString out) where argon2 = variant argon2i argon2d hashVariant -type Argon2Unencoded = Word32 -> Word32 -> Word32 -> CString -> Word64 -> CString -> Word64 -> CString -> Word64 -> IO Int32+type Argon2Unencoded = Word32 -> Word32 -> Word32 -> CString -> CSize -> CString -> CSize -> CString -> CSize -> IO Int32 hash' :: HashOptions -> BS.ByteString
Crypto/Argon2/FFI.hsc view
@@ -44,17 +44,17 @@ import Foreign import Foreign.C -foreign import ccall unsafe "argon2.h argon2i_hash_encoded" argon2i_hash_encoded :: (#type const uint32_t) -> (#type const uint32_t) -> (#type const uint32_t) -> Ptr a -> (#type const size_t) -> Ptr b -> (# type const size_t) -> (#type const size_t) -> CString -> (#type const size_t) -> IO (#type int)+foreign import ccall unsafe "argon2.h argon2i_hash_encoded" argon2i_hash_encoded :: (#type const uint32_t) -> (#type const uint32_t) -> (#type const uint32_t) -> Ptr a -> CSize -> Ptr b -> CSize -> CSize -> CString -> CSize -> IO (#type int) -foreign import ccall unsafe "argon2.h argon2i_hash_raw" argon2i_hash_raw :: (#type const uint32_t) -> (#type const uint32_t) -> (#type const uint32_t) -> Ptr a -> (#type const size_t) -> Ptr b -> (#type size_t) -> Ptr c -> (#type const size_t) -> IO (#type int)+foreign import ccall unsafe "argon2.h argon2i_hash_raw" argon2i_hash_raw :: (#type const uint32_t) -> (#type const uint32_t) -> (#type const uint32_t) -> Ptr a -> CSize -> Ptr b -> CSize -> Ptr c -> CSize -> IO (#type int) -foreign import ccall unsafe "argon2.h argon2d_hash_encoded" argon2d_hash_encoded :: (#type const uint32_t) -> (#type const uint32_t) -> (#type const uint32_t) -> Ptr a -> (#type const size_t) -> Ptr b -> (# type const size_t) -> (#type const size_t) -> CString -> (#type const size_t) -> IO (#type int)+foreign import ccall unsafe "argon2.h argon2d_hash_encoded" argon2d_hash_encoded :: (#type const uint32_t) -> (#type const uint32_t) -> (#type const uint32_t) -> Ptr a -> CSize -> Ptr b -> CSize -> CSize -> CString -> CSize -> IO (#type int) -foreign import ccall unsafe "argon2.h argon2d_hash_raw" argon2d_hash_raw :: (#type const uint32_t) -> (#type const uint32_t) -> (#type const uint32_t) -> Ptr a -> (#type const size_t) -> Ptr b -> (#type size_t) -> Ptr c -> (#type const size_t) -> IO (#type int)+foreign import ccall unsafe "argon2.h argon2d_hash_raw" argon2d_hash_raw :: (#type const uint32_t) -> (#type const uint32_t) -> (#type const uint32_t) -> Ptr a -> CSize -> Ptr b -> CSize -> Ptr c -> CSize -> IO (#type int) -foreign import ccall unsafe "argon2.h argon2i_verify" argon2i_verify :: CString -> Ptr a -> (#type const size_t) -> IO (#type int)+foreign import ccall unsafe "argon2.h argon2i_verify" argon2i_verify :: CString -> Ptr a -> CSize -> IO (#type int) -foreign import ccall unsafe "argon2.h argon2d_verify" argon2d_verify :: CString -> Ptr a -> (#type const size_t) -> IO (#type int)+foreign import ccall unsafe "argon2.h argon2d_verify" argon2d_verify :: CString -> Ptr a -> CSize -> IO (#type int) pattern ARGON2_OK = (#const ARGON2_OK) pattern ARGON2_OUTPUT_PTR_NULL = (#const ARGON2_OUTPUT_PTR_NULL)
HTTP/Client.hs view
@@ -76,7 +76,7 @@ serverRequest' srv a = go Nothing (serverUrls srv) where go lasterr [] = return $ Left $- maybe "no available servers" (\err -> "server failure: " ++ show err) lasterr+ maybe "no known address" (\err -> "server failure: " ++ show err) lasterr go _ (url:urls) = do manager <- torableManager res <- runExceptT $ a manager url
HTTP/Server.hs view
@@ -24,6 +24,7 @@ import Control.Concurrent import Control.Concurrent.Thread.Delay import Control.Concurrent.STM+import Data.Maybe import Data.String import qualified Data.ByteString as B @@ -32,6 +33,7 @@ , storage :: Storage , rateLimiter :: RateLimiter , logger :: Logger+ , serverConfig :: ServerConfig } newServerState :: Maybe LocalStorageDirectory -> ServerConfig -> IO ServerState@@ -42,6 +44,7 @@ <*> pure (serverStorage d) <*> newRateLimiter cfg d l <*> pure l+ <*> pure cfg runServer :: Maybe LocalStorageDirectory -> ServerConfig -> IO () runServer d cfg = do@@ -62,13 +65,13 @@ userAPI = Proxy server :: ServerState -> Server HttpAPI-server st = motd+server st = motd st :<|> getObject st :<|> putObject st :<|> countObjects st -motd :: Handler Motd-motd = return $ Motd "Hello World!"+motd :: ServerState -> Handler Motd+motd = return . Motd . fromMaybe "Hello World!" . serverMotd . serverConfig getObject :: ServerState -> StorableObjectIdent -> Maybe ProofOfWork -> Handler (POWGuarded StorableObject) getObject st i pow = rateLimit (rateLimiter st) (logger st) pow i $ do@@ -93,8 +96,13 @@ sz = B.length (fromStorableObject o) countObjects :: ServerState -> Maybe ProofOfWork -> Handler (POWGuarded CountResult)-countObjects st pow = rateLimit (rateLimiter st) (logger st) pow NoPOWIdent $- liftIO $ countShares $ storage st+countObjects st pow = rateLimit (rateLimiter st) (logger st) pow NoPOWIdent $ do+ v <- liftIO $ countShares $ storage st+ case v of+ CountResult n -> return $+ -- Round down to avoid leaking too much detail.+ CountResult ((n `div` 1000) * 1000)+ CountFailure s -> return (CountFailure s) -- | 1 is a dummy value; the server does not know the actual share numbers. dummyShareNum :: ShareNum
INSTALL view
@@ -3,11 +3,18 @@ This installs keysafe to ~/.local/bin, and is sufficient to use keysafe to back up or restore your private key. -First install Haskell's stack tool, the zlib, g++, readline and argon2-libraries, and zenity. For example, on a Debian system:+First install Haskell's stack tool, ghc, the zlib, g++, readline and argon2+libraries. Also, zenity is needed to use keysafe's GUI. +For example, on a Debian testing system: - sudo apt-get install haskell-stack \+ sudo apt-get install haskell-stack ghc \ zlib1g-dev g++ libreadline-dev libargon2-0-dev zenity++If your distribution does not yet include Haskell's stack tool,+see <http://haskellstack.org/>++If your distribution does not yet include argon2, see +<https://github.com/P-H-C/phc-winner-argon2> Then to build and install keysafe, run this in the keysafe directory:
Makefile view
@@ -2,7 +2,9 @@ # Can be stack or cabal BUILDER?=stack -build: keysafe+build:+ rm -f keysafe+ $(MAKE) keysafe keysafe: $(BUILDER) build@@ -32,5 +34,3 @@ install -m 0644 keysafe.desktop $(PREFIX)/usr/share/applications/keysafe.desktop install -d $(PREFIX)/etc/xdg/autostart/ install -m 0644 keysafe.autostart $(PREFIX)/etc/xdg/autostart/keysafe.desktop--.PHONY: keysafe
+ Output.hs view
@@ -0,0 +1,33 @@+-- All console output in keysafe should go via this module;+-- avoid using putStrLn, print, etc directly.++module Output (ask, progress, say, warn, display) where++import System.IO+import Data.Char++ask :: String -> IO ()+ask s = do+ putStr (escape s)+ hFlush stdout++progress :: String -> IO ()+progress = ask++say :: String -> IO ()+say = putStrLn . escape++warn :: String -> IO ()+warn = hPutStrLn stderr . escape++display :: Show s => s -> IO ()+display = say . show++-- | Prevent malicious escape sequences etc in a string+-- from being output to the console.+escape :: String -> String+escape = concatMap go+ where+ go c = if isPrint c || isSpace c+ then [c]+ else "\\" ++ show (ord c)
Servers.hs view
@@ -7,21 +7,39 @@ import Types.Server import Servant.Client--serverUrls :: Server -> [BaseUrl]-serverUrls srv = map go (serverAddress srv)- where- go (ServerAddress addr port) = BaseUrl Http addr port ""+import System.Random.Shuffle +-- | Keysafe's server list.+--+-- Note: Avoid removing servers from this list, as that will break+-- restores. If necessary, a server can be set to Untrusted to prevent+-- uploads to it.+--+-- Also, avoid changing the ServerName of any server, as that will+-- cause any uploads queued under that name to not go through. networkServers :: [Server] networkServers =- [ Server (ServerName "keysafe.joeyh.name")+ [ Server (ServerName "keysafe.joeyh.name") Alternate [ServerAddress "vzgrspuxbtnlrtup.onion" 4242]- -- Purism server is not yet deployed, but planned.- , Server (ServerName "keysafe.puri.sm")- []- -- Unknown yet who will provide this server, but allocate it now- -- so keysafe can start queuing uploads to it.- , Server (ServerName "thirdserver")+ "Provided by Joey Hess. Digital Ocean VPS, located in Indonesia"++ , Server (ServerName "keysafe.puri.sm") Alternate []+ "Purism server is not yet deployed, but planned."++ , Server (ServerName "thirdserver") Alternate -- still being vetted+ [ServerAddress "eqi7glyxe5ravak5.onion" 4242]+ "Provided by Marek Isalski at Faelix. Currently located in UK, but planned move to CH" ]++-- | Shuffles the server list, keeping Recommended first, then+-- Alternate, and finally Untrusted.+shuffleServers :: [Server] -> IO [Server]+shuffleServers l = concat <$> mapM shuf [minBound..maxBound]+ where+ shuf sl = shuffleM (filter (\s -> serverLevel s == sl) l)++serverUrls :: Server -> [BaseUrl]+serverUrls srv = map go (serverAddress srv)+ where+ go (ServerAddress addr port) = BaseUrl Http addr port ""
Storage.hs view
@@ -10,6 +10,8 @@ import Types import Types.Storage import Types.Server+import Types.Cost+import Output import Share import Storage.Local import Storage.Network@@ -18,17 +20,18 @@ import Data.Maybe import Data.List import Data.Monoid-import System.IO import System.FilePath import Control.Monad import Crypto.Random+import System.Random+import Control.Concurrent.Thread.Delay import Control.Concurrent.Async import qualified Data.Set as S import Network.Wai.Handler.Warp (Port) -networkStorageLocations :: Maybe LocalStorageDirectory -> StorageLocations-networkStorageLocations d = StorageLocations $ - map (networkStorage d) networkServers+networkStorageLocations :: Maybe LocalStorageDirectory -> IO StorageLocations+networkStorageLocations d = StorageLocations . map (networkStorage d)+ <$> shuffleServers networkServers localStorageLocations :: Maybe LocalStorageDirectory -> StorageLocations localStorageLocations d = StorageLocations $@@ -37,11 +40,48 @@ type UpdateProgress = IO () +data StorageProblem+ = FatalProblem String+ | OverridableProblem String+ deriving (Show)++-- | Check if there is a problem with storing shares amoung the provided+-- storage locations, assuming that some random set of the storage+-- locations will be used.+--+-- It's always a problem to store anything on an Untrusted server.+--+-- It should not be possible to reconstruct the encrypted+-- secret key using only objects from Alternate servers, so+-- fewer than neededObjects Alternate servers can be used.+problemStoringIn :: StorageLocations -> Tunables -> Maybe StorageProblem+problemStoringIn (StorageLocations locs) tunables+ | not (null (getlevel Untrusted)) || length locs < totalObjects ps = + Just $ FatalProblem+ "Not enough servers are available to store your encrypted secret key."+ | length alternates >= neededObjects ps = Just $ OverridableProblem $ unlines $+ [ "Not enough keysafe servers are available that can store"+ , "your encrypted secret key with a recommended level of"+ , "security."+ , "" + , "If you continue, some of the following less secure"+ , "servers will be used:"+ , ""+ ] ++ map descserver alternates+ | otherwise = Nothing+ where+ ps = shareParams tunables+ getlevel sl = filter (\s -> serverLevel s == sl) $+ mapMaybe getServer locs+ alternates = getlevel Alternate+ descserver (Server { serverName = ServerName n, serverDesc = d}) = + "* " ++ n ++ " -- " ++ d+ -- | Stores the shares amoung the storage locations. Each location -- gets at most one share from each set. -- -- If a server is not currently accessible, it will be queued locally.--- If this happens at all, returns True.+-- If any uploads are queued, returns True. -- -- TODO: Add shuffling and queueing/chaffing to prevent -- correlation of related shares.@@ -122,22 +162,22 @@ -- | Returns descriptions of any failures. tryUploadQueued :: Maybe LocalStorageDirectory -> IO [String] tryUploadQueued d = do+ StorageLocations locs <- networkStorageLocations d results <- forM locs $ \loc -> case uploadQueue loc of Nothing -> return [] Just q -> moveShares q loc return $ processresults (concat results) [] where- StorageLocations locs = networkStorageLocations d processresults [] c = nub c processresults (StoreSuccess:rs) c = processresults rs c processresults (StoreFailure e:rs) c = processresults rs (e:c) processresults (StoreAlreadyExists:rs) c = processresults rs ("Unable to upload a share to a server due to a name conflict.":c) -storeChaff :: HostName -> Port -> IO ()-storeChaff hn port = forever $ do- putStrLn $ "Sending chaff to " ++ hn ++ " (press ctrl-c to stop)"- putStrLn "Legend: + = successful upload, ! = upload failure"+storeChaff :: HostName -> Port -> Maybe Seconds -> IO ()+storeChaff hn port delayseconds = forever $ do+ say $ "Sending chaff to " ++ hn ++ " (press ctrl-c to stop)"+ say "Legend: + = successful upload, ! = upload failure" rng <- (cprgCreate <$> createEntropyPool) :: IO SystemRNG let (randomname, rng') = cprgGenerate 128 rng -- It's ok the use the testModeTunables here because@@ -148,17 +188,20 @@ mapConcurrently (go sis rng') [1..totalObjects (shareParams testModeTunables)] where- server = networkStorage Nothing $ Server (ServerName hn) - [ServerAddress hn port]+ server = networkStorage Nothing $ + Server (ServerName hn) Untrusted [ServerAddress hn port] "chaff server" objsize = objectSize defaultTunables * shareOverhead defaultTunables+ maxmsdelay = ceiling $ 1000000 * fromMaybe 0 delayseconds go sis rng n = do+ msdelay <- getStdRandom (randomR (0, maxmsdelay))+ delay msdelay+ let (b, rng') = cprgGenerate objsize rng let share = Share 0 (StorableObject b) let (is, sis') = nextShareIdents sis let i = S.toList is !! (n - 1) r <- storeShare server i share case r of- StoreSuccess -> putStr "+"- _ -> putStr "!"- hFlush stdout+ StoreSuccess -> progress "+"+ _ -> progress "!" go sis' rng' n
TODO view
@@ -1,10 +1,13 @@ Soon: * Get some keysafe servers set up.+* Set up --check-servers in a cron job, so I know when servers are down. Later: -* Implement the different categories of servers in the server list.+* The attack cost display can lead to a false sense of security if the user+ takes it as gospel. It needs to be clear that it's an estimate. This and+ other parts of the keysafe UI need usability testing. * improve restore progress bar points (update after every hash try) * If we retrieved enough shares successfully, but decrypt failed, must be a wrong password, so prompt for re-entry and retry with those shares.@@ -21,7 +24,8 @@ for up to several days, with some uploads of chaff, to prevent collaborating evil servers from correlating related shards. * Add some random padding to http requests and responses, to make it- harder for traffic analysis to tell that it's keysafe traffic.+ harder for traffic analysis to tell that given TOR traffic is+ keysafe traffic. Wishlist:
Tests.hs view
@@ -8,6 +8,7 @@ module Tests where import Types+import Output import Tunables import Encryption import Share@@ -16,7 +17,6 @@ import Control.Exception import System.Directory import System.Posix.Temp-import System.IO import qualified Data.ByteString.UTF8 as BU8 import qualified Data.ByteString as B import qualified Data.Set as S@@ -36,27 +36,27 @@ runTest :: Test -> IO Bool runTest (d, t) = do- putStr $ "testing: " ++ show d ++ " ..."- hFlush stdout+ progress $ "testing: " ++ show d ++ " ..." r <- t case r of Right () -> do- putStrLn "ok"+ say "ok" return True Left e -> do- putStrLn $ "failed: " ++ show e+ say $ "failed: " ++ show e return False runTests :: IO () runTests = do r <- mapM runTest tests if all (== True) r- then putStrLn "All tests succeeded."+ then say "All tests succeeded." else error "Tests failed. Report a bug!" tests :: [Test] tests = - [ backupRestoreTest "very small" $ + [ stableNamingTest "stable naming"+ , backupRestoreTest "very small" $ testSecretKey 1 , backupRestoreTest "full size" $ testSecretKey (objectSize testModeTunables)@@ -64,7 +64,6 @@ testSecretKey (objectSize testModeTunables + 1) , backupRestoreTest "many chunks" $ testSecretKey (objectSize testModeTunables * 10)- , stableNamingTest "stable naming" ] testSecretKey :: Int -> SecretKey
Types/Cost.hs view
@@ -14,7 +14,7 @@ deriving (Show, Eq, Ord) newtype Seconds = Seconds Rational- deriving (Num, Fractional, Eq, Ord)+ deriving (Num, Fractional, Real, RealFrac, Eq, Ord) instance Show Seconds where show (Seconds n) = show (fromRational n :: Double) ++ "s"
Types/Server.hs view
@@ -21,15 +21,20 @@ -- | Name used in queuing uploads to the server. Should remain stable -- across keysafe versions.-newtype ServerName = ServerName String+newtype ServerName = ServerName { fromServerName :: String } deriving (Show, Eq, Ord, Generic) instance ToJSON ServerName instance FromJSON ServerName- ++data ServerLevel = Recommended | Alternate | Untrusted+ deriving (Show, Eq, Ord, Bounded, Enum)+ data Server = Server { serverName :: ServerName+ , serverLevel :: ServerLevel , serverAddress :: [ServerAddress] -- ^ A server may have multiple addresses, or no current address.+ , serverDesc :: String } deriving (Show, Eq, Ord)
UI/NonInteractive.hs view
@@ -6,7 +6,7 @@ module UI.NonInteractive (noninteractiveUI) where import Types.UI-import System.IO+import Output import Control.Exception noninteractiveUI :: UI@@ -22,21 +22,19 @@ } myShowError :: Desc -> IO ()-myShowError desc = hPutStrLn stderr $ "Error: " ++ desc+myShowError desc = warn $ "Error: " ++ desc myShowInfo :: Title -> Desc -> IO ()-myShowInfo _title desc = putStrLn desc+myShowInfo _title desc = say desc myPrompt :: Title -> Desc -> x -> IO a myPrompt _title desc _ = do- putStrLn desc+ say desc error "Not running at a terminal and zenity is not installed; cannot interact with user." myWithProgress :: Title -> Desc -> ((Percent -> IO ()) -> IO a) -> IO a myWithProgress _title desc a = bracket_ setup cleanup (a sendpercent) where- setup = putStrLn desc- sendpercent p = do- putStr (show p ++ "% ")- hFlush stdout- cleanup = putStrLn "done"+ setup = say desc+ sendpercent p = progress (show p ++ "% ")+ cleanup = say "done"
UI/Readline.hs view
@@ -7,11 +7,11 @@ import Types.UI import Types+import Output import System.Console.Readline import System.Posix.Terminal import System.Posix.IO import Control.Exception-import System.IO import Data.List import Data.Char import Text.Read@@ -33,23 +33,23 @@ myShowError :: Desc -> IO () myShowError desc = do- hPutStrLn stderr $ "Error: " ++ desc+ warn $ "Error: " ++ desc _ <- readline "[Press Enter]"- putStrLn ""+ say "" myShowInfo :: Title -> Desc -> IO () myShowInfo title desc = do showTitle title- putStrLn desc- putStrLn ""+ say desc+ say "" myPromptQuestion :: Title -> Desc -> Question -> IO Bool myPromptQuestion title desc question = bracket_ setup cleanup go where setup = do showTitle title- putStrLn desc- cleanup = putStrLn ""+ say desc+ cleanup = say "" go = do mresp <- readline $ question ++ " [y/n] " case mresp of@@ -59,7 +59,7 @@ | "n" `isPrefixOf` (map toLower s) -> return False _ -> do- putStrLn "Please enter 'y' or 'n'"+ say "Please enter 'y' or 'n'" go myPromptName :: Title -> Desc -> Maybe Name -> (Name -> Maybe Problem) -> IO (Maybe Name)@@ -68,8 +68,8 @@ where setup = do showTitle title- putStrLn desc- cleanup = putStrLn ""+ say desc+ cleanup = say "" go = do case suggested of Nothing -> return ()@@ -81,10 +81,10 @@ let n = Name $ BU8.fromString s case checkproblem n of Nothing -> do- putStrLn ""+ say "" return $ Just n Just problem -> do- putStrLn problem+ say problem go Nothing -> return Nothing @@ -93,33 +93,31 @@ where setup = do showTitle title- putStrLn desc+ say desc origattr <- getTerminalAttributes stdInput let newattr = origattr `withoutMode` EnableEcho setTerminalAttributes stdInput newattr Immediately return origattr cleanup origattr = do setTerminalAttributes stdInput origattr Immediately- putStrLn ""+ say "" prompt = do- putStr "Enter password> "- hFlush stdout+ ask "Enter password> " p1 <- getLine- putStrLn ""+ say "" if confirm then promptconfirm p1 else return $ mkpassword p1 promptconfirm p1 = do- putStr "Confirm password> "- hFlush stdout+ ask "Confirm password> " p2 <- getLine- putStrLn ""+ say "" if p1 /= p2 then do- putStrLn "Passwords didn't match, try again..."+ say "Passwords didn't match, try again..." prompt else do- putStrLn ""+ say "" return $ mkpassword p1 mkpassword = Just . Password . BU8.fromString @@ -127,25 +125,24 @@ myPromptKeyId _ _ [] = return Nothing myPromptKeyId title desc l = do showTitle title- putStrLn desc- putStrLn ""+ say desc+ say "" forM_ nl $ \(n, ((Name name), (KeyId kid))) ->- putStrLn $ show n ++ ".\t" ++ BU8.toString name ++ " (keyid " ++ T.unpack kid ++ ")"+ say $ show n ++ ".\t" ++ BU8.toString name ++ " (keyid " ++ T.unpack kid ++ ")" prompt where nl = zip [1 :: Integer ..] l prompt = do- putStr "Enter number> "- hFlush stdout+ ask "Enter number> " r <- getLine- putStrLn ""+ say "" case readMaybe r of Just n | n > 0 && n <= length l -> do- putStrLn ""+ say "" return $ Just $ snd (l !! pred n) _ -> do- putStrLn $ "Enter a number from 1 to " ++ show (length l)+ say $ "Enter a number from 1 to " ++ show (length l) prompt myWithProgress :: Title -> Desc -> ((Percent -> IO ()) -> IO a) -> IO a@@ -153,16 +150,14 @@ where setup = do showTitle title- putStrLn desc- sendpercent p = do- putStr (show p ++ "% ")- hFlush stdout+ say desc+ sendpercent p = ask (show p ++ "% ") cleanup = do- putStrLn "done"- putStrLn ""+ say "done"+ say "" showTitle :: Title -> IO () showTitle title = do- putStrLn title- putStrLn (replicate (length title) '-')- putStrLn ""+ say title+ say (replicate (length title) '-')+ say ""
keysafe.cabal view
@@ -1,5 +1,5 @@ Name: keysafe-Version: 0.20160922+Version: 0.20160927 Cabal-Version: >= 1.8 Maintainer: Joey Hess <joey@kitenet.net> Author: Joey Hess@@ -72,6 +72,8 @@ , async == 2.1.* , unix-compat == 0.4.* , exceptions == 0.8.*+ , random-shuffle == 0.0.*+ , MonadRandom == 0.4.* -- Temporarily inlined due to FTBFS bug -- https://github.com/ocharles/argon2/issues/2 -- argon2 == 1.1.*@@ -95,6 +97,7 @@ HTTP.ProofOfWork HTTP.Server HTTP.RateLimit+ Output SecretKey Serialization ServerBackup
keysafe.hs view
@@ -11,6 +11,7 @@ import Tunables import qualified CmdLine import UI+import Output import Encryption import Entropy import Benchmark@@ -19,10 +20,14 @@ import SecretKey import Share import Storage+import Servers import Types.Server import BackupLog import AutoStart+import HTTP import HTTP.Server+import HTTP.Client+import HTTP.ProofOfWork import ServerBackup import qualified Gpg import Data.Maybe@@ -31,6 +36,7 @@ import Data.Monoid import Data.List import Control.DeepSeq+import Control.Concurrent.Async import qualified Data.Text as T import qualified Data.ByteString as B import qualified Data.ByteString.UTF8 as BU8@@ -79,6 +85,8 @@ restoreServer (CmdLine.localstoragedirectory cmdline) d go (CmdLine.Chaff hn) _ = storeChaff hn (CmdLine.serverPort (CmdLine.serverConfig cmdline))+ (CmdLine.chaffMaxDelay cmdline)+ go CmdLine.CheckServers _ = checkServers go CmdLine.Benchmark _ = benchmarkTunables tunables go CmdLine.Test _ =@@ -87,15 +95,31 @@ backup :: CmdLine.CmdLine -> UI -> Tunables -> SecretKeySource -> SecretKey -> IO () backup cmdline ui tunables secretkeysource secretkey = do installAutoStartFile++ let m = totalObjects (shareParams tunables)+ StorageLocations allocs <- cmdLineStorageLocations cmdline+ let locs = StorageLocations (take m allocs)+ case problemStoringIn locs tunables of+ Nothing -> return ()+ Just (FatalProblem p) -> do+ showError ui p+ error "aborting"+ Just (OverridableProblem p) -> do+ ok <- promptQuestion ui "Server problem"+ p "Continue anyway?"+ if ok+ then return ()+ else error "aborting"+ username <- userName Name theirname <- case CmdLine.name cmdline of Just n -> pure n Nothing -> fromMaybe (error "Aborting on no username") <$> promptName ui "Enter your name" usernamedesc (Just username) validateName- go theirname+ go theirname locs where- go theirname = do+ go theirname locs = do cores <- fromMaybe 1 <$> getNumCores Name othername <- case CmdLine.name cmdline of Just n -> pure n@@ -106,15 +130,15 @@ (kek, passwordentropy) <- promptpassword name let sis = shareIdents tunables name secretkeysource let cost = getCreationCost kek <> getCreationCost sis- (r, queued, locs) <- withProgressIncremental ui "Encrypting and storing data"+ (r, queued, usedlocs) <- withProgressIncremental ui "Encrypting and storing data" (encryptdesc cost cores) $ \addpercent -> do let esk = encrypt tunables kek secretkey shares <- genShares esk tunables _ <- esk `deepseq` addpercent 25 _ <- sis `seq` addpercent 25 let step = 50 `div` sum (map S.size shares)- storeShares (cmdLineStorageLocations cmdline) sis shares (addpercent step)- backuplog <- mkBackupLog $ backupMade (mapMaybe getServer locs) secretkeysource passwordentropy+ storeShares locs sis shares (addpercent step)+ backuplog <- mkBackupLog $ backupMade (mapMaybe getServer usedlocs) secretkeysource passwordentropy case r of StoreSuccess -> do storeBackupLog backuplog@@ -130,7 +154,7 @@ [ "Another secret key is already being stored under the name you entered." , "Please try again with a different name." ]- go theirname+ go theirname locs promptpassword name = do password <- fromMaybe (error "Aborting on no password") <$> promptPassword ui True "Enter password" passworddesc@@ -223,7 +247,8 @@ <$> promptPassword ui True "Enter password" passworddesc let mksis tunables = shareIdents tunables name secretkeydest- r <- downloadInitialShares storagelocations ui mksis possibletunables+ locs <- cmdLineStorageLocations cmdline+ r <- downloadInitialShares locs ui mksis possibletunables case r of Nothing -> showError ui "No shares could be downloaded. Perhaps you entered the wrong name?" Just (tunables, shares, sis, usedservers) -> do@@ -235,12 +260,11 @@ Right esk -> do final <- withProgress ui "Decrypting" (decryptdesc cost cores) $ \setpercent ->- go tunables [shares] usedservers sis setpercent $+ go locs tunables [shares] usedservers sis setpercent $ tryDecrypt candidatekeys esk final =<< getPasswordEntropy password name where- storagelocations = cmdLineStorageLocations cmdline- go tunables firstshares firstusedservers sis setpercent r = case r of+ go locs tunables firstshares firstusedservers sis setpercent r = case r of DecryptFailed -> return $ \_ -> showError ui "Decryption failed! Probably you entered the wrong password." DecryptSuccess secretkey -> do@@ -256,13 +280,13 @@ DecryptIncomplete kek -> do -- Download shares for another chunk. (nextshares, sis', nextusedservers) - <- retrieveShares storagelocations sis (return ())+ <- retrieveShares locs sis (return ()) let shares = firstshares ++ [nextshares] let usedservers = nub (firstusedservers ++ nextusedservers) case combineShares tunables shares of Left e -> return $ \_ -> showError ui e Right esk -> - go tunables shares usedservers sis' setpercent $+ go locs tunables shares usedservers sis' setpercent $ decrypt kek esk namedesc = unlines [ "When you backed up your secret key, you entered some information."@@ -336,9 +360,9 @@ u <- getUserEntryForID =<< getEffectiveUserID return $ Name $ BU8.fromString $ takeWhile (/= ',') (userGecos u) -cmdLineStorageLocations :: CmdLine.CmdLine -> StorageLocations+cmdLineStorageLocations :: CmdLine.CmdLine -> IO StorageLocations cmdLineStorageLocations cmdline- | CmdLine.localstorage cmdline = localStorageLocations lsd+ | CmdLine.localstorage cmdline = return (localStorageLocations lsd) | otherwise = networkStorageLocations lsd where lsd = CmdLine.localstoragedirectory cmdline@@ -384,3 +408,41 @@ =<< Gpg.getSecretKey kid else storeBackupLog =<< mkBackupLog (BackupSkipped (GpgKey kid))++checkServers :: IO ()+checkServers = do+ say $ "Checking " ++ show (length networkServers) ++ " servers concurrently; please wait..."+ results <- mapConcurrently check networkServers+ mapM_ displayresult results+ case filter failed results of+ [] -> return ()+ l+ | length l == length networkServers ->+ error "Failed to connect to any servers. Perhaps TOR is not running?"+ | otherwise -> + error $ "Failed to connect to some servers: "+ ++ show (map (sn . fst) l)+ where+ check s = do+ m <- serverRequest' s motd+ c <- serverRequest s Left Right NoPOWIdent countObjects+ case (m, c) of+ (Right (Motd mt), Right (CountResult cr)) ->+ return (s, Right (mt, cr))+ (Left e, _) -> return (s, Left e)+ (_, Left e) -> return (s, Left e)+ (_, Right (CountFailure e)) -> return (s, Left e)++ displayresult (s, v) = do+ say $ "* " ++ sn s ++ " -- " ++ serverDesc s+ case v of+ Right (mt, cr) -> do+ say $ " MOTD: " ++ T.unpack mt+ say $ " object count: " ++ show cr+ Left e -> warn $+ " failed to connect to " ++ sn s ++ ": " ++ e++ failed (_, Left _) = True+ failed _ = False++ sn = fromServerName . serverName