Skip to content

Commit

Permalink
Add encryptPassphrase implementation and tests using scrypt
Browse files Browse the repository at this point in the history
  • Loading branch information
HeinrichApfelmus committed Feb 14, 2024
1 parent 1ad544e commit 9c9f63d
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 106 deletions.
1 change: 0 additions & 1 deletion lib/secrets/cardano-wallet-secrets.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ library
, cryptonite ^>=0.30
, crypto-hash-extra
, deepseq
, extra
, generic-arbitrary
, memory ^>=0.18
, text
Expand Down
195 changes: 147 additions & 48 deletions lib/secrets/src/Cardano/Wallet/Primitive/Passphrase/Legacy.hs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ module Cardano.Wallet.Primitive.Passphrase.Legacy
checkPassphrase
, preparePassphrase

-- * Testing-only scrypt password implementation
, checkPassphraseTestingOnly
, encryptPassphraseTestingOnly

-- * Testing-only helper
, haveScrypt

-- * Internal functions
-- * Internal functions, exposed for testing
, PassphraseHashLength
, encryptPassphraseTestingOnly
, checkPassphraseCryptonite
, checkPassphraseScrypt
, getSalt
, genSalt
) where
Expand Down Expand Up @@ -63,34 +63,97 @@ import qualified Codec.CBOR.Write as CBOR
import qualified Crypto.KDF.Scrypt as Scrypt
import qualified Data.ByteArray as BA
import qualified Data.ByteString.Char8 as B8
import Data.Either.Extra
( eitherToMaybe
)

#if HAVE_SCRYPT
import Crypto.Scrypt
( EncryptedPass (..)
, Pass (..)
, Salt (Salt)
, encryptPass
, scryptParamsLen
, verifyPass'
)

-- | Verify a wallet spending password using the legacy Byron scrypt encryption
-- scheme.
checkPassphrase :: Passphrase "encryption" -> PassphraseHash -> Maybe Bool
checkPassphrase pwd stored = Just $
verifyPass' (Pass (BA.convert (cborify pwd))) encryptedPass
where
encryptedPass = EncryptedPass (BA.convert stored)

haveScrypt :: Bool
haveScrypt = True

#else
-- | Stub function for when compiled without @scrypt@.
checkPassphrase :: Passphrase "encryption" -> PassphraseHash -> Maybe Bool
checkPassphrase _ _ = Nothing

haveScrypt :: Bool
haveScrypt = False

#endif

{- NOTE [LegacyScrypt]
We need to transition away from the unmaintained `scrypt` package.
Specifically, the `scrypt` package is not supported on `aarch64-darwin`.
We can use the `cryptonite` package instead.
In order to ensure that the new package produces the same result as the
old one, we proceed as follows:
* in production:
* use `scrypt` package if available
* use `cryptonite` on `aarch64-darwin`
* in testing:
* generate random passphrases,
encrypted with `scrypt` package if available.
* check that these encrypted passphrases
are verified correctly with the `cryptonite` package
These tests ensure that the code using `cryptonite` can verify
hashed passphrases that were created with `scrypt`.
-}

{-----------------------------------------------------------------------------
Passphrase hashing and verification using the Scrypt algorithm
------------------------------------------------------------------------------}
-- | Verify a wallet spending password using the legacy Byron scrypt encryption
-- scheme.
checkPassphrase :: Passphrase "encryption" -> PassphraseHash -> Maybe Bool
#if HAVE_SCRYPT
checkPassphrase pwd stored = Just $ checkPassphraseScrypt pwd stored
#else
-- Stub function for when compiled without @scrypt@.
checkPassphrase _ _ = Nothing
#endif

-- | Encrypt a wallet spending password using
-- the legacy Byron scrypt encryption scheme.
--
-- Do not use this function in production, only in unit tests!
encryptPassphraseTestingOnly
:: MonadRandom m
=> PassphraseHashLength
-> Passphrase "encryption"
-> m PassphraseHash
#if HAVE_SCRYPT
encryptPassphraseTestingOnly len pwd = mkPassphraseHash <$> genSalt
where
mkPassphraseHash :: Passphrase "salt" -> PassphraseHash
mkPassphraseHash (Passphrase salt) =
PassphraseHash
. BA.convert
. getEncryptedPass
$ encryptPass
params
(Salt $ BA.convert salt)
(Pass . BA.convert . unPassphrase $ cborify pwd)

params =
case scryptParamsLen
(fromIntegral logN)
(fromIntegral r)
(fromIntegral p)
(fromIntegral len)
of
Just x -> x
Nothing -> error "scryptParamsLen: unreachable code path"
#else
encryptPassphraseTestingOnly len pwd =
encryptPassphraseCryptoniteWithLength len <$> genSalt <*> pure pwd
#endif

