From c9c476ae7216b80932b80870a2cd06f9339306aa Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Tue, 16 Aug 2016 13:40:52 -0400 Subject: improve options to select secret key to backup/restore --- CmdLine.hs | 14 +++++++++----- ExpensiveHash.hs | 2 +- Serialization.hs | 25 ++++++++++++++++--------- Shard.hs | 8 ++++---- Tunables.hs | 2 +- Types.hs | 12 +++++------- keysafe.cabal | 20 +++++++++++++++++++- keysafe.hs | 48 ++++++++++++++++++++++++++++++++++++------------ 8 files changed, 91 insertions(+), 40 deletions(-) diff --git a/CmdLine.hs b/CmdLine.hs index 8e3040a..ca574bb 100644 --- a/CmdLine.hs +++ b/CmdLine.hs @@ -11,7 +11,7 @@ import qualified Data.ByteString.UTF8 as BU8 data CmdLine = CmdLine { mode :: Mode - , keytype :: KeyType + , secretkeysource :: Maybe SecretKeySource , testMode :: Bool , gui :: Bool } @@ -23,7 +23,7 @@ data Mode = Backup | Restore | Benchmark parse :: Parser CmdLine parse = CmdLine <$> (backup <|> restore <|> benchmark) - <*> keytypeopt + <*> optional (gpgswitch <|> fileswitch) <*> testmodeswitch <*> guiswitch where @@ -39,9 +39,13 @@ parse = CmdLine ( long "benchmark" <> help "Benchmark speed of keysafe's cryptographic primitives." ) - keytypeopt = KeyType . BU8.fromString <$> strOption - ( long "type" - <> help "Type of key (eg, \"gpg\")." + gpgswitch = GpgKey . KeyId . BU8.fromString <$> strOption + ( long "gpgkeyid" + <> help "Specify keyid of gpg key to back up or restore." + ) + fileswitch = KeyFile <$> strOption + ( long "keyfile" + <> 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.)" ) testmodeswitch = switch ( long "testmode" diff --git a/ExpensiveHash.hs b/ExpensiveHash.hs index 0e33604..9cf647f 100644 --- a/ExpensiveHash.hs +++ b/ExpensiveHash.hs @@ -47,7 +47,7 @@ benchmarkExpensiveHash rounds tunables expected = do start <- getCurrentTime forM_ [1..rounds] $ \_ -> do let ExpensiveHash _ t = expensiveHash tunables - (Salt (KeyId gpgKey ("benchmark" :: B.ByteString))) + (Salt (GpgKey (KeyId ("dummy" :: B.ByteString)))) ("himom" :: B.ByteString) t `deepseq` return () end <- getCurrentTime diff --git a/Serialization.hs b/Serialization.hs index 4d6a671..8177821 100644 --- a/Serialization.hs +++ b/Serialization.hs @@ -11,19 +11,26 @@ module Serialization where import Types import Raaz.Core.Encode import qualified Data.ByteString as B +import qualified Data.ByteString.UTF8 as BU8 import Data.Monoid import Data.Word --- | A KeyId is serialized in the form "keytype value". --- For example "gpg C910D9222512E3C7" -instance Encodable KeyId where - toByteString (KeyId (KeyType t) i) = - t <> B.singleton sepChar <> i +-- | A SecretKeySource is serialized in the form "keytype value". +-- For example "gpg C910D9222512E3C7", or "file path". +instance Encodable SecretKeySource where + toByteString (GpgKey (KeyId b)) = + "gpg" <> B.singleton sepChar <> b + toByteString (KeyFile f) = + "file" <> B.singleton sepChar <> BU8.fromString f fromByteString b = case B.break (== sepChar) b of - (t, n) - | B.null n -> Nothing - | otherwise -> Just $ - KeyId (KeyType t) (B.drop 1 n) + (t, rest) + | B.null rest -> Nothing + | otherwise -> + let i = B.drop 1 rest + in case t of + "gpg" -> Just $ GpgKey (KeyId i) + "file" -> Just $ KeyFile (BU8.toString i) + _ -> Nothing instance Encodable Name where toByteString (Name n) = n diff --git a/Shard.hs b/Shard.hs index ec3332e..ad30fbe 100644 --- a/Shard.hs +++ b/Shard.hs @@ -33,10 +33,10 @@ instance Bruteforceable ShardIdents UnknownName where -- -- This is an expensive operation, to make it difficult for an attacker -- to brute force known/guessed names and find matching shards. --- The keyid is used as a salt, both to avoid collisions when the same --- name is chosen for multiple keys, and to prevent the attacker --- from using a rainbow table from names to expensivehashes. -shardIdents :: Tunables -> Name -> KeyId -> ShardIdents +-- The keyid or filename is used as a salt, both to avoid collisions +-- when the same name is chosen for multiple keys, and to prevent the +-- attacker from using a rainbow table from names to expensivehashes. +shardIdents :: Tunables -> Name -> SecretKeySource -> ShardIdents shardIdents tunables (Name name) keyid = ShardIdents idents creationcost bruteforcecalc where diff --git a/Tunables.hs b/Tunables.hs index bd1d4b0..f4f74a4 100644 --- a/Tunables.hs +++ b/Tunables.hs @@ -91,7 +91,7 @@ defaultTunables = Tunables { nameGenerationHash = argon2 10000 (CPUCost (Seconds (2*600))) } , keyEncryptionKeyTunable = KeyEncryptionKeyTunable - { keyEncryptionKeyHash = argon2 110 (CPUCost (Seconds 0)) + { keyEncryptionKeyHash = argon2 115 (CPUCost (Seconds 0)) , randomSaltBytes = 1 -- The keyEncryptionKeyHash is run 256 times per -- random salt byte to brute-force, and its parameters diff --git a/Types.hs b/Types.hs index 799e76e..d3eeccb 100644 --- a/Types.hs +++ b/Types.hs @@ -45,18 +45,16 @@ newtype Password = Password B.ByteString newtype Name = Name B.ByteString deriving (Show) --- | The type of the key that is stored in keysafe. -newtype KeyType = KeyType B.ByteString +-- | Source of the secret key stored in keysafe. +data SecretKeySource = GpgKey KeyId | KeyFile FilePath deriving (Show) -gpgKey :: KeyType -gpgKey = KeyType "gpg" - -- | The keyid is any value that is unique to a private key, and can be -- looked up somehow without knowing the private key. -- --- A gpg keyid is the obvious example. -data KeyId = KeyId KeyType B.ByteString +-- A gpg keyid is the obvious example. But, if a gpg key is not +-- stored on the key servers, keysafe will instead use "". +data KeyId = KeyId B.ByteString deriving (Show) data BenchmarkResult t = BenchmarkResult { expectedBenchmark :: t, actualBenchmark :: t } diff --git a/keysafe.cabal b/keysafe.cabal index 3eee7c6..175bb9f 100644 --- a/keysafe.cabal +++ b/keysafe.cabal @@ -51,8 +51,26 @@ Executable keysafe Other-Modules: Crypto.Argon2.FFI Crypto.Argon2 - ExpensiveHash + CmdLine + Cost + Crypto.SecretSharing.Internal Encryption + Entropy + ExpensiveHash + Serialization + Shard + Storage + Storage.LocalFiles + Tunables + Types + Types.Cost + Types.UI + UI + UI.Readline + UI.Zenity + Utility.HumanTime + Utility.PartialPrelude + Utility.QuickCheck source-repository head type: git diff --git a/keysafe.hs b/keysafe.hs index 7de3079..7f89004 100644 --- a/keysafe.hs +++ b/keysafe.hs @@ -27,20 +27,44 @@ main :: IO () main = do cmdline <- CmdLine.get ui <- selectUI (CmdLine.gui cmdline) - let keytype = CmdLine.keytype cmdline -- TODO determine gpg key id by examining secret key, -- or retrieving public key from keyserver and examining it. - let keyid = KeyId keytype "dummy key id" let tunables = if CmdLine.testMode cmdline then testModeTunables else defaultTunables - case CmdLine.mode cmdline of - CmdLine.Backup -> storedemo ui keyid tunables - CmdLine.Restore -> retrievedemo ui keyid - CmdLine.Benchmark -> benchmarkTunables tunables + case (CmdLine.mode cmdline, CmdLine.secretkeysource cmdline) of + (CmdLine.Backup, Just secretkeysource) -> + backup ui tunables =<< normalize secretkeysource + (CmdLine.Backup, Nothing) -> do + backup ui tunables =<< normalize =<< pickGpgKey CmdLine.Backup ui + (CmdLine.Restore, Just secretkeydest) -> + restore ui =<< normalize secretkeydest + (CmdLine.Restore, Nothing) -> do + restore ui =<< normalize =<< pickGpgKey CmdLine.Backup ui + (CmdLine.Benchmark, _) -> benchmarkTunables tunables -storedemo :: UI -> KeyId -> Tunables -> IO () -storedemo ui keyid tunables = do +-- | Normalize gpg keyids, by querying the gpg keyserver for the key. +-- If the keyserver knows of the key, the long keyid is used. +-- But, if the keyserver does not know of the key, a null keyid is used. +normalize :: SecretKeySource -> IO SecretKeySource +normalize = return -- TODO + +-- | Pick gpg secret key to back up or restore. +-- +-- When backing up, if there is only one secret +-- key, the choice is obvious. Otherwise prompt the user with a list. +-- +-- When restoring, prompt the user for the name of the key, +-- query the keyserver, and let the user pick from a list. +-- The "other" option uses a null keyid, to handle the case where a key is +-- not stored in the keyserver. +pickGpgKey :: CmdLine.Mode -> UI -> IO SecretKeySource +pickGpgKey CmdLine.Backup ui = error "TODO" +pickGpgKey CmdLine.Restore ui = error "TODO" +pickGpgKey _ ui = error "internal error in pickGpgKey" + +backup :: UI -> Tunables -> SecretKeySource -> IO () +backup ui tunables secretkeysource = do username <- userName name <- fromMaybe (error "Aborting on no name") <$> promptName ui "Enter a name" @@ -50,7 +74,7 @@ storedemo ui keyid tunables = do print $ estimateAttack spotAWS $ estimateBruteforceOf kek (passwordEntropy password []) let esk = encrypt tunables kek secretkey - let sis = shardIdents tunables name keyid + let sis = shardIdents tunables name secretkeysource shards <- genShards esk tunables print =<< mapM (uncurry (storeShard localFiles)) (zip (getIdents sis) shards) print =<< obscureShards localFiles @@ -67,13 +91,13 @@ storedemo ui keyid tunables = do , "(Your own full name is a pretty good choice for the name to enter here.)" ] -retrievedemo :: UI -> KeyId -> IO () -retrievedemo ui keyid = do +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 keyid + 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) -- cgit v1.2.3