diff --git a/lib/core-integration/src/Test/Integration/Plutus.hs b/lib/core-integration/src/Test/Integration/Plutus.hs index ceb353a7d1e..43434f456b0 100644 --- a/lib/core-integration/src/Test/Integration/Plutus.hs +++ b/lib/core-integration/src/Test/Integration/Plutus.hs @@ -38,7 +38,7 @@ import Cardano.Wallet.Api.Types import Cardano.Wallet.Primitive.Types.Hash ( Hash (..) ) import Cardano.Wallet.Unsafe - ( unsafeFromHex, unsafeRight ) + ( unsafeFromHexText, unsafeRight ) import Codec.Binary.Bech32.TH ( humanReadablePart ) import Codec.Serialise @@ -100,7 +100,7 @@ alwaysTrueValidator = hashScript :: Text -> ApiT (Hash "TokenPolicy") hashScript = - ApiT . Hash . blake2b224 . unsafeFromHex . ("01" <>) . T.encodeUtf8 + ApiT . Hash . blake2b224 . unsafeFromHexText . ("01" <>) -- -- Ping Pong @@ -365,7 +365,7 @@ currencyTx input = Aeson.object , CBOR.TBool True, CBOR.TNull ] transaction_witness_set = CBOR.TMap - [ (c_plutus_script, CBOR.TList [ CBOR.TBytes (fromHex policy) ]) + [ (c_plutus_script, CBOR.TList [CBOR.TBytes (unsafeFromHexText policy)]) , (c_plutus_data, CBOR.TList []) -- leave empty ] [c_plutus_script, c_plutus_data] = map CBOR.TInt [3,4] @@ -395,9 +395,6 @@ currencyTx input = Aeson.object toHex :: BS.ByteString -> Text toHex = T.decodeUtf8 . Base16.encode -fromHex :: Text -> BS.ByteString -fromHex = Base16.decodeLenient . T.encodeUtf8 - -- | Minting policy that mints 1_000 units of "apfel" and 1 unit of "banana" -- when a specific UTxO is spent (which can be done only once). mkCurrencyPolicy :: ApiWalletInput n -> (Text, ApiT (Hash "TokenPolicy")) diff --git a/lib/core-integration/src/Test/Integration/Scenario/API/Shelley/Wallets.hs b/lib/core-integration/src/Test/Integration/Scenario/API/Shelley/Wallets.hs index a6edc92676f..d79cd6b674c 100644 --- a/lib/core-integration/src/Test/Integration/Scenario/API/Shelley/Wallets.hs +++ b/lib/core-integration/src/Test/Integration/Scenario/API/Shelley/Wallets.hs @@ -49,7 +49,7 @@ import Cardano.Wallet.Primitive.SyncProgress import Cardano.Wallet.Primitive.Types ( walletNameMaxLength, walletNameMinLength ) import Cardano.Wallet.Unsafe - ( unsafeFromHex, unsafeXPub ) + ( unsafeFromHexText, unsafeXPub ) import Control.Monad ( forM, forM_ ) import Control.Monad.IO.Class @@ -1150,7 +1150,7 @@ spec = describe "SHELLEY_WALLETS" $ do let sigBytes = BL.toStrict $ getFromResponse id rSig let sig = CC.xsignature sigBytes let key = unsafeXPub $ fst (getFromResponse #getApiVerificationKey rKey) <> dummyChainCode - let msgHash = unsafeFromHex "1228cd0fea46f9a091172829f0c492c0516dceff67de08f585a4e048a28a6c9f" + let msgHash = unsafeFromHexText "1228cd0fea46f9a091172829f0c492c0516dceff67de08f585a4e048a28a6c9f" liftIO $ CC.verify key msgHash <$> sig `shouldBe` Right True let goldenSig = "680739414d89eb9f4377192171ce3990c7beea6132a04f327d7c954ae9e7fcfe747dd7b4b9b11acefa1aa75216b837fc81e59c24001b96356ba65598ec159d0c" :: ByteString diff --git a/lib/core/src/Cardano/Wallet/Primitive/Passphrase/Types.hs b/lib/core/src/Cardano/Wallet/Primitive/Passphrase/Types.hs index e47aa250867..7e2957e89d2 100644 --- a/lib/core/src/Cardano/Wallet/Primitive/Passphrase/Types.hs +++ b/lib/core/src/Cardano/Wallet/Primitive/Passphrase/Types.hs @@ -37,7 +37,7 @@ import Crypto.Random.Types import Data.Bifunctor ( first ) import Data.ByteArray - ( ByteArrayAccess, ScrubbedBytes ) + ( ByteArray, ByteArrayAccess, ScrubbedBytes ) import Data.ByteArray.Encoding ( Base (..), convertToBase ) import Data.Proxy @@ -169,7 +169,7 @@ instance ToText PassphraseScheme where newtype PassphraseHash = PassphraseHash { getPassphraseHash :: ScrubbedBytes } deriving stock (Show) - deriving newtype (Eq, NFData, ByteArrayAccess) + deriving newtype (Eq, Ord, Semigroup, Monoid, NFData, ByteArrayAccess, ByteArray) instance ToText PassphraseHash where toText = T.decodeUtf8 . convertToBase Base16 . getPassphraseHash diff --git a/lib/core/src/Cardano/Wallet/Unsafe.hs b/lib/core/src/Cardano/Wallet/Unsafe.hs index 9e2d60be17f..786924258fe 100644 --- a/lib/core/src/Cardano/Wallet/Unsafe.hs +++ b/lib/core/src/Cardano/Wallet/Unsafe.hs @@ -20,6 +20,7 @@ module Cardano.Wallet.Unsafe ( unsafeRight , unsafeFromHex + , unsafeFromHexText , unsafeFromBase64 , unsafeFromHexFile , unsafeDecodeAddress @@ -70,6 +71,8 @@ import Control.Monad.Trans.Except ( ExceptT (..), runExceptT ) import Data.Binary.Get ( Get, runGet ) +import Data.ByteArray + ( ByteArray ) import Data.ByteArray.Encoding ( Base (..), convertFromBase ) import Data.ByteString @@ -104,6 +107,7 @@ import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as B8 import qualified Data.ByteString.Lazy as BL import qualified Data.Text as T +import qualified Data.Text.Encoding as T import qualified Data.Text.IO as TIO -- | Take the right side of an 'Either' value. Crash badly if it was a left. @@ -111,8 +115,13 @@ unsafeRight :: (Buildable e, HasCallStack) => Either e a -> a unsafeRight = either (internalError . build) id -- | Decode an hex-encoded 'ByteString' into raw bytes, or fail. -unsafeFromHex :: HasCallStack => ByteString -> ByteString -unsafeFromHex = unsafeRight . convertFromBase @ByteString @ByteString Base16 +unsafeFromHex :: forall b. (HasCallStack, ByteArray b) => ByteString -> b +unsafeFromHex = unsafeRight . convertFromBase @ByteString @b Base16 + +-- | Decode hex-encoded 'Text' into a 'ByteString', or fail. This variant of +-- 'unsafeFromHex' may be easier to use because it's not polymorphic. +unsafeFromHexText :: HasCallStack => Text -> ByteString +unsafeFromHexText = unsafeFromHex . T.encodeUtf8 -- | Decode a base64-encoded 'ByteString' into raw bytes, or fail. unsafeFromBase64 :: HasCallStack => ByteString -> ByteString diff --git a/lib/core/test/unit/Cardano/Wallet/Primitive/Passphrase/LegacySpec.hs b/lib/core/test/unit/Cardano/Wallet/Primitive/Passphrase/LegacySpec.hs index 440df340c54..75351ebc04f 100644 --- a/lib/core/test/unit/Cardano/Wallet/Primitive/Passphrase/LegacySpec.hs +++ b/lib/core/test/unit/Cardano/Wallet/Primitive/Passphrase/LegacySpec.hs @@ -35,6 +35,10 @@ import Cardano.Wallet.Unsafe ( unsafeFromHex ) import Control.Monad.IO.Class ( liftIO ) +import Data.ByteArray + ( ByteArray ) +import Data.ByteArray.Encoding + ( Base (..), convertToBase ) import Data.ByteString ( ByteString ) import GHC.Stack @@ -48,17 +52,27 @@ import Test.QuickCheck import Test.QuickCheck.Monadic ( monadicIO ) -import qualified Data.ByteArray as BA -import qualified Data.Text.Encoding as T +spec :: Spec +spec = parallel $ do + scryptoniteSpec + onlyWithScrypt $ do + scryptPropsSpec + scryptGoldenSpec +onlyWithScrypt :: HasCallStack => Spec -> Spec +onlyWithScrypt = + if haveScrypt + then id + else before_ (pendingWith "needs to be compiled with scrypt to run") -spec :: Spec -spec = onlyWithScrypt $ describe "Scrypt tests-only cryptonite version" $ do +{------------------------------------------------------------------------------- + Cryptonite-based implementation +-------------------------------------------------------------------------------} + +scryptoniteSpec :: Spec +scryptoniteSpec = describe "Scrypt tests-only cryptonite version" $ do it "Verify passphrase" $ do let Just salt = getSalt fixturePassphraseEncrypted - let - unwrap :: PassphraseHash -> ByteString - unwrap (PassphraseHash x) = BA.convert x let x = encryptPassphraseTestingOnly (preparePassphrase fixturePassphrase) salt @@ -79,63 +93,13 @@ spec = onlyWithScrypt $ describe "Scrypt tests-only cryptonite version" $ do checkPassphraseTestingOnly (preparePassphrase fixturePassphrase) fixturePassphraseEncryptedWrongHash `shouldBe` False it "getSalt" $ do - let - unwrap :: Passphrase "salt" -> ByteString - unwrap (Passphrase x) = BA.convert x - unwrap <$> (getSalt fixturePassphraseEncrypted) - `shouldBe` Just "\250:-\180R\EM\141Y\151\160.\203\149\&0YZV3\ENQ~\ - \\DLEC\a\221\184[\177\nC\aR+" - - it "checkPassphrase p h(p) == Right () for Scrypt passwords" $ - property prop_passphraseFromScryptRoundtrip - it "p /= p' => checkPassphrase p' h(p) == Left ErrWrongPassphrase for Scrypt passwords" $ - property prop_passphraseFromScryptRoundtripFail + let hex :: Passphrase "salt" -> ByteString + hex = convertToBase Base16 . unPassphrase + hex <$> (getSalt fixturePassphraseEncrypted) + `shouldBe` Just "fa3a2db452198d5997a02ecb9530595a5633057e104307ddb85bb10a4307522b" - parallel $ describe "golden test legacy passphrase encryption" $ do - it "compare new implementation with cardano-sl - short password" $ do - let pwd = Passphrase $ BA.convert $ T.encodeUtf8 "patate" - let hash = PassphraseHash $ BA.convert $ unsafeFromHex $ mconcat - [ "31347c387c317c574342652b796362417576356c2b4258676a344a314c" - , "6343675375414c2f5653393661364e576a2b7550766655513d3d7c2f37" - , "6738486c59723174734e394f6e4e753253302b6a65515a6b5437316b45" - , "414941366a515867386539493d" - ] - checkPassphrase EncryptWithScrypt pwd hash `shouldBe` Right () - it "compare new implementation with cardano-sl - normal password" $ do - let pwd = Passphrase @"user" $ BA.convert $ T.encodeUtf8 "Secure Passphrase" - let hash = PassphraseHash $ BA.convert $ unsafeFromHex $ mconcat - [ "31347c387c317c714968506842665966555a336f5156434c384449744b" - , "677642417a6c584d62314d6d4267695433776a556f3d7c53672b436e30" - , "4232766b4475682f704265335569694577633364385845756f55737661" - , "42514e62464443353569474f4135736e453144326743346f47564c472b" - , "524331385958326c6863552f36687a38432f496172773d3d" - ] - checkPassphrase EncryptWithScrypt pwd hash `shouldBe` Right () - it "compare new implementation with cardano-sl - empty password" $ do - let pwd = Passphrase @"user" $ BA.convert $ T.encodeUtf8 "" - let hash = PassphraseHash $ BA.convert $ unsafeFromHex $ mconcat - [ "31347c387c317c5743424875746242496c6a66734d764934314a30727a7" - , "9663076657375724954796376766a793150554e377452673d3d7c54753" - , "434596d6e547957546c5759674a3164494f7974474a7842632b432f786" - , "2507657382b5135356a38303d" - ] - checkPassphrase EncryptWithScrypt pwd hash `shouldBe` Right () - it "compare new implementation with cardano-sl - cardano-wallet password" $ do - let pwd = Passphrase @"user" $ BA.convert $ T.encodeUtf8 "cardano-wallet" - let hash = PassphraseHash $ BA.convert $ unsafeFromHex $ mconcat - [ "31347c387c317c2b6a6f747446495a6a566d586f43374c6c54425a576c" - , "597a425834515177666475467578436b4d485569733d7c78324d646738" - , "49554a3232507235676531393575445a76583646552b7757395a6a6a2f" - , "51303054356c654751794279732f7662753367526d726c316c657a7150" - , "43676d364e6758476d4d2f4b6438343265304b4945773d3d" - ] - checkPassphrase EncryptWithScrypt pwd hash `shouldBe` Right () - -onlyWithScrypt :: HasCallStack => Spec -> Spec -onlyWithScrypt = - if haveScrypt - then id - else before_ (pendingWith "needs to be compiled with scrypt to run") +unwrap :: ByteArray b => b -> ByteString +unwrap = convertToBase Base16 -- | Default passphrase used for fixture wallets fixturePassphrase :: Passphrase "user" @@ -146,7 +110,7 @@ fixturePassphraseWrong = Passphrase "wrong" -- | fixturePassphrase encrypted by Scrypt function fixturePassphraseEncrypted :: PassphraseHash -fixturePassphraseEncrypted = PassphraseHash $ BA.convert $ unsafeFromHex +fixturePassphraseEncrypted = unsafeFromHex "31347c387c317c2b6a6f747446495a6a566d586f43374c6c54425a576c\ \597a425834515177666475467578436b4d485569733d7c78324d646738\ \49554a3232507235676531393575445a76583646552b7757395a6a6a2f\ @@ -154,7 +118,7 @@ fixturePassphraseEncrypted = PassphraseHash $ BA.convert $ unsafeFromHex \43676d364e6758476d4d2f4b6438343265304b4945773d3d" fixturePassphraseEncryptedWrongSalt :: PassphraseHash -fixturePassphraseEncryptedWrongSalt = PassphraseHash $ BA.convert $ unsafeFromHex +fixturePassphraseEncryptedWrongSalt = unsafeFromHex "31347c387c317c206a6f747446495a6a566d586f43374c6c54425a576c\ \597a425834515177666475467578436b4d485569733d7c78324d646738\ \49554a3232507235676531393575445a76583646552b7757395a6a6a2f\ @@ -162,17 +126,69 @@ fixturePassphraseEncryptedWrongSalt = PassphraseHash $ BA.convert $ unsafeFromHe \43676d364e6758476d4d2f4b6438343265304b4945773d3d" fixturePassphraseEncryptedWrongHash :: PassphraseHash -fixturePassphraseEncryptedWrongHash = PassphraseHash $ BA.convert $ unsafeFromHex +fixturePassphraseEncryptedWrongHash = unsafeFromHex "31347c387c317c2b6a6f747446495a6a566d586f43374c6c54425a576c\ \597a425834515177666475467578436b4d485569733d7c78324d646738\ \49554a3232507235676531393575445a76583646552b7757395a6a6a2f\ \51303054356c654751794279732f7662753367526d726c316c657a7150\ \43676d364e6758476d4d2f4b6438343265304b4945773d30" +{------------------------------------------------------------------------------- + Golden Tests +-------------------------------------------------------------------------------} + +scryptGoldenSpec :: Spec +scryptGoldenSpec = + describe "golden tests comparing this implementation with cardano-sl" $ do + it "short password" $ do + let pwd = Passphrase "patate" + let hash = unsafeFromHex + "31347c387c317c574342652b796362417576356c2b4258676a344a314c\ + \6343675375414c2f5653393661364e576a2b7550766655513d3d7c2f37\ + \6738486c59723174734e394f6e4e753253302b6a65515a6b5437316b45\ + \414941366a515867386539493d" + checkPassphrase EncryptWithScrypt pwd hash `shouldBe` Right () + + it "normal password" $ do + let pwd = Passphrase @"user" "Secure Passphrase" + let hash = unsafeFromHex + "31347c387c317c714968506842665966555a336f5156434c384449744b\ + \677642417a6c584d62314d6d4267695433776a556f3d7c53672b436e30\ + \4232766b4475682f704265335569694577633364385845756f55737661\ + \42514e62464443353569474f4135736e453144326743346f47564c472b\ + \524331385958326c6863552f36687a38432f496172773d3d" + checkPassphrase EncryptWithScrypt pwd hash `shouldBe` Right () + + it "empty password" $ do + let pwd = Passphrase @"user" "" + let hash = unsafeFromHex + "31347c387c317c5743424875746242496c6a66734d764934314a30727a\ + \79663076657375724954796376766a793150554e377452673d3d7c5475\ + \3434596d6e547957546c5759674a3164494f7974474a7842632b432f78\ + \62507657382b5135356a38303d" + checkPassphrase EncryptWithScrypt pwd hash `shouldBe` Right () + + it "cardano-wallet password" $ do + let pwd = Passphrase @"user" "cardano-wallet" + let hash = unsafeFromHex + "31347c387c317c2b6a6f747446495a6a566d586f43374c6c54425a576c\ + \597a425834515177666475467578436b4d485569733d7c78324d646738\ + \49554a3232507235676531393575445a76583646552b7757395a6a6a2f\ + \51303054356c654751794279732f7662753367526d726c316c657a7150\ + \43676d364e6758476d4d2f4b6438343265304b4945773d3d" + checkPassphrase EncryptWithScrypt pwd hash `shouldBe` Right () + {------------------------------------------------------------------------------- Properties -------------------------------------------------------------------------------} +scryptPropsSpec :: Spec +scryptPropsSpec = describe "Legacy scrypt password encryption" $ do + it "checkPassphrase p h(p) == Right ()" $ + property prop_passphraseFromScryptRoundtrip + it "p /= p' => checkPassphrase p' h(p) == Left ErrWrongPassphrase" $ + property prop_passphraseFromScryptRoundtripFail + prop_passphraseFromScryptRoundtrip :: Passphrase "user" -> Property diff --git a/lib/core/test/unit/Cardano/Wallet/Primitive/PassphraseSpec.hs b/lib/core/test/unit/Cardano/Wallet/Primitive/PassphraseSpec.hs index 476f80cb543..4556ded483d 100644 --- a/lib/core/test/unit/Cardano/Wallet/Primitive/PassphraseSpec.hs +++ b/lib/core/test/unit/Cardano/Wallet/Primitive/PassphraseSpec.hs @@ -31,6 +31,8 @@ import Cardano.Wallet.Primitive.Passphrase.Gen ) import Cardano.Wallet.Primitive.Passphrase.Legacy ( haveScrypt ) +import Cardano.Wallet.Unsafe + ( unsafeFromHex ) import Control.Monad.IO.Class ( liftIO ) import Data.ByteString @@ -51,11 +53,11 @@ import Test.Text.Roundtrip import qualified Data.ByteArray as BA spec :: Spec -spec = do - parallel $ describe "Text Roundtrip" $ do +spec = parallel $ do + describe "Text Roundtrip" $ do textRoundtrip $ Proxy @(Passphrase "user") - parallel $ describe "Passphrases" $ do + describe "Passphrases" $ do it "checkPassphrase p h(p) == Right ()" $ property prop_passphraseRoundtrip it "p /= p' => checkPassphrase p' h(p) == Left ErrWrongPassphrase" $ @@ -156,20 +158,18 @@ passphraseGolden1 :: Golden passphraseGolden1 = Golden { passphrase = Passphrase "passphrase" , prepared = Passphrase "passphrase" - , hash = PassphraseHash - "\SI\133\128\ESC#\211\232\218\ESC\134>\216\216H\235\206\206\211\134'\ - \\135\253\237\244\226\143\SYN\239!}\247\172\168\CAN\212\DC1\SI\255\235\ - \\215\241\DC4\181\133\177\232=\190\154\249\ETB\EOTd\176\149\249\216\133\ - \\141\188P\188s\159q\250\159^dj\STX\ACK$O@\208\138\236wp" + , hash = unsafeFromHex + "0f85801b23d3e8da1b863ed8d848ebceced3862787fdedf4e28f16ef217df7ac\ + \a818d4110fffebd7f114b585b1e83dbe9af9170464b095f9d8858dbc50bc739f\ + \71fa9f5e646a0206244f40d08aec7770" } passphraseGolden2 :: Golden passphraseGolden2 = Golden { passphrase = Passphrase "" , prepared = Passphrase "" - , hash = PassphraseHash - "\SI\133\128\ESC#\211\232\218\ESC\134>\216\216H\235\206\130\173|\250\ - \\215\130\227^\155\216\176\NUL\145p\190P\CAN\254\155\190\140\DLE\208\ - \\194\207)X\171p*Y\170\192eiZ\243\\\222Mr\174\av\NAK\238\183\DC2\156\ - \\203\196\156,,\245X\161\242\160\148\217k\EM\234" + , hash = unsafeFromHex + "0f85801b23d3e8da1b863ed8d848ebce82ad7cfad782e35e9bd8b0009170be50\ + \18fe9bbe8c10d0c2cf2958ab702a59aac065695af35cde4d72ae077615eeb712\ + \9ccbc49c2c2cf558a1f2a094d96b19ea" }