preparePassphrase :: Passphrase "user" -> Passphrase "encryption"
Expand All @@ -100,45 +163,81 @@ preparePassphrase = Passphrase . hashMaybe . unPassphrase
| pw == mempty = mempty
| otherwise = BA.convert $ blake2b256 pw

-- | This is for use by test cases only. Use only the implementation from the
-- @scrypt@ package for application code.
checkPassphraseTestingOnly :: Passphrase "encryption" -> PassphraseHash -> Bool
checkPassphraseTestingOnly pwd stored = case getSalt stored of
Just salt -> encryptPassphraseTestingOnly pwd salt == stored
Nothing -> False
{-----------------------------------------------------------------------------
Passphrase verification
------------------------------------------------------------------------------}

cborify :: Passphrase "encryption" -> Passphrase "encryption"
cborify = Passphrase . BA.convert . CBOR.toStrictByteString
. CBOR.encodeBytes . BA.convert . unPassphrase
checkPassphraseScrypt :: Passphrase "encryption" -> PassphraseHash -> Bool
#if HAVE_SCRYPT
checkPassphraseScrypt pwd stored =
verifyPass' (Pass (BA.convert (cborify pwd))) encryptedPass
where
encryptedPass = EncryptedPass (BA.convert stored)
#else
checkPassphraseScrypt _ _ = error "checkPassphraseScrypt not available"
#endif

checkPassphraseCryptonite
:: Passphrase "encryption" -> PassphraseHash -> Bool
checkPassphraseCryptonite pwd stored =
case parsePassphraseHash stored of
Just (salt, len) ->
encryptPassphraseCryptoniteWithLength len salt pwd == stored
Nothing -> False

-- | Extract salt field from pipe-delimited password hash.
-- This will fail unless there are exactly 5 fields
getSalt :: PassphraseHash -> Maybe (Passphrase "salt")
getSalt (PassphraseHash stored) = case B8.split '|' (BA.convert stored) of
[_logN, _r, _p, salt, _passHash] -> eitherToMaybe $
Passphrase <$> convertFromBase Base64 salt
_ -> Nothing

-- | This is for use by test cases only.
encryptPassphraseTestingOnly
:: MonadRandom m
=> Passphrase "encryption"
-> m PassphraseHash
encryptPassphraseTestingOnly pwd = mkPassphraseHash <$> genSalt
getSalt = fmap fst . parsePassphraseHash

parsePassphraseHash
:: PassphraseHash
-> Maybe (Passphrase "salt", PassphraseHashLength)
parsePassphraseHash (PassphraseHash stored) =
case B8.split '|' (BA.convert stored) of
[_logN, _r, _p, salt64, hash64]
| Right salt <- convertFromBase Base64 salt64
, Right hash <- convertFromBase Base64 hash64
-> Just (Passphrase salt, B8.length hash)
_ -> Nothing

{-----------------------------------------------------------------------------
Passphrase hashing
------------------------------------------------------------------------------}
type PassphraseHashLength = Int

encryptPassphraseCryptoniteWithLength
:: PassphraseHashLength
-> Passphrase "salt"
-> Passphrase "encryption"
-> PassphraseHash
encryptPassphraseCryptoniteWithLength len (Passphrase salt) pwd =
PassphraseHash $ BA.convert combined
where
mkPassphraseHash salt = PassphraseHash $ BA.convert $ B8.intercalate "|"
[ showBS logN, showBS r, showBS p
, convertToBase Base64 salt, convertToBase Base64 (passHash salt)]
combined = B8.intercalate "|"
[ showBS logN
, showBS r
, showBS p
, convertToBase Base64 salt
, convertToBase Base64 passphraseHash
]

passHash :: Passphrase "salt" -> ByteString
passHash (Passphrase salt) = Scrypt.generate params (cborify pwd) salt
passphraseHash :: ByteString
passphraseHash = Scrypt.generate params (cborify pwd) salt

params = Scrypt.Parameters ((2 :: Word64) ^ logN) r p 64
logN = 14
r = 8
p = 1
params = Scrypt.Parameters ((2 :: Word64) ^ logN) r p len

showBS = B8.pack . show

-- Scrypt parameters
logN, r, p :: Int
logN = 14
r = 8
p = 1

cborify :: Passphrase "encryption" -> Passphrase "encryption"
cborify = Passphrase . BA.convert . CBOR.toStrictByteString
. CBOR.encodeBytes . BA.convert . unPassphrase

genSalt :: MonadRandom m => m (Passphrase "salt")
genSalt = Passphrase <$> getRandomBytes 32
Loading

0 comments on commit 9c9f63d

Please sign in to comment.