diff options
author | Joey Hess <joeyh@joeyh.name> | 2016-08-07 16:25:12 -0400 |
---|---|---|
committer | Joey Hess <joeyh@joeyh.name> | 2016-08-07 16:25:12 -0400 |
commit | 39707fda6289740729bef8cb214a2bf3f555b86e (patch) | |
tree | 98fa699a499b2ef679f88785d65aaee445c748c4 | |
parent | 460edfad8ed45412050822dfdf84f2d54015fb04 (diff) | |
download | keysafe-39707fda6289740729bef8cb214a2bf3f555b86e.tar.gz |
finish AES decryption puzzle implementation
-rw-r--r-- | Cost.hs | 34 | ||||
-rw-r--r-- | Encryption.hs | 87 | ||||
-rw-r--r-- | Tunables.hs | 20 | ||||
-rw-r--r-- | keysafe.cabal | 1 |
4 files changed, 97 insertions, 45 deletions
@@ -88,7 +88,7 @@ bruteForceLinearSearch :: Cost step -> CostCalc BruteForceOp t bruteForceLinearSearch stepcost e = castCost stepcost `raiseCostPower` reduceEntropy e 1 --- | Things that can be brute-forced expose a CostCalc. +-- | Things that can be brute-forced track their CostCalc. class Bruteforceable t a where getBruteCostCalc :: t -> CostCalc BruteForceOp a @@ -96,22 +96,32 @@ class Bruteforceable t a where estimateBruteforceOf :: Bruteforceable t a => t -> Entropy a -> Cost BruteForceOp estimateBruteforceOf t e = getBruteCostCalc t e --- | Estimate of cost of brute force attack using AWS Spot instances, --- in US dollars. --- +data DataCenterPrice = DataCenterPrice + { instanceCpuCores :: Integer + , instanceCostPerHour :: Cents + } + -- August 2016 spot pricing: 36 CPU core c4.8xlarge at 33c/hour +spotAWS :: DataCenterPrice +spotAWS = DataCenterPrice + { instanceCpuCores = 36 + , instanceCostPerHour = Cents 33 + } + +-- | Estimate of cost of brute force attack using a datacenter. -- --- Note that less GPU time is available on these instances; --- there are not 36 GPU cores. But for simplicity we assume there are --- that many GPU cores. So, this underestimates the price to brute --- force such operations. -estimateAWSSpotAttack :: Cost BruteForceOp -> Dollars -estimateAWSSpotAttack opcost = centsToDollars $ costcents +-- Note that this assumes that CPU cores and GPU cores are of equal number, +-- which is unlikely to be the case; typically there will be many more +-- cores than GPUs. So, this underestimates the price to brute force +-- operations which run faster on GPUs. +estimateAttack :: DataCenterPrice -> Cost BruteForceOp -> Dollars +estimateAttack dc opcost = centsToDollars $ costcents where (Seconds cpuseconds) = fst (totalCost opcost) cpuyears = cpuseconds `div` (60*60*24*365) - cpucores = 36 - costpercpuyear = Cents (33*24*365 `div` cpucores) + costpercpuyear = Cents $ + fromIntegral (instanceCostPerHour dc) * 24 * 365 + `div` instanceCpuCores dc costcents = Cents cpuyears * costpercpuyear newtype Cents = Cents Integer diff --git a/Encryption.hs b/Encryption.hs index 23da288..a31c004 100644 --- a/Encryption.hs +++ b/Encryption.hs @@ -6,11 +6,13 @@ import Types import Cost import Tunables import ExpensiveHash -import qualified Data.ByteString as B -import Raaz.Core.Encode -import qualified Raaz.Cipher.AES as AES import Data.Word +import Data.Bits import Data.Monoid +import Data.Maybe +import qualified Data.ByteString as B +import Raaz +import qualified Raaz.Cipher.AES as AES -- | An AES key, which is used to encrypt the key that is stored -- in keysafe. @@ -24,38 +26,73 @@ instance Bruteforceable KeyEncryptionKey UnknownPassword where -- | The ExpensiveHash of the Password is combined with a -- RandomObstacle to form the AES key. Combination method is logical OR. -genKeyEncryptionKey :: Tunables -> KeyIdent -> Password -> KeyEncryptionKey -genKeyEncryptionKey tunables keyident password = - KeyEncryptionKey k decryptcost bruteforcecalc +genKeyEncryptionKey :: Tunables -> KeyIdent -> Password -> IO KeyEncryptionKey +genKeyEncryptionKey tunables keyident password = do + ob@(RandomObstacle ok) <- genRandomObstacle tunables + -- Truncate the hash to the AES key length. + let truncatedhashb = B.take (B.length (toByteString ok)) hashb + let k = fromMaybe (error "genKeyEncryptionKey fromByteString failed") $ + fromByteString truncatedhashb + let strongk = mixinRandomObstacle ob k + return $ KeyEncryptionKey strongk decryptcost bruteforcecalc where - k = undefined -- hashb <> ob -- TODO use logical OR (ExpensiveHash hashcost hashb) = expensiveHash tunables salt password salt = Salt keyident - (RandomObstacle ob) = genRandomObstacle decryptcost - decryptcost = CombinedCost (decryptionCost tunables) (castCost hashcost) + decryptcost = CombinedCost (decryptionPuzzleCost tunables) (castCost hashcost) -- To brute force data encrypted with this key, -- an attacker needs to pay the decryptcost for each password -- checked. bruteforcecalc = bruteForceLinearSearch decryptcost --- | A random value which adds difficulty to decrypting, since it's never --- written down anywhere and must always be brute-forced. +-- | A random value which can be mixed into an AES key to +-- require decrypting it to perform some brute-force work. -- --- It's always 64 bits long, and is left padded with 0's, --- which are followed by a series of random bits (which necessarily always --- starts with 1). Eg: --- --- > 0000000000000000000000000000000000000000000000000000000100011100 --- --- The fewer leading 0's and thus longer the random bits, --- the harder it is. -data RandomObstacle = RandomObstacle B.ByteString +-- The random value is right-padded with NULL bytes, so ORing it with an AES +-- key varies the initial bytes of the key. +data RandomObstacle = RandomObstacle AES.KEY256 --- | Generate a random obstacle that will add the specified cost to AES --- decryption. +-- | Length of the random obstacle, in bytes, that will satisfy the +-- decryptionPuzzleCost. -- -- AES can be calculated more efficiently by a GPU, so the cost must be -- a GPU cost. -genRandomObstacle :: Cost DecryptionOp -> RandomObstacle -genRandomObstacle (GPUCost c) = undefined -genRandomObstacle _ = error "decryptionCost must be a GPUCost" +-- +-- This depends on the objectSize, because to brute force the +-- RandomObstable, AES decryptions must be done repeatedly, and the +-- time needed for an AES decryption depends on the amount of data. +sizeRandomObstacle :: Tunables -> Int +sizeRandomObstacle tunables = ceiling $ nbits / 8 + where + -- in 2016, a GPU can run AES at 10 GB/s. + bytespersecond = 10*1024*1024*1024 + triespersecond = bytespersecond `div` fromIntegral (objectSize tunables) + targetseconds = case decryptionPuzzleCost tunables of + GPUCost (Seconds n) -> n + _ -> error "decryptionPuzzleCost must be a GPUCost" + -- Add one bit of entropy, because a brute-force attack will + -- on average succeed half-way through the search space. + nbits = logBase 2 (fromIntegral $ targetseconds * triespersecond) + 1 + +mkRandomObstacle :: AES.KEY256 -> Int -> AES.KEY256 +mkRandomObstacle k nbytes = + fromMaybe (error "mkRandomObstacle fromByteString failed") $ + fromByteString ob + where + kb = toByteString k + rightnulls = B.replicate (B.length kb - nbytes) 0 + ob = B.take nbytes kb <> rightnulls + +genRandomObstacle :: Tunables -> IO RandomObstacle +genRandomObstacle tunables = do + prg <- newPRG () :: IO SystemPRG + randomkey <- random prg :: IO AES.KEY256 + let size = sizeRandomObstacle tunables + return $ RandomObstacle $ mkRandomObstacle randomkey size + +mixinRandomObstacle :: RandomObstacle -> AES.KEY256 -> AES.KEY256 +mixinRandomObstacle (RandomObstacle r) k = + fromMaybe (error "mixinRandomObstacle fromByteString failed") $ + fromByteString $ toByteString r `orBytes` toByteString k + +orBytes :: B.ByteString -> B.ByteString -> B.ByteString +orBytes a b = B.pack $ map (uncurry (.|.)) $ zip (B.unpack a) (B.unpack b) diff --git a/Tunables.hs b/Tunables.hs index f5832b4..79fb2a8 100644 --- a/Tunables.hs +++ b/Tunables.hs @@ -5,16 +5,19 @@ import Cost import qualified Crypto.Argon2 as Argon2 data Tunables = Tunables - { argonOptions :: Argon2.HashOptions + { objectSize :: Int + -- ^ size of objects stored in keysafe, in bytes + , argonOptions :: Argon2.HashOptions , argonCost :: Cost CreationOp -- ^ should correspond to the argonOptions - , decryptionCost :: Cost DecryptionOp - -- ^ controls the decryption cost + , decryptionPuzzleCost :: Cost DecryptionOp + -- ^ cost of decryption puzzle } defaultTunables :: Tunables defaultTunables = Tunables - { argonOptions = Argon2.HashOptions + { objectSize = 1024*64 -- 64 kb + , argonOptions = Argon2.HashOptions { Argon2.hashIterations = 10000 , Argon2.hashMemory = 131072 -- 128 mebibtyes per thread , Argon2.hashParallelism = 4 -- 4 threads @@ -30,13 +33,14 @@ defaultTunables = Tunables -- This is set to only 1 minute because GPUs are quite a lot -- faster than CPUs at AES, and so setting it higher would make -- clients too slow at key recovery. - , decryptionCost = GPUCost (Seconds 60) + , decryptionPuzzleCost = GPUCost (Seconds 60) } -- | Dials back cryptographic difficulty, not for production use. testModeTunables :: Tunables testModeTunables = Tunables - { argonOptions = Argon2.defaultHashOptions - , argonCost = CPUCost (Seconds 0) - , decryptionCost = GPUCost (Seconds 0) + { objectSize = 1024*64 + , argonOptions = Argon2.defaultHashOptions + , argonCost = CPUCost (Seconds (2*600)) + , decryptionPuzzleCost = GPUCost (Seconds 60) } diff --git a/keysafe.cabal b/keysafe.cabal index 6b1729e..1e586f7 100644 --- a/keysafe.cabal +++ b/keysafe.cabal @@ -21,6 +21,7 @@ Executable keysafe base (>= 4.5) , bytestring == 0.10.* , deepseq == 1.4.* + , random == 1.1.* , secret-sharing == 1.0.* , raaz == 0.0.2 , argon2 == 1.1.* |