summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--Cost.hs34
-rw-r--r--Encryption.hs87
-rw-r--r--Tunables.hs20
-rw-r--r--keysafe.cabal1
4 files changed, 97 insertions, 45 deletions
diff --git a/Cost.hs b/Cost.hs
index c31c4f8..38b6e28 100644
--- a/Cost.hs
+++ b/Cost.hs
@@ -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.*