From fe05a591af54ccf1f301da5b9254ec09b688a6ce Mon Sep 17 00:00:00 2001 From: Pawel Jakubas Date: Wed, 3 Jul 2024 18:08:06 +0200 Subject: [PATCH 01/26] fromMetadataEncrypted - part 1 --- .../Cardano/Wallet/Api/Http/Shelley/Server.hs | 83 ++++++++++++++++++- lib/wallet/src/Cardano/Wallet.hs | 9 ++ 2 files changed, 88 insertions(+), 4 deletions(-) diff --git a/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs b/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs index 542055d8203..e3263c32374 100644 --- a/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs +++ b/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs @@ -181,6 +181,7 @@ import Cardano.Wallet , ErrConstructSharedWallet (..) , ErrConstructTx (..) , ErrCreateMigrationPlan (..) + , ErrDecodeTx (..) , ErrGetPolicyId (..) , ErrNoSuchWallet (..) , ErrReadRewardAccount (..) @@ -3014,6 +3015,15 @@ constructTransaction api knownPools poolStatus apiWalletId body = do cip20MetadataKey :: Word64 cip20MetadataKey = 674 +cip83EncryptMethodKey :: Text +cip83EncryptMethodKey = "enc" + +cip83EncryptPayloadKey :: Text +cip83EncryptPayloadKey = "msg" + +cip83EncryptPayloadValue :: Text +cip83EncryptPayloadValue = "basic" + -- When encryption is enabled we do the following: -- (a) find field `msg` in the object of "674" label -- (b) encrypt the 'msg' value if present, if there is neither "674" label @@ -3047,7 +3057,11 @@ toMetadataEncrypted apiEncrypt payload saltM = Nothing where getValue :: (TxMetadataValue, TxMetadataValue) -> Maybe TxMetadataValue - getValue (TxMetaText "msg", v) = Just v + getValue (TxMetaText k, v) = + if k == cip83EncryptPayloadKey then + Just v + else + Nothing getValue _ = Nothing validKeyAndMessage :: Word64 -> TxMetadataValue -> Bool @@ -3075,7 +3089,7 @@ toMetadataEncrypted apiEncrypt payload saltM = :: (TxMetadataValue, TxMetadataValue) -> Either ErrConstructTx [(TxMetadataValue, TxMetadataValue)] encryptPairIfQualifies = \case - (TxMetaText "msg", m) -> + (TxMetaText "msg", m) -> do bimap ErrConstructTxEncryptMetadata toPair (encryptValue m) pair -> Right [pair] @@ -3089,8 +3103,8 @@ toMetadataEncrypted apiEncrypt payload saltM = toPair :: ByteString -> [(TxMetadataValue, TxMetadataValue)] toPair encryptedMessage = - [ (TxMetaText "msg", TxMetaList (toChunks encryptedMessage)) - , (TxMetaText "enc", TxMetaText "basic") + [ (TxMetaText cip83EncryptPayloadKey, TxMetaList (toChunks encryptedMessage)) + , (TxMetaText cip83EncryptMethodKey, TxMetaText cip83EncryptPayloadValue) ] toChunks :: ByteString -> [TxMetadataValue] @@ -3105,6 +3119,67 @@ toMetadataEncrypted apiEncrypt payload saltM = where TxMetadata themap = payload ^. #txMetadataWithSchema_metadata +-- When encryption is enabled we do the following: +-- (a) retrieve list of TxMetaBytes under proper key, ie.674, +-- cip20MetadataKey +-- (b) recreate encrypted payload from chunks +-- (0, TxMetaBytes chunk1) +-- (1, TxMetaBytes chunk2) +-- .... +-- (N, TxMetaBytes chunkN) +-- ie., payload=chunk1+chunk2+...+chunkN +-- (c) decrypt payload +-- (d) decode metadata +fromMetadataEncrypted + :: ApiEncryptMetadata + -> Cardano.TxMetadata + -> Either ErrDecodeTx TxMetadataWithSchema +fromMetadataEncrypted apiEncrypt metadata = + composePayload metadata >>= + decrypt >>= + decodeFromJSON + where + checkPresenceOfMethod value = + let presentPair (Cardano.TxMetaText k, Cardano.TxMetaText v) = + k == cip83EncryptMethodKey && v == cip83EncryptPayloadValue + presentPair _ = False + in case value of + Cardano.TxMetaMap list -> null $ filter presentPair list + _ -> True + getEncryptedPayload value = + let presentPair (Cardano.TxMetaText k, Cardano.TxMetaList _) = + k == cip83EncryptPayloadKey + presentPair _ = False + in case value of + Cardano.TxMetaMap list -> snd <$> filter presentPair list + _ -> [] + extractTxt (Cardano.TxMetaText txt) = txt + extractTxt _ = + error "TxMetaText is expected" + extractPayload (Cardano.TxMetaList chunks)= + foldl T.append T.empty $ extractTxt <$> chunks + extractPayload _ = T.empty + composePayload (Cardano.TxMetadata themap) = do + validValue <- case Map.lookup cip20MetadataKey themap of + Nothing -> Left ErrDecodeTxMissingMetadataKey + Just v -> pure v + when (checkPresenceOfMethod validValue) $ + Left ErrDecodeTxMissingEncryptionMethod + let payloads = getEncryptedPayload validValue + if length payloads == 0 then + Left ErrDecodeTxMissingValidEncryptionPayload + else do + let extracted = extractPayload <$> payloads + when (any (==T.empty) extracted) $ + Left ErrDecodeTxMissingValidEncryptionPayload + Right extracted + + decrypt _payload = undefined + + decodeFromJSON = + first (ErrDecodeTxDecryptedPayload . T.pack) . + Aeson.eitherDecode . BL.fromStrict + metadataPBKDF2Config :: PBKDF2Config SHA256 metadataPBKDF2Config = PBKDF2Config { hash = SHA256 diff --git a/lib/wallet/src/Cardano/Wallet.hs b/lib/wallet/src/Cardano/Wallet.hs index 4d6c12810dc..327bc9cf4ad 100644 --- a/lib/wallet/src/Cardano/Wallet.hs +++ b/lib/wallet/src/Cardano/Wallet.hs @@ -107,6 +107,7 @@ module Cardano.Wallet , ErrReadPolicyPublicKey (..) , ErrWritePolicyPublicKey (..) , ErrGetPolicyId (..) + , ErrDecodeTx (..) , readWalletMeta , isStakeKeyRegistered , putDelegationCertificate @@ -3765,6 +3766,14 @@ data ErrGetPolicyId | ErrGetPolicyIdWrongMintingBurningTemplate deriving (Show, Eq) +-- | Errors that can occur when decoding a transaction. +data ErrDecodeTx + = ErrDecodeTxMissingMetadataKey + | ErrDecodeTxMissingEncryptionMethod + | ErrDecodeTxMissingValidEncryptionPayload + | ErrDecodeTxDecryptedPayload Text + deriving (Show, Eq) + -- | Errors that can occur when signing a transaction. data ErrWitnessTx = ErrWitnessTxSignTx ErrSignTx From a72c6b5777c7c52aa56e12db99ee62991f3f3a64 Mon Sep 17 00:00:00 2001 From: Pawel Jakubas Date: Thu, 4 Jul 2024 09:54:50 +0200 Subject: [PATCH 02/26] fromMetadataEncrypted - part 2 --- .../Cardano/Wallet/Api/Http/Shelley/Server.hs | 25 +++++++++++++------ lib/wallet/src/Cardano/Wallet.hs | 2 ++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs b/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs index e3263c32374..1149c7daf0b 100644 --- a/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs +++ b/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs @@ -3122,22 +3122,23 @@ toMetadataEncrypted apiEncrypt payload saltM = -- When encryption is enabled we do the following: -- (a) retrieve list of TxMetaBytes under proper key, ie.674, -- cip20MetadataKey --- (b) recreate encrypted payload from chunks +-- (b) recreate each encrypted payload from chunks -- (0, TxMetaBytes chunk1) -- (1, TxMetaBytes chunk2) -- .... -- (N, TxMetaBytes chunkN) -- ie., payload=chunk1+chunk2+...+chunkN --- (c) decrypt payload --- (d) decode metadata +-- (c) decrypt each payload +-- (d) update structure +-- (e) decode metadata fromMetadataEncrypted :: ApiEncryptMetadata -> Cardano.TxMetadata -> Either ErrDecodeTx TxMetadataWithSchema fromMetadataEncrypted apiEncrypt metadata = composePayload metadata >>= - decrypt >>= - decodeFromJSON + mapM decrypt >>= + adjust metadata where checkPresenceOfMethod value = let presentPair (Cardano.TxMetaText k, Cardano.TxMetaText v) = @@ -3174,11 +3175,21 @@ fromMetadataEncrypted apiEncrypt metadata = Left ErrDecodeTxMissingValidEncryptionPayload Right extracted - decrypt _payload = undefined - + pwd = BA.convert $ unPassphrase $ getApiT $ apiEncrypt ^. #passphrase decodeFromJSON = first (ErrDecodeTxDecryptedPayload . T.pack) . Aeson.eitherDecode . BL.fromStrict + decrypt payload = case AES256CBC.getSaltFromEncrypted payloadBS of + Nothing -> Left ErrDecodeTxMissingSalt + Just salt -> + let (secretKey, iv) = + PBKDF2.generateKey metadataPBKDF2Config pwd (Just salt) + in decodeFromJSON <$> + bimap ErrDecodeTxDecryptPayload fst + (AES256CBC.decrypt WithPadding secretKey iv payloadBS) + where + payloadBS = T.encodeUtf8 payload + adjust _metadata _ = undefined metadataPBKDF2Config :: PBKDF2Config SHA256 metadataPBKDF2Config = PBKDF2Config diff --git a/lib/wallet/src/Cardano/Wallet.hs b/lib/wallet/src/Cardano/Wallet.hs index 327bc9cf4ad..3051f449690 100644 --- a/lib/wallet/src/Cardano/Wallet.hs +++ b/lib/wallet/src/Cardano/Wallet.hs @@ -3772,6 +3772,8 @@ data ErrDecodeTx | ErrDecodeTxMissingEncryptionMethod | ErrDecodeTxMissingValidEncryptionPayload | ErrDecodeTxDecryptedPayload Text + | ErrDecodeTxMissingSalt + | ErrDecodeTxDecryptPayload AES256CBC.CipherError deriving (Show, Eq) -- | Errors that can occur when signing a transaction. From 060cc25689286cdf8577c07641a9126407544ac0 Mon Sep 17 00:00:00 2001 From: Pawel Jakubas Date: Fri, 5 Jul 2024 08:50:53 +0200 Subject: [PATCH 03/26] metadataValueFromJsonNoSchema --- lib/api/cardano-wallet-api.cabal | 11 ++ .../Wallet/Api/Types/SchemaMetadata.hs | 139 ++++++++++++++++++ 2 files changed, 150 insertions(+) diff --git a/lib/api/cardano-wallet-api.cabal b/lib/api/cardano-wallet-api.cabal index 1b99f38a124..a32f55e135f 100644 --- a/lib/api/cardano-wallet-api.cabal +++ b/lib/api/cardano-wallet-api.cabal @@ -38,7 +38,11 @@ library build-depends: , address-derivation-discovery , aeson + , aeson-pretty + , ansi-terminal + , attoparsec , base + , base16-bytestring , bech32 , bech32-th , binary @@ -48,13 +52,19 @@ library , cardano-balance-tx:{cardano-balance-tx, internal} , cardano-binary , cardano-crypto + , cardano-ledger-alonzo + , cardano-ledger-babbage + , cardano-ledger-binary + , cardano-ledger-conway , cardano-ledger-core + , cardano-ledger-shelley , cardano-wallet , cardano-wallet-launcher , cardano-wallet-network-layer , cardano-wallet-primitive , cardano-wallet-read , cardano-wallet-secrets + , cborg , containers , contra-tracer , crypto-primitives @@ -77,6 +87,7 @@ library , quiet , random , safe + , scientific , servant , servant-client , servant-server diff --git a/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs b/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs index 99ea85b0e46..d8d34b429eb 100644 --- a/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs +++ b/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs @@ -8,6 +8,8 @@ {-# LANGUAGE NoMonomorphismRestriction #-} {-# LANGUAGE StrictData #-} +{-# OPTIONS_GHC -Wno-orphans #-} + -- | -- Copyright: © 2018-2022 IOHK, 2023 Cardano Foundation -- License: Apache-2.0 @@ -18,8 +20,11 @@ module Cardano.Wallet.Api.Types.SchemaMetadata where import Cardano.Api ( TxMetadataJsonSchema (..) + , TxMetadataJsonSchemaError (..) + , TxMetadataValue (..) , metadataFromJson , metadataToJson + , metadataValueToJsonNoSchema ) import Cardano.Api.Error ( displayError @@ -30,6 +35,10 @@ import Cardano.Wallet.Primitive.Types.Tx import Control.Applicative ( (<|>) ) +import Control.Monad + ( guard + , when + ) import Control.DeepSeq ( NFData ) @@ -37,11 +46,39 @@ import Data.Aeson ( FromJSON (parseJSON) , ToJSON (toJSON) ) +import Data.Bifunctor + ( first + ) +import Data.ByteString + ( ByteString + ) +import Data.Maybe + ( fromMaybe + ) +import Data.Text + ( Text + ) import GHC.Generics ( Generic ) import Prelude +import qualified Cardano.Ledger.Binary as CBOR +import qualified Cardano.Ledger.Shelley.TxAuxData as Ledger +import qualified Codec.CBOR.Magic as CBOR +import qualified Data.Aeson as Aeson +import qualified Data.Aeson.Key as Aeson +import qualified Data.Aeson.KeyMap as Aeson +import qualified Data.Attoparsec.ByteString.Char8 as Atto +import qualified Data.ByteString as BS +import qualified Data.ByteString.Base16 as BS16 +import qualified Data.ByteString.Char8 as BS8 +import qualified Data.List as L +import qualified Data.Scientific as Scientific +import qualified Data.Text as T +import qualified Data.Text.Encoding as T +import qualified Data.Vector as V + -- | A tag to select the json codec data TxMetadataSchema = TxMetadataNoSchema | TxMetadataDetailedSchema deriving (Show, Eq, Generic, NFData) @@ -99,3 +136,105 @@ instance FromJSON TxMetadataWithSchema where . either (fail . displayError) pure . metadataFromJson TxMetadataJsonNoSchema ) + +instance ToJSON TxMetadataValue where + toJSON = metadataValueToJsonNoSchema + +instance FromJSON TxMetadataValue where + parseJSON = either (fail . displayError) pure . metadataValueFromJsonNoSchema + +-- when cardano-api exports metadataValueFromJsonNoSchema the below could be removed (together with cabal dependencies) +metadataValueFromJsonNoSchema + :: Aeson.Value + -> Either TxMetadataJsonSchemaError TxMetadataValue +metadataValueFromJsonNoSchema = conv + where + conv :: Aeson.Value -> Either TxMetadataJsonSchemaError TxMetadataValue + conv Aeson.Null = Left TxMetadataJsonNullNotAllowed + conv Aeson.Bool{} = Left TxMetadataJsonBoolNotAllowed + + conv (Aeson.Number d) = + case Scientific.floatingOrInteger d :: Either Double Integer of + Left n -> Left (TxMetadataJsonNumberNotInteger n) + Right n -> Right (TxMetaNumber n) + + conv (Aeson.String s) + | Just s' <- T.stripPrefix bytesPrefix s + , let bs' = T.encodeUtf8 s' + , Right bs <- BS16.decode bs' + , not (BS8.any (\c -> c >= 'A' && c <= 'F') bs') + = Right (TxMetaBytes bs) + + conv (Aeson.String s) = Right (TxMetaText s) + + conv (Aeson.Array vs) = + fmap TxMetaList + . traverse conv + $ V.toList vs + + conv (Aeson.Object kvs) = + fmap + ( TxMetaMap + . sortCanonicalForCbor + ) + . traverse (\(k,v) -> (,) (convKey k) <$> conv v) + . fmap (first Aeson.toText) + $ Aeson.toList kvs + + convKey :: Text -> TxMetadataValue + convKey s = + fromMaybe (TxMetaText s) $ + parseAll ((fmap TxMetaNumber pSigned <* Atto.endOfInput) + <|> (fmap TxMetaBytes pBytes <* Atto.endOfInput)) s + +bytesPrefix :: Text +bytesPrefix = "0x" + +parseAll :: Atto.Parser a -> Text -> Maybe a +parseAll p = + either (const Nothing) Just + . Atto.parseOnly p + . T.encodeUtf8 + +pUnsigned :: Atto.Parser Integer +pUnsigned = do + bs <- Atto.takeWhile1 Atto.isDigit + -- no redundant leading 0s allowed, or we cannot round-trip properly + guard (not (BS.length bs > 1 && BS8.head bs == '0')) + return $! BS.foldl' step 0 bs + where + step a w = a * 10 + fromIntegral (w - 48) + +pSigned :: Atto.Parser Integer +pSigned = Atto.signed pUnsigned + +pBytes :: Atto.Parser ByteString +pBytes = do + _ <- Atto.string "0x" + remaining <- Atto.takeByteString + when (BS8.any hexUpper remaining) $ fail ("Unexpected uppercase hex characters in " <> show remaining) + case BS16.decode remaining of + Right bs -> return bs + _ -> fail ("Expecting base16 encoded string, found: " <> show remaining) + where + hexUpper c = c >= 'A' && c <= 'F' + +sortCanonicalForCbor + :: [(TxMetadataValue, TxMetadataValue)] -> [(TxMetadataValue, TxMetadataValue)] +sortCanonicalForCbor = + map snd + . L.sortOn fst + . map (\e@(k, _) -> (CBOR.uintegerFromBytes $ serialiseKey k, e)) + where + serialiseKey = CBOR.serialize' CBOR.shelleyProtVer . toShelleyMetadatum + +toShelleyMetadatum :: TxMetadataValue -> Ledger.Metadatum +toShelleyMetadatum (TxMetaNumber x) = Ledger.I x +toShelleyMetadatum (TxMetaBytes x) = Ledger.B x +toShelleyMetadatum (TxMetaText x) = Ledger.S x +toShelleyMetadatum (TxMetaList xs) = + Ledger.List [ toShelleyMetadatum x | x <- xs ] +toShelleyMetadatum (TxMetaMap xs) = + Ledger.Map [ (toShelleyMetadatum k, + toShelleyMetadatum v) + | (k,v) <- xs ] From 9c740f05118a459c1ad5fe8ec74a97030b488bbe Mon Sep 17 00:00:00 2001 From: Pawel Jakubas Date: Fri, 5 Jul 2024 09:05:45 +0200 Subject: [PATCH 04/26] fromMetadataEncrypted - part 3 --- .../Cardano/Wallet/Api/Http/Shelley/Server.hs | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs b/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs index 1149c7daf0b..0459321f4ca 100644 --- a/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs +++ b/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs @@ -3134,7 +3134,7 @@ toMetadataEncrypted apiEncrypt payload saltM = fromMetadataEncrypted :: ApiEncryptMetadata -> Cardano.TxMetadata - -> Either ErrDecodeTx TxMetadataWithSchema + -> Either ErrDecodeTx Cardano.TxMetadata fromMetadataEncrypted apiEncrypt metadata = composePayload metadata >>= mapM decrypt >>= @@ -3176,20 +3176,42 @@ fromMetadataEncrypted apiEncrypt metadata = Right extracted pwd = BA.convert $ unPassphrase $ getApiT $ apiEncrypt ^. #passphrase - decodeFromJSON = + decodeFromJSON = ---use metadataValueFromJsonNoSchema first (ErrDecodeTxDecryptedPayload . T.pack) . Aeson.eitherDecode . BL.fromStrict decrypt payload = case AES256CBC.getSaltFromEncrypted payloadBS of Nothing -> Left ErrDecodeTxMissingSalt - Just salt -> + Just salt -> do let (secretKey, iv) = PBKDF2.generateKey metadataPBKDF2Config pwd (Just salt) - in decodeFromJSON <$> - bimap ErrDecodeTxDecryptPayload fst - (AES256CBC.decrypt WithPadding secretKey iv payloadBS) + decrypted <- bimap ErrDecodeTxDecryptPayload fst + (AES256CBC.decrypt WithPadding secretKey iv payloadBS) + decodeFromJSON decrypted where payloadBS = T.encodeUtf8 payload - adjust _metadata _ = undefined + adjust (Cardano.TxMetadata metadata) decodedElems = + pure $ Cardano.TxMetadata $ Map.adjust updateMetaMap cip20MetadataKey metadata + where + updateElem acc@(decryptedList, list) elem = case elem of + (Cardano.TxMetaText k, Cardano.TxMetaText v) -> + if k == cip83EncryptMethodKey && v == cip83EncryptPayloadValue then + -- omiting this element + acc + else + (decryptedList, list ++ [elem]) + (Cardano.TxMetaText k, v) -> + -- it is secure to do it as we check it already in composePayload + let (toAdd : rest) = decryptedList + in if k == cip83EncryptPayloadKey then + (rest, list ++ [(Cardano.TxMetaText k, toAdd)] ) + else + (decryptedList, list ++ [(Cardano.TxMetaText k, v)] ) + _ -> error "we have checked already in composePayload that there is pair (TxMetaText, something)" + + updateMetaMap v = case v of + Cardano.TxMetaMap list -> + Cardano.TxMetaMap $ snd $ L.foldl updateElem (decodedElems,[]) list + _ -> error "we have checked already in composePayload that there is TxMetaMap" metadataPBKDF2Config :: PBKDF2Config SHA256 metadataPBKDF2Config = PBKDF2Config From 2caf5b052b1012de7595207f97e8c38e9dd85dc5 Mon Sep 17 00:00:00 2001 From: Pawel Jakubas Date: Mon, 8 Jul 2024 07:37:27 +0200 Subject: [PATCH 05/26] integrate fromMetadataEncryption and add IsSerrverError instance for ErrDecodetx --- .../Cardano/Wallet/Api/Http/Server/Error.hs | 36 +++++++++++++++++ .../Cardano/Wallet/Api/Http/Shelley/Server.hs | 40 ++++++++++++------- lib/api/src/Cardano/Wallet/Api/Types/Error.hs | 1 + 3 files changed, 62 insertions(+), 15 deletions(-) diff --git a/lib/api/src/Cardano/Wallet/Api/Http/Server/Error.hs b/lib/api/src/Cardano/Wallet/Api/Http/Server/Error.hs index 24d16dcc8c7..3ba21544a48 100644 --- a/lib/api/src/Cardano/Wallet/Api/Http/Server/Error.hs +++ b/lib/api/src/Cardano/Wallet/Api/Http/Server/Error.hs @@ -42,6 +42,7 @@ import Cardano.Wallet , ErrConstructTx (..) , ErrCreateMigrationPlan (..) , ErrCreateRandomAddress (..) + , ErrDecodeTx (..) , ErrDerivePublicKey (..) , ErrFetchRewards (..) , ErrGetPolicyId (..) @@ -493,6 +494,41 @@ instance IsServerError ErrConstructTx where apiError err501 NotImplemented "This feature is not yet implemented." +instance IsServerError ErrDecodeTx where + toServerError = \case + ErrDecodeTxMissingMetadataKey -> + apiError err403 InvalidMetadataDecryption $ mconcat + [ "It looks like the encrypted metadata has wrong structure. " + , "It is expected to be a map with key '674' - see CIP20." + ] + ErrDecodeTxMissingEncryptionMethod -> + apiError err403 InvalidMetadataDecryption $ mconcat + [ "It looks like the encrypted metadata has wrong structure. " + , "It is expected to have encryption method under 'enc' key - see CIP83." + ] + ErrDecodeTxMissingValidEncryptionPayload -> + apiError err403 InvalidMetadataDecryption $ mconcat + [ "It looks like the encrypted metadata has wrong structure. " + , "It is expected to have encryption payload under 'msg' key - see CIP83." + ] + ErrDecodeTxDecryptedPayload err -> + apiError err403 InvalidMetadataDecryption $ mconcat + [ "It looks like the decrypted metadata cannot be decoded. " + , "The exact error is: " + , err + ] + ErrDecodeTxMissingSalt -> + apiError err403 InvalidMetadataDecryption $ mconcat + [ "It looks like the decrypted metadata can be decoded, but " + , "misses salt." + ] + ErrDecodeTxDecryptPayload cryptoError -> + apiError err403 InvalidMetadataDecryption $ mconcat + [ "It looks like the encrypted metadata cannot be decrypted. " + , "The exact error is: " + , T.pack (show cryptoError) + ] + instance IsServerError ErrGetPolicyId where toServerError = \case ErrGetPolicyIdReadPolicyPubliKey e -> toServerError e diff --git a/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs b/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs index 0459321f4ca..c9e6ecb0a79 100644 --- a/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs +++ b/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs @@ -122,6 +122,7 @@ module Cardano.Wallet.Api.Http.Shelley.Server , withWorkerCtx , getCurrentEpoch , toMetadataEncrypted + , fromMetadataEncrypted , metadataPBKDF2Config -- * Workers @@ -3189,23 +3190,24 @@ fromMetadataEncrypted apiEncrypt metadata = decodeFromJSON decrypted where payloadBS = T.encodeUtf8 payload - adjust (Cardano.TxMetadata metadata) decodedElems = - pure $ Cardano.TxMetadata $ Map.adjust updateMetaMap cip20MetadataKey metadata + adjust (Cardano.TxMetadata metadata') decodedElems = + pure $ Cardano.TxMetadata $ + Map.adjust updateMetaMap cip20MetadataKey metadata' where - updateElem acc@(decryptedList, list) elem = case elem of + updateElem acc@(decryptedList, list) elem' = case elem' of (Cardano.TxMetaText k, Cardano.TxMetaText v) -> if k == cip83EncryptMethodKey && v == cip83EncryptPayloadValue then -- omiting this element acc else - (decryptedList, list ++ [elem]) - (Cardano.TxMetaText k, v) -> - -- it is secure to do it as we check it already in composePayload - let (toAdd : rest) = decryptedList - in if k == cip83EncryptPayloadKey then - (rest, list ++ [(Cardano.TxMetaText k, toAdd)] ) - else - (decryptedList, list ++ [(Cardano.TxMetaText k, v)] ) + (decryptedList, list ++ [elem']) + (Cardano.TxMetaText k, v) -> case decryptedList of + toAdd : rest -> + if k == cip83EncryptPayloadKey then + (rest, list ++ [(Cardano.TxMetaText k, toAdd)] ) + else + (decryptedList, list ++ [(Cardano.TxMetaText k, v)] ) + _ -> error "we have checked already in composePayload that there is enough elements in decrypedList" _ -> error "we have checked already in composePayload that there is pair (TxMetaText, something)" updateMetaMap v = case v of @@ -3671,8 +3673,8 @@ decodeTransaction withWorkerCtx ctx wid liftE liftE $ \wrk -> do (k, _) <- liftHandler $ W.readPolicyPublicKey wrk let keyhash = KeyHash Policy (xpubToBytes k) - let TxExtended{..} = decodeTx tl era sealed - let Tx { txId + TxExtended{..} = decodeTx tl era sealed + Tx { txId , fee , resolvedInputs , resolvedCollateralInputs @@ -3681,7 +3683,15 @@ decodeTransaction , metadata , scriptValidity } = walletTx - let db = wrk ^. dbLayer + db = wrk ^. dbLayer + metadata' <- case (decryptMetadata, metadata) of + (Just apiDecrypt, Just meta) -> + case fromMetadataEncrypted apiDecrypt meta of + Left err -> + liftHandler $ throwE err + Right txmetadata -> + pure . Just . ApiT $ txmetadata + _ -> pure $ ApiT <$> metadata (acct, _, acctPath) <- liftHandler $ W.shelleyOnlyReadRewardAccount @s db inputPaths <- @@ -3714,7 +3724,7 @@ decodeTransaction , depositsReturned = (ApiAmount.fromCoin . W.stakeKeyDeposit $ pp) <$ filter ourRewardAccountDeregistration certs - , metadata = ApiTxMetadata $ ApiT <$> metadata + , metadata = ApiTxMetadata metadata' , scriptValidity = ApiT <$> scriptValidity , validityInterval = ApiValidityIntervalExplicit <$> validity , witnessCount = mkApiWitnessCount $ witnessCount diff --git a/lib/api/src/Cardano/Wallet/Api/Types/Error.hs b/lib/api/src/Cardano/Wallet/Api/Types/Error.hs index f3d9514fe28..c9d9a311fdc 100644 --- a/lib/api/src/Cardano/Wallet/Api/Types/Error.hs +++ b/lib/api/src/Cardano/Wallet/Api/Types/Error.hs @@ -171,6 +171,7 @@ data ApiErrorInfo | InputsDepleted | InsufficientCollateral | InvalidCoinSelection + | InvalidMetadataDecryption | InvalidMetadataEncryption | InvalidValidityBounds | InvalidWalletType From 89eae08bcc876c2dbe812bd71687071c39e60a9d Mon Sep 17 00:00:00 2001 From: Pawel Jakubas Date: Mon, 8 Jul 2024 12:03:48 +0200 Subject: [PATCH 06/26] decodeTransaction integration tests - part 1 --- .../Scenario/API/Shelley/TransactionsNew.hs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib/integration/scenarios/Test/Integration/Scenario/API/Shelley/TransactionsNew.hs b/lib/integration/scenarios/Test/Integration/Scenario/API/Shelley/TransactionsNew.hs index 315d3deffa1..06c5ed36288 100644 --- a/lib/integration/scenarios/Test/Integration/Scenario/API/Shelley/TransactionsNew.hs +++ b/lib/integration/scenarios/Test/Integration/Scenario/API/Shelley/TransactionsNew.hs @@ -72,6 +72,7 @@ import Cardano.Wallet.Api.Hex ) import Cardano.Wallet.Api.Http.Shelley.Server ( metadataPBKDF2Config + , toMetadataEncrypted ) import Cardano.Wallet.Api.Types ( AddressAmount (..) @@ -5534,6 +5535,30 @@ spec = describe "NEW_SHELLEY_TRANSACTIONS" $ do , expectResponseCode HTTP.status202 ] + let decodePayloadEncrypted = Json (toJSON signedTx) + let (Right expMetadataEncrypted) = + ApiT <$> toMetadataEncrypted encryptMetadata metadataToBeEncrypted + (Just salt) + rDecodedTxEncrypted <- request @(ApiDecodedTransaction n) ctx + (Link.decodeTransaction @'Shelley wa) Default decodePayloadEncrypted + verify rDecodedTxEncrypted + [ expectResponseCode HTTP.status202 + , expectField #metadata + (`shouldBe` (ApiTxMetadata (Just expMetadataEncrypted))) + ] +{-- + let decodePayloadDecrypted = Json [json|{ + "decrypt_metadata": #{toJSON encryptMetadata}, + "transaction": #{serialisedTxSealed signedTx} + }|] + rDecodedTxDecrypted <- request @(ApiDecodedTransaction n) ctx + (Link.decodeTransaction @'Shelley wa) Default decodePayloadDecrypted + verify rDecodedTxDecrypted + [ expectResponseCode HTTP.status202 + , expectField #metadata + (`shouldBe` (ApiTxMetadata (Just (ApiT metadataRaw)))) + ] +--} burnAssetsCheck :: MonadUnliftIO m => Context From d052244a8143b2c4322b026397e0e8602cfefe4a Mon Sep 17 00:00:00 2001 From: Pawel Jakubas Date: Tue, 9 Jul 2024 09:47:13 +0200 Subject: [PATCH 07/26] decodeTransaction integration tests - part 2 --- .../Cardano/Wallet/Api/Http/Server/Error.hs | 4 +++ .../Cardano/Wallet/Api/Http/Shelley/Server.hs | 27 ++++++++++--------- .../Scenario/API/Shelley/TransactionsNew.hs | 10 +++---- lib/wallet/src/Cardano/Wallet.hs | 1 + 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/lib/api/src/Cardano/Wallet/Api/Http/Server/Error.hs b/lib/api/src/Cardano/Wallet/Api/Http/Server/Error.hs index 3ba21544a48..7205d747c5e 100644 --- a/lib/api/src/Cardano/Wallet/Api/Http/Server/Error.hs +++ b/lib/api/src/Cardano/Wallet/Api/Http/Server/Error.hs @@ -528,6 +528,10 @@ instance IsServerError ErrDecodeTx where , "The exact error is: " , T.pack (show cryptoError) ] + ErrDecodeTxEncryptedPayloadWrongBase -> + apiError err403 InvalidMetadataDecryption $ mconcat + [ "It looks like the encrypted metadata is not represented as Base64." + ] instance IsServerError ErrGetPolicyId where toServerError = \case diff --git a/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs b/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs index c9e6ecb0a79..e863fca9b34 100644 --- a/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs +++ b/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs @@ -742,6 +742,7 @@ import Data.Bifunctor ) import Data.ByteArray.Encoding ( Base (..) + , convertFromBase , convertToBase ) import Data.ByteString @@ -3177,19 +3178,22 @@ fromMetadataEncrypted apiEncrypt metadata = Right extracted pwd = BA.convert $ unPassphrase $ getApiT $ apiEncrypt ^. #passphrase - decodeFromJSON = ---use metadataValueFromJsonNoSchema + decodeFromJSON = ---use metadataValueFromJsonNoSchema when available from cardano-api first (ErrDecodeTxDecryptedPayload . T.pack) . Aeson.eitherDecode . BL.fromStrict - decrypt payload = case AES256CBC.getSaltFromEncrypted payloadBS of - Nothing -> Left ErrDecodeTxMissingSalt - Just salt -> do - let (secretKey, iv) = - PBKDF2.generateKey metadataPBKDF2Config pwd (Just salt) - decrypted <- bimap ErrDecodeTxDecryptPayload fst - (AES256CBC.decrypt WithPadding secretKey iv payloadBS) - decodeFromJSON decrypted - where - payloadBS = T.encodeUtf8 payload + decrypt payload = case convertFromBase Base64 (T.encodeUtf8 payload) of + Right payloadBS -> + case AES256CBC.getSaltFromEncrypted payloadBS of + Nothing -> Left ErrDecodeTxMissingSalt + Just salt -> do + let (secretKey, iv) = + PBKDF2.generateKey metadataPBKDF2Config pwd (Just salt) + decrypted <- bimap ErrDecodeTxDecryptPayload fst + (AES256CBC.decrypt WithPadding secretKey iv payloadBS) + decodeFromJSON decrypted + Left _ -> + Left ErrDecodeTxEncryptedPayloadWrongBase + adjust (Cardano.TxMetadata metadata') decodedElems = pure $ Cardano.TxMetadata $ Map.adjust updateMetaMap cip20MetadataKey metadata' @@ -3668,7 +3672,6 @@ decodeTransaction decodeTransaction ctx@ApiLayer{..} (ApiT wid) postData = do let ApiDecodeTransactionPostData (ApiT sealed) decryptMetadata = postData - when (isJust decryptMetadata) $ error "not implemented" era <- liftIO $ NW.currentNodeEra netLayer withWorkerCtx ctx wid liftE liftE $ \wrk -> do (k, _) <- liftHandler $ W.readPolicyPublicKey wrk diff --git a/lib/integration/scenarios/Test/Integration/Scenario/API/Shelley/TransactionsNew.hs b/lib/integration/scenarios/Test/Integration/Scenario/API/Shelley/TransactionsNew.hs index 06c5ed36288..2482859231c 100644 --- a/lib/integration/scenarios/Test/Integration/Scenario/API/Shelley/TransactionsNew.hs +++ b/lib/integration/scenarios/Test/Integration/Scenario/API/Shelley/TransactionsNew.hs @@ -570,7 +570,7 @@ spec = describe "NEW_SHELLEY_TRANSACTIONS" $ do ] decodeErrorInfo rTx `shouldBe` InvalidMetadataEncryption - it "TRANS_NEW_CREATE_02c - \ + it "TRANS_NEW_CREATE_02d - \ \Correct metadata structure to be encrypted - short" $ \ctx -> runResourceT $ do let toBeEncrypted = TxMetaText "world" @@ -582,7 +582,7 @@ spec = describe "NEW_SHELLEY_TRANSACTIONS" $ do ] checkMetadataEncryption ctx toBeEncrypted metadataRaw - it "TRANS_NEW_CREATE_02c - \ + it "TRANS_NEW_CREATE_02e - \ \Correct metadata structure to be encrypted - long" $ \ctx -> runResourceT $ do let toBeEncrypted = @@ -600,7 +600,7 @@ spec = describe "NEW_SHELLEY_TRANSACTIONS" $ do ] checkMetadataEncryption ctx toBeEncrypted metadataRaw - it "TRANS_NEW_CREATE_02d - \ + it "TRANS_NEW_CREATE_02f - \ \Encrypt multiple metadata messages" $ \ctx -> runResourceT $ do wa <- fixtureWallet ctx @@ -5546,7 +5546,7 @@ spec = describe "NEW_SHELLEY_TRANSACTIONS" $ do , expectField #metadata (`shouldBe` (ApiTxMetadata (Just expMetadataEncrypted))) ] -{-- + let decodePayloadDecrypted = Json [json|{ "decrypt_metadata": #{toJSON encryptMetadata}, "transaction": #{serialisedTxSealed signedTx} @@ -5558,7 +5558,7 @@ spec = describe "NEW_SHELLEY_TRANSACTIONS" $ do , expectField #metadata (`shouldBe` (ApiTxMetadata (Just (ApiT metadataRaw)))) ] ---} + burnAssetsCheck :: MonadUnliftIO m => Context diff --git a/lib/wallet/src/Cardano/Wallet.hs b/lib/wallet/src/Cardano/Wallet.hs index 3051f449690..fe95c7c5dea 100644 --- a/lib/wallet/src/Cardano/Wallet.hs +++ b/lib/wallet/src/Cardano/Wallet.hs @@ -3774,6 +3774,7 @@ data ErrDecodeTx | ErrDecodeTxDecryptedPayload Text | ErrDecodeTxMissingSalt | ErrDecodeTxDecryptPayload AES256CBC.CipherError + | ErrDecodeTxEncryptedPayloadWrongBase deriving (Show, Eq) -- | Errors that can occur when signing a transaction. From 42e5e6f36d145f45bb7e9ea7a91017d8fdcf237c Mon Sep 17 00:00:00 2001 From: Pawel Jakubas Date: Tue, 9 Jul 2024 11:08:25 +0200 Subject: [PATCH 08/26] update api spec --- specifications/api/metadata-encrypt.md | 85 +++++++++++++++++++------- 1 file changed, 63 insertions(+), 22 deletions(-) diff --git a/specifications/api/metadata-encrypt.md b/specifications/api/metadata-encrypt.md index b2c28530b21..30c4315e7ea 100644 --- a/specifications/api/metadata-encrypt.md +++ b/specifications/api/metadata-encrypt.md @@ -6,11 +6,16 @@ In addition "Transactions New > Decode" HTTP endpoint is described in the contex ## Metadata encryption Encryption of metadata is optional and when chosen the metadata in transaction is to be encrypted -via AEAD scheme using ChaCha20 and Poly1305 (see [RFC 7539][ref]). PBKDF2 password stretching is used to get a 32-byte symmetric key -that is required for the adopted encryption algorithm. In detail, PBKDF2 encryption uses HMAC with the hash algorithm SHA512. +via AES256CBC according to [CIP-0020][ref1] and [CIP-0083][ref2]. +A PKCS#7 padding of payload is used before encryption as the required +input length must be a multiple of block size, ie., 16 bytes. +PBKDF2 password stretching is used to get a 32-byte symmetric key +that is required for the adopted encryption algorithm. In detail, +PBKDF2 encryption uses HMAC with the hash algorithm SHA512. As a consequence the encrypted metadata, not its raw version, is going to be stored in blockchain. - [ref]: https://datatracker.ietf.org/doc/html/rfc7539 + [ref1]: https://github.com/cardano-foundation/CIPs/tree/master/CIP-0020 + [ref2]: https://github.com/cardano-foundation/CIPs/tree/master/CIP-0083 The "Transactions New > Construct" HTTP endpoint allows the encryption of metadata. The "Transactions New > Decode" HTTP endpoint allows for decrypting of the encrypted metadata. @@ -29,7 +34,11 @@ Specifically: "encrypt_metadata": { "passphrase": "my secret encryption password" }, - "metadata": "raw metadata" + "metadata": + { "674" : { + "msg": "raw metadata ... " + } + } ... } ``` @@ -38,13 +47,22 @@ Specifically: ``` { ... - "metadata": "metadata encrypted" + "metadata": + { "674": + { + "enc": "basic", + "msg": + [ + "base64-string 1", "base64-string 2", "base64-string 3" ... + ] + } + } ... } ``` The same is the case for `GET` transaction. `encrypt_metadata` is an object as we might want to introduce optional choice of encryption method in the future. In that case the new enhancement to api will be introduced in - nonintrusive way. + non-intrusive way. Metadata encryption can be used for shared wallet style when calling `/shared-wallets/{walletId}/transactions-construct` endpoint with the same `POST` payload. @@ -55,15 +73,25 @@ Specifically: "encrypt_metadata": { "passphrase": "metadata-secret" }, - "metadata": {"1":"hello"} + "metadata": + { "674" : { + "msg":"world" + } + } + ... } ``` - will return + will return (for salt "yoDCYXKaVhA=") ``` { ... - "metadata": {"0":"0x0aa4f9a016215f71ef007b60601708dec0d10b4ade6071b387295f95b4"} + "metadata": + { "674" : { + "enc": "basic", + "msg": [ "U2FsdGVkX1/KgMJhcppWEG6t0aUcMqdEJmnSHVOCgpw=" ] + } + } ... } ``` @@ -75,24 +103,33 @@ Specifically: "encrypt_metadata": { "passphrase": "metadata-secret" }, - "metadata": - { "1": "Hard times create strong men." - , "2": "Strong men create good times." - , "3": "Good times create weak men." - , "4": "And, weak men create hard times." - } + "metadata": + { "674" : { + "msg": + [ "Hard times create strong men." + , "Strong men create good times." + , "Good times create weak men." + , "And, weak men create hard times." + ] + } + } ... } ``` - will return + will return (for salt "XG1cgIw56q8=") ``` { ... - "metadata": - { "0": "0x0aa4f9a016217f75f10834367493f6d7e74197417ca25c7615cae02bc345382906fb6990daf8f138b2d9192e057d0d0b555f9d5fb287abb1842928c90f26e597" - , "1": "0x559ee85f00f1588b3ee32e81dc4c84aee208a10c1eec97fffe6e0e66c69d4e0b1e3e22d7edc1618df3b20b484527d86bc3bebad4295a2ad888d034b5fec38077" - , "2": "0x8d42154f681230124c64630ea68b841aec22f0530ec830cb662d59ef423ef23d7ff3" - } + { "674" : { + "enc": "basic", + "msg": + [ "U2FsdGVkX19cbVyAjDnqr5eksQ9gnxJDz6dWhAaXvZGQl31HdEtTpBa91osBavdQ" + , "xvOJpGuA8vQGJUgn9RVuqFbVxpggHGCspU6Z5BV5j1LlSqnp6GfHFvrTL3sZcZMq" + , "MtOMZSx+d6nPRJL6453wC3rh0cny6SnrEUt9awwxx4PDZk7pDT85h3ygQf1I8fow" + , "tYtj3GY0cBwIHfkRLrsxbg==" + ] + } + } ... } ``` @@ -116,7 +153,11 @@ Specifically: ``` { ... - "metadata": "raw metadata" + "metadata": + { "674" : { + "msg": "raw metadata ... " + } + } ... } ``` From cec44c8658f8d89994ef81ed81034120f30951de Mon Sep 17 00:00:00 2001 From: Pawel Jakubas Date: Tue, 9 Jul 2024 11:15:33 +0200 Subject: [PATCH 09/26] fixes and adding missing bits to swagger --- .../Cardano/Wallet/Api/Http/Shelley/Server.hs | 6 +++--- .../Wallet/Api/Types/SchemaMetadata.hs | 20 +++++++++---------- specifications/api/swagger.yaml | 20 +++++++++++++++++++ 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs b/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs index e863fca9b34..58dda86e355 100644 --- a/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs +++ b/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs @@ -3147,7 +3147,7 @@ fromMetadataEncrypted apiEncrypt metadata = k == cip83EncryptMethodKey && v == cip83EncryptPayloadValue presentPair _ = False in case value of - Cardano.TxMetaMap list -> null $ filter presentPair list + Cardano.TxMetaMap list -> not (any presentPair list) _ -> True getEncryptedPayload value = let presentPair (Cardano.TxMetaText k, Cardano.TxMetaList _) = @@ -3169,11 +3169,11 @@ fromMetadataEncrypted apiEncrypt metadata = when (checkPresenceOfMethod validValue) $ Left ErrDecodeTxMissingEncryptionMethod let payloads = getEncryptedPayload validValue - if length payloads == 0 then + if null payloads then Left ErrDecodeTxMissingValidEncryptionPayload else do let extracted = extractPayload <$> payloads - when (any (==T.empty) extracted) $ + when (elem T.empty extracted) $ Left ErrDecodeTxMissingValidEncryptionPayload Right extracted diff --git a/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs b/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs index d8d34b429eb..5ae14ba9b9d 100644 --- a/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs +++ b/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs @@ -35,13 +35,13 @@ import Cardano.Wallet.Primitive.Types.Tx import Control.Applicative ( (<|>) ) +import Control.DeepSeq + ( NFData + ) import Control.Monad ( guard , when ) -import Control.DeepSeq - ( NFData - ) import Data.Aeson ( FromJSON (parseJSON) , ToJSON (toJSON) @@ -71,8 +71,8 @@ import qualified Data.Aeson.Key as Aeson import qualified Data.Aeson.KeyMap as Aeson import qualified Data.Attoparsec.ByteString.Char8 as Atto import qualified Data.ByteString as BS -import qualified Data.ByteString.Base16 as BS16 -import qualified Data.ByteString.Char8 as BS8 +import qualified Data.ByteString.Base16 as B16 +import qualified Data.ByteString.Char8 as B8 import qualified Data.List as L import qualified Data.Scientific as Scientific import qualified Data.Text as T @@ -161,8 +161,8 @@ metadataValueFromJsonNoSchema = conv conv (Aeson.String s) | Just s' <- T.stripPrefix bytesPrefix s , let bs' = T.encodeUtf8 s' - , Right bs <- BS16.decode bs' - , not (BS8.any (\c -> c >= 'A' && c <= 'F') bs') + , Right bs <- B16.decode bs' + , not (B8.any (\c -> c >= 'A' && c <= 'F') bs') = Right (TxMetaBytes bs) conv (Aeson.String s) = Right (TxMetaText s) @@ -200,7 +200,7 @@ pUnsigned :: Atto.Parser Integer pUnsigned = do bs <- Atto.takeWhile1 Atto.isDigit -- no redundant leading 0s allowed, or we cannot round-trip properly - guard (not (BS.length bs > 1 && BS8.head bs == '0')) + guard (not (BS.length bs > 1 && B8.head bs == '0')) return $! BS.foldl' step 0 bs where step a w = a * 10 + fromIntegral (w - 48) @@ -212,8 +212,8 @@ pBytes :: Atto.Parser ByteString pBytes = do _ <- Atto.string "0x" remaining <- Atto.takeByteString - when (BS8.any hexUpper remaining) $ fail ("Unexpected uppercase hex characters in " <> show remaining) - case BS16.decode remaining of + when (B8.any hexUpper remaining) $ fail ("Unexpected uppercase hex characters in " <> show remaining) + case B16.decode remaining of Right bs -> return bs _ -> fail ("Expecting base16 encoded string, found: " <> show remaining) where diff --git a/specifications/api/swagger.yaml b/specifications/api/swagger.yaml index d5d2df2383b..b6e6aaad64c 100644 --- a/specifications/api/swagger.yaml +++ b/specifications/api/swagger.yaml @@ -4766,6 +4766,19 @@ x-errInvalidMetadataEncryption: &errInvalidMetadataEncryption type: string enum: ['invalid_metadata_encryption'] +x-errInvalidMetadataDecryption: &errInvalidMetadataDecryption + <<: *responsesErr + title: invalid_metadata_decryption + properties: + message: + type: string + description: | + The supplied encrypted metadata object is not compatible with standard + specified by CIP-83 (https://cips.cardano.org/cip/CIP-83). + code: + type: string + enum: ['invalid_metadata_decryption'] + x-errInputsDepleted: &errInputsDepleted <<: *responsesErr title: inputs_depleted @@ -6241,6 +6254,13 @@ x-responsesDecodedTransaction: &responsesDecodedTransaction <<: *responsesErr404WalletNotInitialized <<: *responsesErr406 <<: *responsesErr415UnsupportedMediaType + 403: + description: Forbidden + content: + application/json: + schema: + oneOf: + - <<: *errInvalidMetadataDecryption 202: description: Accepted content: From 4f2fddfad4c522e1466377f7376ecb1603fe5bf9 Mon Sep 17 00:00:00 2001 From: Pawel Jakubas Date: Tue, 9 Jul 2024 11:26:49 +0200 Subject: [PATCH 10/26] more fixes --- lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs b/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs index 58dda86e355..a43d942c617 100644 --- a/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs +++ b/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs @@ -3173,7 +3173,7 @@ fromMetadataEncrypted apiEncrypt metadata = Left ErrDecodeTxMissingValidEncryptionPayload else do let extracted = extractPayload <$> payloads - when (elem T.empty extracted) $ + when (T.empty `elem` extracted) $ Left ErrDecodeTxMissingValidEncryptionPayload Right extracted From 2fc041c38412817ec8150685e9b34eae74bc9290 Mon Sep 17 00:00:00 2001 From: Pawel Jakubas Date: Wed, 17 Jul 2024 12:44:09 +0200 Subject: [PATCH 11/26] reformat metadata-encrypt.md more --- specifications/api/metadata-encrypt.md | 68 +++++++++++++------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/specifications/api/metadata-encrypt.md b/specifications/api/metadata-encrypt.md index 30c4315e7ea..18056d54c9c 100644 --- a/specifications/api/metadata-encrypt.md +++ b/specifications/api/metadata-encrypt.md @@ -6,16 +6,19 @@ In addition "Transactions New > Decode" HTTP endpoint is described in the contex ## Metadata encryption Encryption of metadata is optional and when chosen the metadata in transaction is to be encrypted -via AES256CBC according to [CIP-0020][ref1] and [CIP-0083][ref2]. +via AES256CBC according to [CIP-0020][cip0020] and [CIP-0083][cip0083]. A PKCS#7 padding of payload is used before encryption as the required input length must be a multiple of block size, ie., 16 bytes. PBKDF2 password stretching is used to get a 32-byte symmetric key that is required for the adopted encryption algorithm. In detail, PBKDF2 encryption uses HMAC with the hash algorithm SHA512. + As a consequence the encrypted metadata, not its raw version, is going to be stored in blockchain. - [ref1]: https://github.com/cardano-foundation/CIPs/tree/master/CIP-0020 - [ref2]: https://github.com/cardano-foundation/CIPs/tree/master/CIP-0083 +However, in line with [CIP-0020][cip0020] and [CIP-0083][cip0083], only the field `674` of the `metadata` field of the transaction will be affected. + + [cip0020]: https://github.com/cardano-foundation/CIPs/tree/master/CIP-0020 + [cip0083]: https://github.com/cardano-foundation/CIPs/tree/master/CIP-0083 The "Transactions New > Construct" HTTP endpoint allows the encryption of metadata. The "Transactions New > Decode" HTTP endpoint allows for decrypting of the encrypted metadata. @@ -24,9 +27,9 @@ Specifically: 1. Creation of a transaction output that contains a metadata with encryption enabled. - In the `encrypt_metadata` field, passphrase used in encryption is established. `metadata` field to be encrypted is required. +In the `encrypt_metadata` field, passphrase used in encryption is established. `metadata` field to be encrypted is required. - Example `POST` data for the endpoint, ie., /wallets/{walletId}/transactions-construct`: +Example `POST` data for the endpoint, ie., /wallets/{walletId}/transactions-construct`: ``` { @@ -34,7 +37,7 @@ Specifically: "encrypt_metadata": { "passphrase": "my secret encryption password" }, - "metadata": + "metadata": { "674" : { "msg": "raw metadata ... " } @@ -43,50 +46,49 @@ Specifically: } ``` - As a result we get transaction with metadata encrypted: +As a result we get transaction with metadata encrypted: ``` { ... - "metadata": + "metadata": { "674": - { + { "enc": "basic", - "msg": - [ + "msg": + [ "base64-string 1", "base64-string 2", "base64-string 3" ... ] - } + } } ... } ``` - The same is the case for `GET` transaction. `encrypt_metadata` is an object as we might want to introduce - optional choice of encryption method in the future. In that case the new enhancement to api will be introduced in - non-intrusive way. +The same is the case for `GET` transaction. `encrypt_metadata` is an object as we might want to introduce +optional choice of encryption method in the future. In that case the new enhancement to api will be introduced in +non-intrusive way. - Metadata encryption can be used for shared wallet style when calling `/shared-wallets/{walletId}/transactions-construct` endpoint with the same `POST` payload. +Metadata encryption can be used for shared wallet style when calling `/shared-wallets/{walletId}/transactions-construct` endpoint with the same `POST` payload. - Example: +Example: ``` { ... "encrypt_metadata": { "passphrase": "metadata-secret" }, - "metadata": + "metadata": { "674" : { "msg":"world" } } - ... } ``` - will return (for salt "yoDCYXKaVhA=") +will return (for the example salt "yoDCYXKaVhA=") ``` { ... - "metadata": + "metadata": { "674" : { "enc": "basic", "msg": [ "U2FsdGVkX1/KgMJhcppWEG6t0aUcMqdEJmnSHVOCgpw=" ] @@ -96,44 +98,44 @@ Specifically: } ``` - Example: +Example: ``` { ... "encrypt_metadata": { "passphrase": "metadata-secret" }, - "metadata": + "metadata": { "674" : { "msg": [ "Hard times create strong men." , "Strong men create good times." , "Good times create weak men." , "And, weak men create hard times." - ] + ] } - } + } ... } ``` - will return (for salt "XG1cgIw56q8=") +will return (for the example salt "XG1cgIw56q8=") ``` { ... { "674" : { - "enc": "basic", + "enc": "basic", "msg": [ "U2FsdGVkX19cbVyAjDnqr5eksQ9gnxJDz6dWhAaXvZGQl31HdEtTpBa91osBavdQ" , "xvOJpGuA8vQGJUgn9RVuqFbVxpggHGCspU6Z5BV5j1LlSqnp6GfHFvrTL3sZcZMq" , "MtOMZSx+d6nPRJL6453wC3rh0cny6SnrEUt9awwxx4PDZk7pDT85h3ygQf1I8fow" , "tYtj3GY0cBwIHfkRLrsxbg==" - ] + ] } - } + } ... } ``` - as metadata values have 64-byte limit. In that case the encrypted metadata is encoded in the successive bytes. +as metadata values have 64-byte limit. In that case the encrypted metadata is encoded in the successive bytes. ## Metadata decryption @@ -149,11 +151,11 @@ Specifically: } ``` - As a result we get decoded transaction with metadata decrypted: +As a result we get decoded transaction with metadata decrypted: ``` { ... - "metadata": + "metadata": { "674" : { "msg": "raw metadata ... " } @@ -162,4 +164,4 @@ Specifically: } ``` - Metadata decryption can be used for shared wallet style when calling `/shared-wallets/{walletId}/transactions-decode` endpoint with the same `POST` payload. +Metadata decryption can be used for shared wallet style when calling `/shared-wallets/{walletId}/transactions-decode` endpoint with the same `POST` payload. From 1b34e7daa3f205706441cf726ded1a6e6156b266 Mon Sep 17 00:00:00 2001 From: Pawel Jakubas Date: Wed, 17 Jul 2024 16:25:15 +0200 Subject: [PATCH 12/26] relocate metadata encryption - part 1 --- .../Cardano/Wallet/Api/Http/Shelley/Server.hs | 18 ++---------------- .../Cardano/Wallet/Api/Types/SchemaMetadata.hs | 15 +++++++++++++++ .../Scenario/API/Shelley/TransactionsNew.hs | 4 ++-- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs b/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs index a43d942c617..8b630baa192 100644 --- a/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs +++ b/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs @@ -123,7 +123,6 @@ module Cardano.Wallet.Api.Http.Shelley.Server , getCurrentEpoch , toMetadataEncrypted , fromMetadataEncrypted - , metadataPBKDF2Config -- * Workers , manageRewardBalance @@ -482,6 +481,7 @@ import Cardano.Wallet.Api.Types.MintBurn import Cardano.Wallet.Api.Types.SchemaMetadata ( TxMetadataSchema (..) , TxMetadataWithSchema (TxMetadataWithSchema) + , metadataPBKDF2Config ) import Cardano.Wallet.Api.Types.Transaction ( ApiAddress (..) @@ -730,12 +730,6 @@ import Cryptography.Cipher.AES256CBC import Cryptography.Core ( genSalt ) -import Cryptography.Hash.Core - ( SHA256 (..) - ) -import Cryptography.KDF.PBKDF2 - ( PBKDF2Config (..) - ) import Data.Bifunctor ( bimap , first @@ -3121,7 +3115,7 @@ toMetadataEncrypted apiEncrypt payload saltM = where TxMetadata themap = payload ^. #txMetadataWithSchema_metadata --- When encryption is enabled we do the following: +-- When decryption is enabled we do the following: -- (a) retrieve list of TxMetaBytes under proper key, ie.674, -- cip20MetadataKey -- (b) recreate each encrypted payload from chunks @@ -3219,14 +3213,6 @@ fromMetadataEncrypted apiEncrypt metadata = Cardano.TxMetaMap $ snd $ L.foldl updateElem (decodedElems,[]) list _ -> error "we have checked already in composePayload that there is TxMetaMap" -metadataPBKDF2Config :: PBKDF2Config SHA256 -metadataPBKDF2Config = PBKDF2Config - { hash = SHA256 - , iterations = 10000 - , keyLength = 32 - , ivLength = 16 - } - toUsignedTxWdrl :: c -> ApiWithdrawalGeneral n -> Maybe (RewardAccount, Coin, c) toUsignedTxWdrl p = \case diff --git a/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs b/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs index 5ae14ba9b9d..fce7ddf3e13 100644 --- a/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs +++ b/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs @@ -42,6 +42,12 @@ import Control.Monad ( guard , when ) +import Cryptography.Hash.Core + ( SHA256 (..) + ) +import Cryptography.KDF.PBKDF2 + ( PBKDF2Config (..) + ) import Data.Aeson ( FromJSON (parseJSON) , ToJSON (toJSON) @@ -238,3 +244,12 @@ toShelleyMetadatum (TxMetaMap xs) = Ledger.Map [ (toShelleyMetadatum k, toShelleyMetadatum v) | (k,v) <- xs ] + +-- Metadata encryption/decryption +metadataPBKDF2Config :: PBKDF2Config SHA256 +metadataPBKDF2Config = PBKDF2Config + { hash = SHA256 + , iterations = 10000 + , keyLength = 32 + , ivLength = 16 + } diff --git a/lib/integration/scenarios/Test/Integration/Scenario/API/Shelley/TransactionsNew.hs b/lib/integration/scenarios/Test/Integration/Scenario/API/Shelley/TransactionsNew.hs index 2482859231c..030a2112ab6 100644 --- a/lib/integration/scenarios/Test/Integration/Scenario/API/Shelley/TransactionsNew.hs +++ b/lib/integration/scenarios/Test/Integration/Scenario/API/Shelley/TransactionsNew.hs @@ -71,8 +71,7 @@ import Cardano.Wallet.Api.Hex ( fromHexText ) import Cardano.Wallet.Api.Http.Shelley.Server - ( metadataPBKDF2Config - , toMetadataEncrypted + ( toMetadataEncrypted ) import Cardano.Wallet.Api.Types ( AddressAmount (..) @@ -127,6 +126,7 @@ import Cardano.Wallet.Api.Types.Error import Cardano.Wallet.Api.Types.SchemaMetadata ( TxMetadataSchema (..) , TxMetadataWithSchema (..) + , metadataPBKDF2Config ) import Cardano.Wallet.Api.Types.Transaction ( ApiAddress (..) From 24587a269f69a2fdfdf255da3eda332539b430ad Mon Sep 17 00:00:00 2001 From: Pawel Jakubas Date: Wed, 17 Jul 2024 16:33:14 +0200 Subject: [PATCH 13/26] relocate metadata encryption - part 2 --- .../Cardano/Wallet/Api/Http/Shelley/Server.hs | 23 ++++--------------- .../Wallet/Api/Types/SchemaMetadata.hs | 22 ++++++++++++++++++ 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs b/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs index 8b630baa192..3f24242faa0 100644 --- a/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs +++ b/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs @@ -482,6 +482,10 @@ import Cardano.Wallet.Api.Types.SchemaMetadata ( TxMetadataSchema (..) , TxMetadataWithSchema (TxMetadataWithSchema) , metadataPBKDF2Config + , cip20MetadataKey + , cip83EncryptMethodKey + , cip83EncryptPayloadKey + , cip83EncryptPayloadValue ) import Cardano.Wallet.Api.Types.Transaction ( ApiAddress (..) @@ -3001,25 +3005,6 @@ constructTransaction api knownPools poolStatus apiWalletId body = do . Map.toList . foldr (uncurry (Map.insertWith (<>))) Map.empty --- A key that identifies transaction metadata, defined in CIP-20 and used by --- CIP-83. --- --- See: --- https://github.com/cardano-foundation/CIPs/tree/master/CIP-0020 --- https://github.com/cardano-foundation/CIPs/tree/master/CIP-0083 --- -cip20MetadataKey :: Word64 -cip20MetadataKey = 674 - -cip83EncryptMethodKey :: Text -cip83EncryptMethodKey = "enc" - -cip83EncryptPayloadKey :: Text -cip83EncryptPayloadKey = "msg" - -cip83EncryptPayloadValue :: Text -cip83EncryptPayloadValue = "basic" - -- When encryption is enabled we do the following: -- (a) find field `msg` in the object of "674" label -- (b) encrypt the 'msg' value if present, if there is neither "674" label diff --git a/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs b/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs index fce7ddf3e13..517818afa3b 100644 --- a/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs +++ b/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs @@ -64,6 +64,9 @@ import Data.Maybe import Data.Text ( Text ) +import Data.Word + ( Word64 + ) import GHC.Generics ( Generic ) @@ -253,3 +256,22 @@ metadataPBKDF2Config = PBKDF2Config , keyLength = 32 , ivLength = 16 } + +-- A key that identifies transaction metadata, defined in CIP-20 and used by +-- CIP-83. +-- +-- See: +-- https://github.com/cardano-foundation/CIPs/tree/master/CIP-0020 +-- https://github.com/cardano-foundation/CIPs/tree/master/CIP-0083 +-- +cip20MetadataKey :: Word64 +cip20MetadataKey = 674 + +cip83EncryptMethodKey :: Text +cip83EncryptMethodKey = "enc" + +cip83EncryptPayloadKey :: Text +cip83EncryptPayloadKey = "msg" + +cip83EncryptPayloadValue :: Text +cip83EncryptPayloadValue = "basic" From 80cb9b1524268f92c3573da96881c88c3efebd67 Mon Sep 17 00:00:00 2001 From: Pawel Jakubas Date: Wed, 17 Jul 2024 17:04:02 +0200 Subject: [PATCH 14/26] relocate metadata encryption - part 3 --- .../Cardano/Wallet/Api/Http/Shelley/Server.hs | 108 +-------------- .../Wallet/Api/Types/SchemaMetadata.hs | 123 +++++++++++++++++- .../Scenario/API/Shelley/TransactionsNew.hs | 7 +- .../test/unit/Cardano/Wallet/Api/TypesSpec.hs | 68 +++------- 4 files changed, 145 insertions(+), 161 deletions(-) diff --git a/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs b/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs index 3f24242faa0..cebb022ed4e 100644 --- a/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs +++ b/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs @@ -121,7 +121,6 @@ module Cardano.Wallet.Api.Http.Shelley.Server , rndStateChange , withWorkerCtx , getCurrentEpoch - , toMetadataEncrypted , fromMetadataEncrypted -- * Workers @@ -154,8 +153,6 @@ import Cardano.Address.Script import Cardano.Api ( NetworkId , SerialiseAsCBOR (..) - , TxMetadata (TxMetadata) - , TxMetadataValue (TxMetaList, TxMetaMap, TxMetaText) , toNetworkMagic , unNetworkMagic ) @@ -486,6 +483,7 @@ import Cardano.Wallet.Api.Types.SchemaMetadata , cip83EncryptMethodKey , cip83EncryptPayloadKey , cip83EncryptPayloadValue + , toMetadataEncrypted ) import Cardano.Wallet.Api.Types.Transaction ( ApiAddress (..) @@ -728,8 +726,7 @@ import Control.Tracer , contramap ) import Cryptography.Cipher.AES256CBC - ( CipherError - , CipherMode (..) + ( CipherMode (..) ) import Cryptography.Core ( genSalt @@ -741,7 +738,6 @@ import Data.Bifunctor import Data.ByteArray.Encoding ( Base (..) , convertFromBase - , convertToBase ) import Data.ByteString ( ByteString @@ -823,7 +819,6 @@ import Data.Traversable ) import Data.Word ( Word32 - , Word64 ) import Fmt ( pretty @@ -2608,7 +2603,10 @@ constructTransaction api knownPools poolStatus apiWalletId body = do metadata <- case (body ^. #encryptMetadata, body ^. #metadata) of (Just apiEncrypt, Just metadataWithSchema) -> do salt <- liftIO $ genSalt 8 - toMetadataEncrypted apiEncrypt metadataWithSchema (Just salt) + let pwd :: ByteString + pwd = BA.convert $ unPassphrase $ getApiT $ + apiEncrypt ^. #passphrase + toMetadataEncrypted pwd metadataWithSchema (Just salt) & \case Left err -> liftHandler $ throwE err @@ -3005,100 +3003,6 @@ constructTransaction api knownPools poolStatus apiWalletId body = do . Map.toList . foldr (uncurry (Map.insertWith (<>))) Map.empty --- When encryption is enabled we do the following: --- (a) find field `msg` in the object of "674" label --- (b) encrypt the 'msg' value if present, if there is neither "674" label --- nor 'msg' value inside object of it emit error --- (c) update value of `msg` with the encrypted initial value(s) encoded in --- base64: --- [TxMetaText base64_1, TxMetaText base64_2, ..., TxMetaText base64_n] --- (d) add `enc` field with encryption method value 'basic' -toMetadataEncrypted - :: ApiEncryptMetadata - -> TxMetadataWithSchema - -> Maybe ByteString - -> Either ErrConstructTx TxMetadata -toMetadataEncrypted apiEncrypt payload saltM = - fmap updateTxMetadata . encryptMessage =<< extractMessage - where - pwd :: ByteString - pwd = BA.convert $ unPassphrase $ getApiT $ apiEncrypt ^. #passphrase - - secretKey, iv :: ByteString - (secretKey, iv) = PBKDF2.generateKey metadataPBKDF2Config pwd saltM - - -- `msg` is embedded at the first level - parseMessage :: TxMetadataValue -> Maybe [TxMetadataValue] - parseMessage = \case - TxMetaMap kvs -> - case mapMaybe getValue kvs of - [ ] -> Nothing - vs -> Just vs - _ -> - Nothing - where - getValue :: (TxMetadataValue, TxMetadataValue) -> Maybe TxMetadataValue - getValue (TxMetaText k, v) = - if k == cip83EncryptPayloadKey then - Just v - else - Nothing - getValue _ = Nothing - - validKeyAndMessage :: Word64 -> TxMetadataValue -> Bool - validKeyAndMessage k v = k == cip20MetadataKey && isJust (parseMessage v) - - extractMessage :: Either ErrConstructTx TxMetadataValue - extractMessage - | [v] <- F.toList filteredMap = - Right v - | otherwise = - Left ErrConstructTxIncorrectRawMetadata - where - TxMetadata themap = payload ^. #txMetadataWithSchema_metadata - filteredMap = Map.filterWithKey validKeyAndMessage themap - - encryptMessage :: TxMetadataValue -> Either ErrConstructTx TxMetadataValue - encryptMessage = \case - TxMetaMap pairs -> - TxMetaMap . reverse . L.nub . reverse . concat <$> - mapM encryptPairIfQualifies pairs - _ -> - error "encryptMessage should have TxMetaMap value" - where - encryptPairIfQualifies - :: (TxMetadataValue, TxMetadataValue) - -> Either ErrConstructTx [(TxMetadataValue, TxMetadataValue)] - encryptPairIfQualifies = \case - (TxMetaText "msg", m) -> do - bimap ErrConstructTxEncryptMetadata toPair (encryptValue m) - pair -> - Right [pair] - - encryptValue :: TxMetadataValue -> Either CipherError ByteString - encryptValue - = AES256CBC.encrypt WithPadding secretKey iv saltM - . BL.toStrict - . Aeson.encode - . Cardano.metadataValueToJsonNoSchema - - toPair :: ByteString -> [(TxMetadataValue, TxMetadataValue)] - toPair encryptedMessage = - [ (TxMetaText cip83EncryptPayloadKey, TxMetaList (toChunks encryptedMessage)) - , (TxMetaText cip83EncryptMethodKey, TxMetaText cip83EncryptPayloadValue) - ] - - toChunks :: ByteString -> [TxMetadataValue] - toChunks - = fmap TxMetaText - . T.chunksOf 64 - . T.decodeUtf8 - . convertToBase Base64 - - updateTxMetadata :: TxMetadataValue -> W.TxMetadata - updateTxMetadata v = TxMetadata (Map.insert cip20MetadataKey v themap) - where - TxMetadata themap = payload ^. #txMetadataWithSchema_metadata -- When decryption is enabled we do the following: -- (a) retrieve list of TxMetaBytes under proper key, ie.674, diff --git a/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs b/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs index 517818afa3b..c8087648086 100644 --- a/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs +++ b/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs @@ -6,6 +6,7 @@ {-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NoMonomorphismRestriction #-} +{-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE StrictData #-} {-# OPTIONS_GHC -Wno-orphans #-} @@ -29,8 +30,11 @@ import Cardano.Api import Cardano.Api.Error ( displayError ) +import Cardano.Wallet + ( ErrConstructTx (..) + ) import Cardano.Wallet.Primitive.Types.Tx - ( TxMetadata + ( TxMetadata (..) ) import Control.Applicative ( (<|>) @@ -42,6 +46,10 @@ import Control.Monad ( guard , when ) +import Cryptography.Cipher.AES256CBC + ( CipherError + , CipherMode (..) + ) import Cryptography.Hash.Core ( SHA256 (..) ) @@ -53,14 +61,26 @@ import Data.Aeson , ToJSON (toJSON) ) import Data.Bifunctor - ( first + ( bimap + , first + ) +import Data.ByteArray.Encoding + ( Base (..) + , convertToBase ) import Data.ByteString ( ByteString ) import Data.Maybe - ( fromMaybe + ( isJust + , fromMaybe + , mapMaybe ) +import Data.Generics.Internal.VL.Lens + ( (^.) + ) +import Data.Generics.Labels + () import Data.Text ( Text ) @@ -75,6 +95,8 @@ import Prelude import qualified Cardano.Ledger.Binary as CBOR import qualified Cardano.Ledger.Shelley.TxAuxData as Ledger import qualified Codec.CBOR.Magic as CBOR +import qualified Cryptography.Cipher.AES256CBC as AES256CBC +import qualified Cryptography.KDF.PBKDF2 as PBKDF2 import qualified Data.Aeson as Aeson import qualified Data.Aeson.Key as Aeson import qualified Data.Aeson.KeyMap as Aeson @@ -82,6 +104,9 @@ import qualified Data.Attoparsec.ByteString.Char8 as Atto import qualified Data.ByteString as BS import qualified Data.ByteString.Base16 as B16 import qualified Data.ByteString.Char8 as B8 +import qualified Data.ByteString.Lazy as BL +import qualified Data.Foldable as F +import qualified Data.Map.Strict as Map import qualified Data.List as L import qualified Data.Scientific as Scientific import qualified Data.Text as T @@ -275,3 +300,95 @@ cip83EncryptPayloadKey = "msg" cip83EncryptPayloadValue :: Text cip83EncryptPayloadValue = "basic" + +-- When encryption is enabled we do the following: +-- (a) find field `msg` in the object of "674" label +-- (b) encrypt the 'msg' value if present, if there is neither "674" label +-- nor 'msg' value inside object of it emit error +-- (c) update value of `msg` with the encrypted initial value(s) encoded in +-- base64: +-- [TxMetaText base64_1, TxMetaText base64_2, ..., TxMetaText base64_n] +-- (d) add `enc` field with encryption method value 'basic' +toMetadataEncrypted + :: ByteString + -> TxMetadataWithSchema + -> Maybe ByteString + -> Either ErrConstructTx TxMetadata +toMetadataEncrypted pwd payload saltM = + fmap updateTxMetadata . encryptMessage =<< extractMessage + where + secretKey, iv :: ByteString + (secretKey, iv) = PBKDF2.generateKey metadataPBKDF2Config pwd saltM + + -- `msg` is embedded at the first level + parseMessage :: TxMetadataValue -> Maybe [TxMetadataValue] + parseMessage = \case + TxMetaMap kvs -> + case mapMaybe getValue kvs of + [ ] -> Nothing + vs -> Just vs + _ -> + Nothing + where + getValue :: (TxMetadataValue, TxMetadataValue) -> Maybe TxMetadataValue + getValue (TxMetaText k, v) = + if k == cip83EncryptPayloadKey then + Just v + else + Nothing + getValue _ = Nothing + + validKeyAndMessage :: Word64 -> TxMetadataValue -> Bool + validKeyAndMessage k v = k == cip20MetadataKey && isJust (parseMessage v) + + extractMessage :: Either ErrConstructTx TxMetadataValue + extractMessage + | [v] <- F.toList filteredMap = + Right v + | otherwise = + Left ErrConstructTxIncorrectRawMetadata + where + TxMetadata themap = payload ^. #txMetadataWithSchema_metadata + filteredMap = Map.filterWithKey validKeyAndMessage themap + + encryptMessage :: TxMetadataValue -> Either ErrConstructTx TxMetadataValue + encryptMessage = \case + TxMetaMap pairs -> + TxMetaMap . reverse . L.nub . reverse . concat <$> + mapM encryptPairIfQualifies pairs + _ -> + error "encryptMessage should have TxMetaMap value" + where + encryptPairIfQualifies + :: (TxMetadataValue, TxMetadataValue) + -> Either ErrConstructTx [(TxMetadataValue, TxMetadataValue)] + encryptPairIfQualifies = \case + (TxMetaText "msg", m) -> do + bimap ErrConstructTxEncryptMetadata toPair (encryptValue m) + pair -> + Right [pair] + + encryptValue :: TxMetadataValue -> Either CipherError ByteString + encryptValue + = AES256CBC.encrypt WithPadding secretKey iv saltM + . BL.toStrict + . Aeson.encode + . metadataValueToJsonNoSchema + + toPair :: ByteString -> [(TxMetadataValue, TxMetadataValue)] + toPair encryptedMessage = + [ (TxMetaText cip83EncryptPayloadKey, TxMetaList (toChunks encryptedMessage)) + , (TxMetaText cip83EncryptMethodKey, TxMetaText cip83EncryptPayloadValue) + ] + + toChunks :: ByteString -> [TxMetadataValue] + toChunks + = fmap TxMetaText + . T.chunksOf 64 + . T.decodeUtf8 + . convertToBase Base64 + + updateTxMetadata :: TxMetadataValue -> TxMetadata + updateTxMetadata v = TxMetadata (Map.insert cip20MetadataKey v themap) + where + TxMetadata themap = payload ^. #txMetadataWithSchema_metadata diff --git a/lib/integration/scenarios/Test/Integration/Scenario/API/Shelley/TransactionsNew.hs b/lib/integration/scenarios/Test/Integration/Scenario/API/Shelley/TransactionsNew.hs index 030a2112ab6..c8bbd8f705e 100644 --- a/lib/integration/scenarios/Test/Integration/Scenario/API/Shelley/TransactionsNew.hs +++ b/lib/integration/scenarios/Test/Integration/Scenario/API/Shelley/TransactionsNew.hs @@ -70,9 +70,6 @@ import Cardano.Wallet.Address.Keys.WalletKey import Cardano.Wallet.Api.Hex ( fromHexText ) -import Cardano.Wallet.Api.Http.Shelley.Server - ( toMetadataEncrypted - ) import Cardano.Wallet.Api.Types ( AddressAmount (..) , ApiAddressWithPath (..) @@ -127,6 +124,7 @@ import Cardano.Wallet.Api.Types.SchemaMetadata ( TxMetadataSchema (..) , TxMetadataWithSchema (..) , metadataPBKDF2Config + , toMetadataEncrypted ) import Cardano.Wallet.Api.Types.Transaction ( ApiAddress (..) @@ -5537,8 +5535,7 @@ spec = describe "NEW_SHELLEY_TRANSACTIONS" $ do let decodePayloadEncrypted = Json (toJSON signedTx) let (Right expMetadataEncrypted) = - ApiT <$> toMetadataEncrypted encryptMetadata metadataToBeEncrypted - (Just salt) + ApiT <$> toMetadataEncrypted pwd metadataToBeEncrypted (Just salt) rDecodedTxEncrypted <- request @(ApiDecodedTransaction n) ctx (Link.decodeTransaction @'Shelley wa) Default decodePayloadEncrypted verify rDecodedTxEncrypted diff --git a/lib/unit/test/unit/Cardano/Wallet/Api/TypesSpec.hs b/lib/unit/test/unit/Cardano/Wallet/Api/TypesSpec.hs index 87157420570..381951ec206 100644 --- a/lib/unit/test/unit/Cardano/Wallet/Api/TypesSpec.hs +++ b/lib/unit/test/unit/Cardano/Wallet/Api/TypesSpec.hs @@ -112,9 +112,6 @@ import Cardano.Wallet.Address.Keys.WalletKey import Cardano.Wallet.Api ( Api ) -import Cardano.Wallet.Api.Http.Shelley.Server - ( toMetadataEncrypted - ) import Cardano.Wallet.Api.Types ( AccountPostData (..) , AddressAmount (..) @@ -307,6 +304,7 @@ import Cardano.Wallet.Api.Types.RestorationMode import Cardano.Wallet.Api.Types.SchemaMetadata ( TxMetadataSchema (..) , TxMetadataWithSchema (..) + , toMetadataEncrypted ) import Cardano.Wallet.Api.Types.Transaction ( ApiAddress (..) @@ -1225,11 +1223,7 @@ spec = do -- $ echo -n '"secret data"' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -nosalt -- vBSywXY+WGcrckHUCyjJcQ== it "short msg - no salt" $ do - let apiEncrypt = ApiEncryptMetadata - { passphrase = ApiT $ Passphrase "cardano" - , method = Nothing - } - schemaBefore = + let schemaBefore = TxMetadataWithSchema TxMetadataNoSchema $ Cardano.TxMetadata $ Map.fromList @@ -1262,18 +1256,14 @@ spec = do ] ) ] - toMetadataEncrypted apiEncrypt schemaBefore Nothing + toMetadataEncrypted "cardano" schemaBefore Nothing `shouldBe` Right schemaAfter -- $ echo -n '"secret data that is long enough to produce more than 64 bytes"' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -nosalt -- OLSOdRF+P56rW9gUopHcs0HHcdmPP5ujhSuB+r84VJgvsMOsqmIZx2etosnkyOc8 -- ygjbu25gCdhJh7iEpAJVaA== it "long msg - no salt" $ do - let apiEncrypt = ApiEncryptMetadata - { passphrase = ApiT $ Passphrase "cardano" - , method = Nothing - } - schemaBefore = + let schemaBefore = TxMetadataWithSchema TxMetadataNoSchema $ Cardano.TxMetadata $ Map.fromList @@ -1310,18 +1300,14 @@ spec = do ] ) ] - toMetadataEncrypted apiEncrypt schemaBefore Nothing + toMetadataEncrypted "cardano" schemaBefore Nothing `shouldBe` Right schemaAfter -- $ echo -n '["Invoice-No: 123456789","Order-No: 7654321","Email: john@doe.com"]' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -nosalt -- IBcjjGQ7akr/CV2Zb0HtCvEPQNndZujCZ7iaFGMjOX3q3PJg5aRUvHgO3gPnDzYE -- 7jFsGUK1bCdwsrn8kqI92NccbG8oAtPJUktZTTcO/bg= it "cip msg - no salt" $ do - let apiEncrypt = ApiEncryptMetadata - { passphrase = ApiT $ Passphrase "cardano" - , method = Nothing - } - schemaBefore = + let schemaBefore = TxMetadataWithSchema TxMetadataNoSchema $ Cardano.TxMetadata $ Map.fromList @@ -1360,17 +1346,13 @@ spec = do ] ) ] - toMetadataEncrypted apiEncrypt schemaBefore Nothing + toMetadataEncrypted "cardano" schemaBefore Nothing `shouldBe` Right schemaAfter -- $ $ echo -n '"secret data"' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -S 3030303030303030 -- U2FsdGVkX18wMDAwMDAwMF0ea/2sHeptB3SvZtgc600= it "short msg - salted" $ do - let apiEncrypt = ApiEncryptMetadata - { passphrase = ApiT $ Passphrase "cardano" - , method = Nothing - } - schemaBefore = + let schemaBefore = TxMetadataWithSchema TxMetadataNoSchema $ Cardano.TxMetadata $ Map.fromList [ ( 674 @@ -1404,18 +1386,14 @@ spec = do ) ] saltM = fromHexToM "3030303030303030" - toMetadataEncrypted apiEncrypt schemaBefore saltM + toMetadataEncrypted "cardano" schemaBefore saltM `shouldBe` Right schemaAfter -- $ echo -n '"secret data that is long enough to produce more than 64 bytes"' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -S 3030303030303030 -- U2FsdGVkX18wMDAwMDAwMPNdhZQON/Hlwqvk4+sNRCa90QrAVpIGUlWgZhgNlwKh -- PbR/qyT2q0tejHQmsHdORif5rvZYTzJGsTutA0RIcFU= it "long msg - salted" $ do - let apiEncrypt = ApiEncryptMetadata - { passphrase = ApiT $ Passphrase "cardano" - , method = Nothing - } - schemaBefore = + let schemaBefore = TxMetadataWithSchema TxMetadataNoSchema $ Cardano.TxMetadata $ Map.fromList [ ( 674 @@ -1450,18 +1428,14 @@ spec = do ) ] saltM = fromHexToM "3030303030303030" - toMetadataEncrypted apiEncrypt schemaBefore saltM + toMetadataEncrypted "cardano" schemaBefore saltM `shouldBe` Right schemaAfter -- $ $ echo -n '["Invoice-No: 123456789","Order-No: 7654321","Email: john@doe.com"]' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -S 3030303030303030 -- U2FsdGVkX18wMDAwMDAwMFlOS4b0tXrZA7U5aQaHeI/sP74h84EPEjGv0wl4D8Do -- +SIXXn04a9xkoFHk4ZH281nIfH5lpClsO16p2vRpSsdBDFO78aTPX3bsHsRE0L2A it "cip msg - salted" $ do - let apiEncrypt = ApiEncryptMetadata - { passphrase = ApiT $ Passphrase "cardano" - , method = Nothing - } - schemaBefore = + let schemaBefore = TxMetadataWithSchema TxMetadataNoSchema $ Cardano.TxMetadata $ Map.fromList @@ -1501,15 +1475,11 @@ spec = do ) ] saltM = fromHexToM "3030303030303030" - toMetadataEncrypted apiEncrypt schemaBefore saltM + toMetadataEncrypted "cardano" schemaBefore saltM `shouldBe` Right schemaAfter it "msg wrong label - no salt" $ do - let apiEncrypt = ApiEncryptMetadata - { passphrase = ApiT $ Passphrase "cardano" - , method = Nothing - } - schemaBefore = + let schemaBefore = TxMetadataWithSchema TxMetadataNoSchema $ Cardano.TxMetadata $ Map.fromList [ ( 675 @@ -1527,15 +1497,11 @@ spec = do ] ) ] - toMetadataEncrypted apiEncrypt schemaBefore Nothing + toMetadataEncrypted "cardano" schemaBefore Nothing `shouldBe` Left ErrConstructTxIncorrectRawMetadata it "msg without 'msg field' - no salt" $ do - let apiEncrypt = ApiEncryptMetadata - { passphrase = ApiT $ Passphrase "cardano" - , method = Nothing - } - schemaBefore = + let schemaBefore = TxMetadataWithSchema TxMetadataNoSchema $ Cardano.TxMetadata $ Map.fromList @@ -1554,7 +1520,7 @@ spec = do ] ) ] - toMetadataEncrypted apiEncrypt schemaBefore Nothing + toMetadataEncrypted "cardano" schemaBefore Nothing `shouldBe` Left ErrConstructTxIncorrectRawMetadata fromHexToM :: Text -> Maybe ByteString From fc40a5a0233a6018f0c583ae5c5ad633597a1c13 Mon Sep 17 00:00:00 2001 From: Pawel Jakubas Date: Wed, 17 Jul 2024 17:21:40 +0200 Subject: [PATCH 15/26] relocate metadata encryption - part 4 --- .../Cardano/Wallet/Api/Http/Shelley/Server.hs | 129 +----------------- .../Wallet/Api/Types/SchemaMetadata.hs | 99 ++++++++++++++ 2 files changed, 105 insertions(+), 123 deletions(-) diff --git a/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs b/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs index cebb022ed4e..da1075bb463 100644 --- a/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs +++ b/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs @@ -121,7 +121,6 @@ module Cardano.Wallet.Api.Http.Shelley.Server , rndStateChange , withWorkerCtx , getCurrentEpoch - , fromMetadataEncrypted -- * Workers , manageRewardBalance @@ -178,7 +177,6 @@ import Cardano.Wallet , ErrConstructSharedWallet (..) , ErrConstructTx (..) , ErrCreateMigrationPlan (..) - , ErrDecodeTx (..) , ErrGetPolicyId (..) , ErrNoSuchWallet (..) , ErrReadRewardAccount (..) @@ -357,7 +355,6 @@ import Cardano.Wallet.Api.Types , ApiDRepSpecifier (..) , ApiDecodeTransactionPostData (..) , ApiDecodedTransaction (..) - , ApiEncryptMetadata (..) , ApiExternalInput (..) , ApiFee (..) , ApiForeignStakeKey (..) @@ -478,11 +475,7 @@ import Cardano.Wallet.Api.Types.MintBurn import Cardano.Wallet.Api.Types.SchemaMetadata ( TxMetadataSchema (..) , TxMetadataWithSchema (TxMetadataWithSchema) - , metadataPBKDF2Config - , cip20MetadataKey - , cip83EncryptMethodKey - , cip83EncryptPayloadKey - , cip83EncryptPayloadValue + , fromMetadataEncrypted , toMetadataEncrypted ) import Cardano.Wallet.Api.Types.Transaction @@ -725,19 +718,11 @@ import Control.Tracer ( Tracer , contramap ) -import Cryptography.Cipher.AES256CBC - ( CipherMode (..) - ) import Cryptography.Core ( genSalt ) import Data.Bifunctor - ( bimap - , first - ) -import Data.ByteArray.Encoding - ( Base (..) - , convertFromBase + ( first ) import Data.ByteString ( ByteString @@ -915,19 +900,14 @@ import qualified Cardano.Wallet.Read as Read import qualified Cardano.Wallet.Read.Hash as Hash import qualified Cardano.Wallet.Registry as Registry import qualified Control.Concurrent.Concierge as Concierge -import qualified Cryptography.Cipher.AES256CBC as AES256CBC -import qualified Cryptography.KDF.PBKDF2 as PBKDF2 -import qualified Data.Aeson as Aeson import qualified Data.ByteArray as BA import qualified Data.ByteString as BS -import qualified Data.ByteString.Lazy as BL import qualified Data.Foldable as F import qualified Data.List as L import qualified Data.List.NonEmpty as NE import qualified Data.Map.Strict as Map import qualified Data.Set as Set import qualified Data.Text as T -import qualified Data.Text.Encoding as T import qualified Internal.Cardano.Write.Tx as Write ( Datum (DatumHash, NoDatum) , IsRecentEra @@ -3003,105 +2983,6 @@ constructTransaction api knownPools poolStatus apiWalletId body = do . Map.toList . foldr (uncurry (Map.insertWith (<>))) Map.empty - --- When decryption is enabled we do the following: --- (a) retrieve list of TxMetaBytes under proper key, ie.674, --- cip20MetadataKey --- (b) recreate each encrypted payload from chunks --- (0, TxMetaBytes chunk1) --- (1, TxMetaBytes chunk2) --- .... --- (N, TxMetaBytes chunkN) --- ie., payload=chunk1+chunk2+...+chunkN --- (c) decrypt each payload --- (d) update structure --- (e) decode metadata -fromMetadataEncrypted - :: ApiEncryptMetadata - -> Cardano.TxMetadata - -> Either ErrDecodeTx Cardano.TxMetadata -fromMetadataEncrypted apiEncrypt metadata = - composePayload metadata >>= - mapM decrypt >>= - adjust metadata - where - checkPresenceOfMethod value = - let presentPair (Cardano.TxMetaText k, Cardano.TxMetaText v) = - k == cip83EncryptMethodKey && v == cip83EncryptPayloadValue - presentPair _ = False - in case value of - Cardano.TxMetaMap list -> not (any presentPair list) - _ -> True - getEncryptedPayload value = - let presentPair (Cardano.TxMetaText k, Cardano.TxMetaList _) = - k == cip83EncryptPayloadKey - presentPair _ = False - in case value of - Cardano.TxMetaMap list -> snd <$> filter presentPair list - _ -> [] - extractTxt (Cardano.TxMetaText txt) = txt - extractTxt _ = - error "TxMetaText is expected" - extractPayload (Cardano.TxMetaList chunks)= - foldl T.append T.empty $ extractTxt <$> chunks - extractPayload _ = T.empty - composePayload (Cardano.TxMetadata themap) = do - validValue <- case Map.lookup cip20MetadataKey themap of - Nothing -> Left ErrDecodeTxMissingMetadataKey - Just v -> pure v - when (checkPresenceOfMethod validValue) $ - Left ErrDecodeTxMissingEncryptionMethod - let payloads = getEncryptedPayload validValue - if null payloads then - Left ErrDecodeTxMissingValidEncryptionPayload - else do - let extracted = extractPayload <$> payloads - when (T.empty `elem` extracted) $ - Left ErrDecodeTxMissingValidEncryptionPayload - Right extracted - - pwd = BA.convert $ unPassphrase $ getApiT $ apiEncrypt ^. #passphrase - decodeFromJSON = ---use metadataValueFromJsonNoSchema when available from cardano-api - first (ErrDecodeTxDecryptedPayload . T.pack) . - Aeson.eitherDecode . BL.fromStrict - decrypt payload = case convertFromBase Base64 (T.encodeUtf8 payload) of - Right payloadBS -> - case AES256CBC.getSaltFromEncrypted payloadBS of - Nothing -> Left ErrDecodeTxMissingSalt - Just salt -> do - let (secretKey, iv) = - PBKDF2.generateKey metadataPBKDF2Config pwd (Just salt) - decrypted <- bimap ErrDecodeTxDecryptPayload fst - (AES256CBC.decrypt WithPadding secretKey iv payloadBS) - decodeFromJSON decrypted - Left _ -> - Left ErrDecodeTxEncryptedPayloadWrongBase - - adjust (Cardano.TxMetadata metadata') decodedElems = - pure $ Cardano.TxMetadata $ - Map.adjust updateMetaMap cip20MetadataKey metadata' - where - updateElem acc@(decryptedList, list) elem' = case elem' of - (Cardano.TxMetaText k, Cardano.TxMetaText v) -> - if k == cip83EncryptMethodKey && v == cip83EncryptPayloadValue then - -- omiting this element - acc - else - (decryptedList, list ++ [elem']) - (Cardano.TxMetaText k, v) -> case decryptedList of - toAdd : rest -> - if k == cip83EncryptPayloadKey then - (rest, list ++ [(Cardano.TxMetaText k, toAdd)] ) - else - (decryptedList, list ++ [(Cardano.TxMetaText k, v)] ) - _ -> error "we have checked already in composePayload that there is enough elements in decrypedList" - _ -> error "we have checked already in composePayload that there is pair (TxMetaText, something)" - - updateMetaMap v = case v of - Cardano.TxMetaMap list -> - Cardano.TxMetaMap $ snd $ L.foldl updateElem (decodedElems,[]) list - _ -> error "we have checked already in composePayload that there is TxMetaMap" - toUsignedTxWdrl :: c -> ApiWithdrawalGeneral n -> Maybe (RewardAccount, Coin, c) toUsignedTxWdrl p = \case @@ -3563,8 +3444,10 @@ decodeTransaction } = walletTx db = wrk ^. dbLayer metadata' <- case (decryptMetadata, metadata) of - (Just apiDecrypt, Just meta) -> - case fromMetadataEncrypted apiDecrypt meta of + (Just apiDecrypt, Just meta) -> do + let pwd = BA.convert $ unPassphrase $ + getApiT $ apiDecrypt ^. #passphrase + case fromMetadataEncrypted pwd meta of Left err -> liftHandler $ throwE err Right txmetadata -> diff --git a/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs b/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs index c8087648086..022ba45b5b3 100644 --- a/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs +++ b/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs @@ -32,6 +32,7 @@ import Cardano.Api.Error ) import Cardano.Wallet ( ErrConstructTx (..) + , ErrDecodeTx (..) ) import Cardano.Wallet.Primitive.Types.Tx ( TxMetadata (..) @@ -66,6 +67,7 @@ import Data.Bifunctor ) import Data.ByteArray.Encoding ( Base (..) + , convertFromBase , convertToBase ) import Data.ByteString @@ -392,3 +394,100 @@ toMetadataEncrypted pwd payload saltM = updateTxMetadata v = TxMetadata (Map.insert cip20MetadataKey v themap) where TxMetadata themap = payload ^. #txMetadataWithSchema_metadata + +-- When decryption is enabled we do the following: +-- (a) retrieve list of TxMetaBytes under proper key, ie.674, +-- cip20MetadataKey +-- (b) recreate each encrypted payload from chunks +-- (0, TxMetaBytes chunk1) +-- (1, TxMetaBytes chunk2) +-- .... +-- (N, TxMetaBytes chunkN) +-- ie., payload=chunk1+chunk2+...+chunkN +-- (c) decrypt each payload +-- (d) update structure +-- (e) decode metadata +fromMetadataEncrypted + :: ByteString + -> TxMetadata + -> Either ErrDecodeTx TxMetadata +fromMetadataEncrypted pwd metadata = + composePayload metadata >>= + mapM decrypt >>= + adjust metadata + where + checkPresenceOfMethod value = + let presentPair (TxMetaText k, TxMetaText v) = + k == cip83EncryptMethodKey && v == cip83EncryptPayloadValue + presentPair _ = False + in case value of + TxMetaMap list -> not (any presentPair list) + _ -> True + getEncryptedPayload value = + let presentPair (TxMetaText k, TxMetaList _) = + k == cip83EncryptPayloadKey + presentPair _ = False + in case value of + TxMetaMap list -> snd <$> filter presentPair list + _ -> [] + extractTxt (TxMetaText txt) = txt + extractTxt _ = + error "TxMetaText is expected" + extractPayload (TxMetaList chunks)= + foldl T.append T.empty $ extractTxt <$> chunks + extractPayload _ = T.empty + composePayload (TxMetadata themap) = do + validValue <- case Map.lookup cip20MetadataKey themap of + Nothing -> Left ErrDecodeTxMissingMetadataKey + Just v -> pure v + when (checkPresenceOfMethod validValue) $ + Left ErrDecodeTxMissingEncryptionMethod + let payloads = getEncryptedPayload validValue + if null payloads then + Left ErrDecodeTxMissingValidEncryptionPayload + else do + let extracted = extractPayload <$> payloads + when (T.empty `elem` extracted) $ + Left ErrDecodeTxMissingValidEncryptionPayload + Right extracted + + decodeFromJSON = ---use metadataValueFromJsonNoSchema when available from cardano-api + first (ErrDecodeTxDecryptedPayload . T.pack) . + Aeson.eitherDecode . BL.fromStrict + decrypt payload = case convertFromBase Base64 (T.encodeUtf8 payload) of + Right payloadBS -> + case AES256CBC.getSaltFromEncrypted payloadBS of + Nothing -> Left ErrDecodeTxMissingSalt + Just salt -> do + let (secretKey, iv) = + PBKDF2.generateKey metadataPBKDF2Config pwd (Just salt) + decrypted <- bimap ErrDecodeTxDecryptPayload fst + (AES256CBC.decrypt WithPadding secretKey iv payloadBS) + decodeFromJSON decrypted + Left _ -> + Left ErrDecodeTxEncryptedPayloadWrongBase + + adjust (TxMetadata metadata') decodedElems = + pure $ TxMetadata $ + Map.adjust updateMetaMap cip20MetadataKey metadata' + where + updateElem acc@(decryptedList, list) elem' = case elem' of + (TxMetaText k, TxMetaText v) -> + if k == cip83EncryptMethodKey && v == cip83EncryptPayloadValue then + -- omiting this element + acc + else + (decryptedList, list ++ [elem']) + (TxMetaText k, v) -> case decryptedList of + toAdd : rest -> + if k == cip83EncryptPayloadKey then + (rest, list ++ [(TxMetaText k, toAdd)] ) + else + (decryptedList, list ++ [(TxMetaText k, v)] ) + _ -> error "we have checked already in composePayload that there is enough elements in decrypedList" + _ -> error "we have checked already in composePayload that there is pair (TxMetaText, something)" + + updateMetaMap v = case v of + TxMetaMap list -> + TxMetaMap $ snd $ L.foldl updateElem (decodedElems,[]) list + _ -> error "we have checked already in composePayload that there is TxMetaMap" From ec258b175a5a26444a6b2a41091c5758d62d2683 Mon Sep 17 00:00:00 2001 From: Pawel Jakubas Date: Thu, 18 Jul 2024 08:36:08 +0200 Subject: [PATCH 16/26] code standards --- .../src/Cardano/Wallet/Api/Types/SchemaMetadata.hs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs b/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs index 022ba45b5b3..2fe8b1a8cd8 100644 --- a/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs +++ b/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs @@ -73,16 +73,16 @@ import Data.ByteArray.Encoding import Data.ByteString ( ByteString ) -import Data.Maybe - ( isJust - , fromMaybe - , mapMaybe - ) import Data.Generics.Internal.VL.Lens ( (^.) ) import Data.Generics.Labels () +import Data.Maybe + ( isJust + , fromMaybe + , mapMaybe + ) import Data.Text ( Text ) @@ -108,8 +108,8 @@ import qualified Data.ByteString.Base16 as B16 import qualified Data.ByteString.Char8 as B8 import qualified Data.ByteString.Lazy as BL import qualified Data.Foldable as F -import qualified Data.Map.Strict as Map import qualified Data.List as L +import qualified Data.Map.Strict as Map import qualified Data.Scientific as Scientific import qualified Data.Text as T import qualified Data.Text.Encoding as T From d536a7e5261782c4a4eb1bcd8b861f7b2cf705ef Mon Sep 17 00:00:00 2001 From: Pawel Jakubas Date: Fri, 26 Jul 2024 12:57:18 +0200 Subject: [PATCH 17/26] relocation to primitive 1 relocation to primitive 2 relocation to primitive 3 relocation to primitive 4 relocation to primitive 5 --- lib/api/cardano-wallet-api.cabal | 2 - .../Cardano/Wallet/Api/Http/Server/Error.hs | 25 +- .../Cardano/Wallet/Api/Http/Shelley/Server.hs | 18 +- .../Wallet/Api/Types/SchemaMetadata.hs | 390 +--------------- .../Scenario/API/Shelley/TransactionsNew.hs | 8 +- lib/primitive/cardano-wallet-primitive.cabal | 6 +- .../Primitive/Types/MetadataEncryption.hs | 428 ++++++++++++++++++ .../test/unit/Cardano/Wallet/Api/TypesSpec.hs | 17 +- lib/wallet/src/Cardano/Wallet.hs | 18 +- 9 files changed, 478 insertions(+), 434 deletions(-) create mode 100644 lib/primitive/lib/Cardano/Wallet/Primitive/Types/MetadataEncryption.hs diff --git a/lib/api/cardano-wallet-api.cabal b/lib/api/cardano-wallet-api.cabal index a32f55e135f..e72eb52b469 100644 --- a/lib/api/cardano-wallet-api.cabal +++ b/lib/api/cardano-wallet-api.cabal @@ -40,9 +40,7 @@ library , aeson , aeson-pretty , ansi-terminal - , attoparsec , base - , base16-bytestring , bech32 , bech32-th , binary diff --git a/lib/api/src/Cardano/Wallet/Api/Http/Server/Error.hs b/lib/api/src/Cardano/Wallet/Api/Http/Server/Error.hs index 7205d747c5e..7299941bf99 100644 --- a/lib/api/src/Cardano/Wallet/Api/Http/Server/Error.hs +++ b/lib/api/src/Cardano/Wallet/Api/Http/Server/Error.hs @@ -137,6 +137,10 @@ import Cardano.Wallet.Primitive.Ledger.Convert import Cardano.Wallet.Primitive.Slotting ( PastHorizonException ) +import Cardano.Wallet.Primitive.Types.MetadataEncryption + ( ErrMetadataDecryption (..) + , ErrMetadataEncryption (..) + ) import Cardano.Wallet.Primitive.Types.TokenBundle ( TokenBundle (TokenBundle) ) @@ -479,12 +483,12 @@ instance IsServerError ErrConstructTx where , "Please delegate again (in that case, the wallet will automatically vote to abstain), " , "or make a vote transaction before the withdrawal transaction." ] - ErrConstructTxIncorrectRawMetadata -> + ErrConstructTxFromMetadataEncryption ErrIncorrectRawMetadata -> apiError err403 InvalidMetadataEncryption $ mconcat [ "It looks like the metadata does not " , "have `msg` field that is supposed to be encrypted." ] - ErrConstructTxEncryptMetadata cryptoError -> + ErrConstructTxFromMetadataEncryption (ErrCannotEncryptMetadata cryptoError) -> apiError err403 InvalidMetadataEncryption $ mconcat [ "It looks like the metadata cannot be encrypted. " , "The exact error is: " @@ -496,41 +500,42 @@ instance IsServerError ErrConstructTx where instance IsServerError ErrDecodeTx where toServerError = \case - ErrDecodeTxMissingMetadataKey -> + ErrDecodeTxFromMetadataDecryption ErrMissingMetadataKey -> apiError err403 InvalidMetadataDecryption $ mconcat [ "It looks like the encrypted metadata has wrong structure. " , "It is expected to be a map with key '674' - see CIP20." ] - ErrDecodeTxMissingEncryptionMethod -> + ErrDecodeTxFromMetadataDecryption ErrMissingEncryptionMethod -> apiError err403 InvalidMetadataDecryption $ mconcat [ "It looks like the encrypted metadata has wrong structure. " , "It is expected to have encryption method under 'enc' key - see CIP83." ] - ErrDecodeTxMissingValidEncryptionPayload -> + ErrDecodeTxFromMetadataDecryption ErrMissingValidEncryptionPayload -> apiError err403 InvalidMetadataDecryption $ mconcat [ "It looks like the encrypted metadata has wrong structure. " , "It is expected to have encryption payload under 'msg' key - see CIP83." ] - ErrDecodeTxDecryptedPayload err -> + ErrDecodeTxFromMetadataDecryption (ErrCannotAesonDecodePayload err) -> apiError err403 InvalidMetadataDecryption $ mconcat [ "It looks like the decrypted metadata cannot be decoded. " , "The exact error is: " , err ] - ErrDecodeTxMissingSalt -> + ErrDecodeTxFromMetadataDecryption ErrMissingSalt -> apiError err403 InvalidMetadataDecryption $ mconcat [ "It looks like the decrypted metadata can be decoded, but " , "misses salt." ] - ErrDecodeTxDecryptPayload cryptoError -> + ErrDecodeTxFromMetadataDecryption (ErrCannotDecryptPayload cryptoError) -> apiError err403 InvalidMetadataDecryption $ mconcat [ "It looks like the encrypted metadata cannot be decrypted. " , "The exact error is: " , T.pack (show cryptoError) ] - ErrDecodeTxEncryptedPayloadWrongBase -> + ErrDecodeTxFromMetadataDecryption ErrEncryptedPayloadWrongBase -> apiError err403 InvalidMetadataDecryption $ mconcat - [ "It looks like the encrypted metadata is not represented as Base64." + [ "It looks like the encrypted metadata is not represented as a list of Base64 " + , "- see CIP83." ] instance IsServerError ErrGetPolicyId where diff --git a/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs b/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs index da1075bb463..858ce090394 100644 --- a/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs +++ b/lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs @@ -177,6 +177,7 @@ import Cardano.Wallet , ErrConstructSharedWallet (..) , ErrConstructTx (..) , ErrCreateMigrationPlan (..) + , ErrDecodeTx (..) , ErrGetPolicyId (..) , ErrNoSuchWallet (..) , ErrReadRewardAccount (..) @@ -475,8 +476,6 @@ import Cardano.Wallet.Api.Types.MintBurn import Cardano.Wallet.Api.Types.SchemaMetadata ( TxMetadataSchema (..) , TxMetadataWithSchema (TxMetadataWithSchema) - , fromMetadataEncrypted - , toMetadataEncrypted ) import Cardano.Wallet.Api.Types.Transaction ( ApiAddress (..) @@ -608,6 +607,10 @@ import Cardano.Wallet.Primitive.Types.DRep import Cardano.Wallet.Primitive.Types.Hash ( Hash (..) ) +import Cardano.Wallet.Primitive.Types.MetadataEncryption + ( fromMetadataEncrypted + , toMetadataEncrypted + ) import Cardano.Wallet.Primitive.Types.TokenBundle ( TokenBundle (..) ) @@ -2586,12 +2589,13 @@ constructTransaction api knownPools poolStatus apiWalletId body = do let pwd :: ByteString pwd = BA.convert $ unPassphrase $ getApiT $ apiEncrypt ^. #passphrase - toMetadataEncrypted pwd metadataWithSchema (Just salt) + meta = metadataWithSchema ^. #txMetadataWithSchema_metadata + toMetadataEncrypted pwd meta (Just salt) & \case Left err -> - liftHandler $ throwE err - Right meta -> - pure $ Just meta + liftHandler $ throwE $ ErrConstructTxFromMetadataEncryption err + Right meta' -> + pure $ Just meta' _ -> pure $ body ^? #metadata . traverse . #txMetadataWithSchema_metadata @@ -3449,7 +3453,7 @@ decodeTransaction getApiT $ apiDecrypt ^. #passphrase case fromMetadataEncrypted pwd meta of Left err -> - liftHandler $ throwE err + liftHandler $ throwE $ ErrDecodeTxFromMetadataDecryption err Right txmetadata -> pure . Just . ApiT $ txmetadata _ -> pure $ ApiT <$> metadata diff --git a/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs b/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs index 2fe8b1a8cd8..d8c694592cd 100644 --- a/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs +++ b/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs @@ -21,19 +21,12 @@ module Cardano.Wallet.Api.Types.SchemaMetadata where import Cardano.Api ( TxMetadataJsonSchema (..) - , TxMetadataJsonSchemaError (..) - , TxMetadataValue (..) , metadataFromJson , metadataToJson - , metadataValueToJsonNoSchema ) import Cardano.Api.Error ( displayError ) -import Cardano.Wallet - ( ErrConstructTx (..) - , ErrDecodeTx (..) - ) import Cardano.Wallet.Primitive.Types.Tx ( TxMetadata (..) ) @@ -43,77 +36,15 @@ import Control.Applicative import Control.DeepSeq ( NFData ) -import Control.Monad - ( guard - , when - ) -import Cryptography.Cipher.AES256CBC - ( CipherError - , CipherMode (..) - ) -import Cryptography.Hash.Core - ( SHA256 (..) - ) -import Cryptography.KDF.PBKDF2 - ( PBKDF2Config (..) - ) import Data.Aeson ( FromJSON (parseJSON) , ToJSON (toJSON) ) -import Data.Bifunctor - ( bimap - , first - ) -import Data.ByteArray.Encoding - ( Base (..) - , convertFromBase - , convertToBase - ) -import Data.ByteString - ( ByteString - ) -import Data.Generics.Internal.VL.Lens - ( (^.) - ) -import Data.Generics.Labels - () -import Data.Maybe - ( isJust - , fromMaybe - , mapMaybe - ) -import Data.Text - ( Text - ) -import Data.Word - ( Word64 - ) import GHC.Generics ( Generic ) -import Prelude -import qualified Cardano.Ledger.Binary as CBOR -import qualified Cardano.Ledger.Shelley.TxAuxData as Ledger -import qualified Codec.CBOR.Magic as CBOR -import qualified Cryptography.Cipher.AES256CBC as AES256CBC -import qualified Cryptography.KDF.PBKDF2 as PBKDF2 -import qualified Data.Aeson as Aeson -import qualified Data.Aeson.Key as Aeson -import qualified Data.Aeson.KeyMap as Aeson -import qualified Data.Attoparsec.ByteString.Char8 as Atto -import qualified Data.ByteString as BS -import qualified Data.ByteString.Base16 as B16 -import qualified Data.ByteString.Char8 as B8 -import qualified Data.ByteString.Lazy as BL -import qualified Data.Foldable as F -import qualified Data.List as L -import qualified Data.Map.Strict as Map -import qualified Data.Scientific as Scientific -import qualified Data.Text as T -import qualified Data.Text.Encoding as T -import qualified Data.Vector as V +import Prelude -- | A tag to select the json codec data TxMetadataSchema = TxMetadataNoSchema | TxMetadataDetailedSchema @@ -172,322 +103,3 @@ instance FromJSON TxMetadataWithSchema where . either (fail . displayError) pure . metadataFromJson TxMetadataJsonNoSchema ) - -instance ToJSON TxMetadataValue where - toJSON = metadataValueToJsonNoSchema - -instance FromJSON TxMetadataValue where - parseJSON = either (fail . displayError) pure . metadataValueFromJsonNoSchema - --- when cardano-api exports metadataValueFromJsonNoSchema the below could be removed (together with cabal dependencies) -metadataValueFromJsonNoSchema - :: Aeson.Value - -> Either TxMetadataJsonSchemaError TxMetadataValue -metadataValueFromJsonNoSchema = conv - where - conv :: Aeson.Value -> Either TxMetadataJsonSchemaError TxMetadataValue - conv Aeson.Null = Left TxMetadataJsonNullNotAllowed - conv Aeson.Bool{} = Left TxMetadataJsonBoolNotAllowed - - conv (Aeson.Number d) = - case Scientific.floatingOrInteger d :: Either Double Integer of - Left n -> Left (TxMetadataJsonNumberNotInteger n) - Right n -> Right (TxMetaNumber n) - - conv (Aeson.String s) - | Just s' <- T.stripPrefix bytesPrefix s - , let bs' = T.encodeUtf8 s' - , Right bs <- B16.decode bs' - , not (B8.any (\c -> c >= 'A' && c <= 'F') bs') - = Right (TxMetaBytes bs) - - conv (Aeson.String s) = Right (TxMetaText s) - - conv (Aeson.Array vs) = - fmap TxMetaList - . traverse conv - $ V.toList vs - - conv (Aeson.Object kvs) = - fmap - ( TxMetaMap - . sortCanonicalForCbor - ) - . traverse (\(k,v) -> (,) (convKey k) <$> conv v) - . fmap (first Aeson.toText) - $ Aeson.toList kvs - - convKey :: Text -> TxMetadataValue - convKey s = - fromMaybe (TxMetaText s) $ - parseAll ((fmap TxMetaNumber pSigned <* Atto.endOfInput) - <|> (fmap TxMetaBytes pBytes <* Atto.endOfInput)) s - -bytesPrefix :: Text -bytesPrefix = "0x" - -parseAll :: Atto.Parser a -> Text -> Maybe a -parseAll p = - either (const Nothing) Just - . Atto.parseOnly p - . T.encodeUtf8 - -pUnsigned :: Atto.Parser Integer -pUnsigned = do - bs <- Atto.takeWhile1 Atto.isDigit - -- no redundant leading 0s allowed, or we cannot round-trip properly - guard (not (BS.length bs > 1 && B8.head bs == '0')) - return $! BS.foldl' step 0 bs - where - step a w = a * 10 + fromIntegral (w - 48) - -pSigned :: Atto.Parser Integer -pSigned = Atto.signed pUnsigned - -pBytes :: Atto.Parser ByteString -pBytes = do - _ <- Atto.string "0x" - remaining <- Atto.takeByteString - when (B8.any hexUpper remaining) $ fail ("Unexpected uppercase hex characters in " <> show remaining) - case B16.decode remaining of - Right bs -> return bs - _ -> fail ("Expecting base16 encoded string, found: " <> show remaining) - where - hexUpper c = c >= 'A' && c <= 'F' - -sortCanonicalForCbor - :: [(TxMetadataValue, TxMetadataValue)] -> [(TxMetadataValue, TxMetadataValue)] -sortCanonicalForCbor = - map snd - . L.sortOn fst - . map (\e@(k, _) -> (CBOR.uintegerFromBytes $ serialiseKey k, e)) - where - serialiseKey = CBOR.serialize' CBOR.shelleyProtVer . toShelleyMetadatum - -toShelleyMetadatum :: TxMetadataValue -> Ledger.Metadatum -toShelleyMetadatum (TxMetaNumber x) = Ledger.I x -toShelleyMetadatum (TxMetaBytes x) = Ledger.B x -toShelleyMetadatum (TxMetaText x) = Ledger.S x -toShelleyMetadatum (TxMetaList xs) = - Ledger.List [ toShelleyMetadatum x | x <- xs ] -toShelleyMetadatum (TxMetaMap xs) = - Ledger.Map [ (toShelleyMetadatum k, - toShelleyMetadatum v) - | (k,v) <- xs ] - --- Metadata encryption/decryption -metadataPBKDF2Config :: PBKDF2Config SHA256 -metadataPBKDF2Config = PBKDF2Config - { hash = SHA256 - , iterations = 10000 - , keyLength = 32 - , ivLength = 16 - } - --- A key that identifies transaction metadata, defined in CIP-20 and used by --- CIP-83. --- --- See: --- https://github.com/cardano-foundation/CIPs/tree/master/CIP-0020 --- https://github.com/cardano-foundation/CIPs/tree/master/CIP-0083 --- -cip20MetadataKey :: Word64 -cip20MetadataKey = 674 - -cip83EncryptMethodKey :: Text -cip83EncryptMethodKey = "enc" - -cip83EncryptPayloadKey :: Text -cip83EncryptPayloadKey = "msg" - -cip83EncryptPayloadValue :: Text -cip83EncryptPayloadValue = "basic" - --- When encryption is enabled we do the following: --- (a) find field `msg` in the object of "674" label --- (b) encrypt the 'msg' value if present, if there is neither "674" label --- nor 'msg' value inside object of it emit error --- (c) update value of `msg` with the encrypted initial value(s) encoded in --- base64: --- [TxMetaText base64_1, TxMetaText base64_2, ..., TxMetaText base64_n] --- (d) add `enc` field with encryption method value 'basic' -toMetadataEncrypted - :: ByteString - -> TxMetadataWithSchema - -> Maybe ByteString - -> Either ErrConstructTx TxMetadata -toMetadataEncrypted pwd payload saltM = - fmap updateTxMetadata . encryptMessage =<< extractMessage - where - secretKey, iv :: ByteString - (secretKey, iv) = PBKDF2.generateKey metadataPBKDF2Config pwd saltM - - -- `msg` is embedded at the first level - parseMessage :: TxMetadataValue -> Maybe [TxMetadataValue] - parseMessage = \case - TxMetaMap kvs -> - case mapMaybe getValue kvs of - [ ] -> Nothing - vs -> Just vs - _ -> - Nothing - where - getValue :: (TxMetadataValue, TxMetadataValue) -> Maybe TxMetadataValue - getValue (TxMetaText k, v) = - if k == cip83EncryptPayloadKey then - Just v - else - Nothing - getValue _ = Nothing - - validKeyAndMessage :: Word64 -> TxMetadataValue -> Bool - validKeyAndMessage k v = k == cip20MetadataKey && isJust (parseMessage v) - - extractMessage :: Either ErrConstructTx TxMetadataValue - extractMessage - | [v] <- F.toList filteredMap = - Right v - | otherwise = - Left ErrConstructTxIncorrectRawMetadata - where - TxMetadata themap = payload ^. #txMetadataWithSchema_metadata - filteredMap = Map.filterWithKey validKeyAndMessage themap - - encryptMessage :: TxMetadataValue -> Either ErrConstructTx TxMetadataValue - encryptMessage = \case - TxMetaMap pairs -> - TxMetaMap . reverse . L.nub . reverse . concat <$> - mapM encryptPairIfQualifies pairs - _ -> - error "encryptMessage should have TxMetaMap value" - where - encryptPairIfQualifies - :: (TxMetadataValue, TxMetadataValue) - -> Either ErrConstructTx [(TxMetadataValue, TxMetadataValue)] - encryptPairIfQualifies = \case - (TxMetaText "msg", m) -> do - bimap ErrConstructTxEncryptMetadata toPair (encryptValue m) - pair -> - Right [pair] - - encryptValue :: TxMetadataValue -> Either CipherError ByteString - encryptValue - = AES256CBC.encrypt WithPadding secretKey iv saltM - . BL.toStrict - . Aeson.encode - . metadataValueToJsonNoSchema - - toPair :: ByteString -> [(TxMetadataValue, TxMetadataValue)] - toPair encryptedMessage = - [ (TxMetaText cip83EncryptPayloadKey, TxMetaList (toChunks encryptedMessage)) - , (TxMetaText cip83EncryptMethodKey, TxMetaText cip83EncryptPayloadValue) - ] - - toChunks :: ByteString -> [TxMetadataValue] - toChunks - = fmap TxMetaText - . T.chunksOf 64 - . T.decodeUtf8 - . convertToBase Base64 - - updateTxMetadata :: TxMetadataValue -> TxMetadata - updateTxMetadata v = TxMetadata (Map.insert cip20MetadataKey v themap) - where - TxMetadata themap = payload ^. #txMetadataWithSchema_metadata - --- When decryption is enabled we do the following: --- (a) retrieve list of TxMetaBytes under proper key, ie.674, --- cip20MetadataKey --- (b) recreate each encrypted payload from chunks --- (0, TxMetaBytes chunk1) --- (1, TxMetaBytes chunk2) --- .... --- (N, TxMetaBytes chunkN) --- ie., payload=chunk1+chunk2+...+chunkN --- (c) decrypt each payload --- (d) update structure --- (e) decode metadata -fromMetadataEncrypted - :: ByteString - -> TxMetadata - -> Either ErrDecodeTx TxMetadata -fromMetadataEncrypted pwd metadata = - composePayload metadata >>= - mapM decrypt >>= - adjust metadata - where - checkPresenceOfMethod value = - let presentPair (TxMetaText k, TxMetaText v) = - k == cip83EncryptMethodKey && v == cip83EncryptPayloadValue - presentPair _ = False - in case value of - TxMetaMap list -> not (any presentPair list) - _ -> True - getEncryptedPayload value = - let presentPair (TxMetaText k, TxMetaList _) = - k == cip83EncryptPayloadKey - presentPair _ = False - in case value of - TxMetaMap list -> snd <$> filter presentPair list - _ -> [] - extractTxt (TxMetaText txt) = txt - extractTxt _ = - error "TxMetaText is expected" - extractPayload (TxMetaList chunks)= - foldl T.append T.empty $ extractTxt <$> chunks - extractPayload _ = T.empty - composePayload (TxMetadata themap) = do - validValue <- case Map.lookup cip20MetadataKey themap of - Nothing -> Left ErrDecodeTxMissingMetadataKey - Just v -> pure v - when (checkPresenceOfMethod validValue) $ - Left ErrDecodeTxMissingEncryptionMethod - let payloads = getEncryptedPayload validValue - if null payloads then - Left ErrDecodeTxMissingValidEncryptionPayload - else do - let extracted = extractPayload <$> payloads - when (T.empty `elem` extracted) $ - Left ErrDecodeTxMissingValidEncryptionPayload - Right extracted - - decodeFromJSON = ---use metadataValueFromJsonNoSchema when available from cardano-api - first (ErrDecodeTxDecryptedPayload . T.pack) . - Aeson.eitherDecode . BL.fromStrict - decrypt payload = case convertFromBase Base64 (T.encodeUtf8 payload) of - Right payloadBS -> - case AES256CBC.getSaltFromEncrypted payloadBS of - Nothing -> Left ErrDecodeTxMissingSalt - Just salt -> do - let (secretKey, iv) = - PBKDF2.generateKey metadataPBKDF2Config pwd (Just salt) - decrypted <- bimap ErrDecodeTxDecryptPayload fst - (AES256CBC.decrypt WithPadding secretKey iv payloadBS) - decodeFromJSON decrypted - Left _ -> - Left ErrDecodeTxEncryptedPayloadWrongBase - - adjust (TxMetadata metadata') decodedElems = - pure $ TxMetadata $ - Map.adjust updateMetaMap cip20MetadataKey metadata' - where - updateElem acc@(decryptedList, list) elem' = case elem' of - (TxMetaText k, TxMetaText v) -> - if k == cip83EncryptMethodKey && v == cip83EncryptPayloadValue then - -- omiting this element - acc - else - (decryptedList, list ++ [elem']) - (TxMetaText k, v) -> case decryptedList of - toAdd : rest -> - if k == cip83EncryptPayloadKey then - (rest, list ++ [(TxMetaText k, toAdd)] ) - else - (decryptedList, list ++ [(TxMetaText k, v)] ) - _ -> error "we have checked already in composePayload that there is enough elements in decrypedList" - _ -> error "we have checked already in composePayload that there is pair (TxMetaText, something)" - - updateMetaMap v = case v of - TxMetaMap list -> - TxMetaMap $ snd $ L.foldl updateElem (decodedElems,[]) list - _ -> error "we have checked already in composePayload that there is TxMetaMap" diff --git a/lib/integration/scenarios/Test/Integration/Scenario/API/Shelley/TransactionsNew.hs b/lib/integration/scenarios/Test/Integration/Scenario/API/Shelley/TransactionsNew.hs index c8bbd8f705e..345a5b2d52f 100644 --- a/lib/integration/scenarios/Test/Integration/Scenario/API/Shelley/TransactionsNew.hs +++ b/lib/integration/scenarios/Test/Integration/Scenario/API/Shelley/TransactionsNew.hs @@ -123,8 +123,6 @@ import Cardano.Wallet.Api.Types.Error import Cardano.Wallet.Api.Types.SchemaMetadata ( TxMetadataSchema (..) , TxMetadataWithSchema (..) - , metadataPBKDF2Config - , toMetadataEncrypted ) import Cardano.Wallet.Api.Types.Transaction ( ApiAddress (..) @@ -172,6 +170,10 @@ import Cardano.Wallet.Primitive.Types.DRep import Cardano.Wallet.Primitive.Types.Hash ( Hash (..) ) +import Cardano.Wallet.Primitive.Types.MetadataEncryption + ( metadataPBKDF2Config + , toMetadataEncrypted + ) import Cardano.Wallet.Primitive.Types.RewardAccount ( RewardAccount (..) ) @@ -5535,7 +5537,7 @@ spec = describe "NEW_SHELLEY_TRANSACTIONS" $ do let decodePayloadEncrypted = Json (toJSON signedTx) let (Right expMetadataEncrypted) = - ApiT <$> toMetadataEncrypted pwd metadataToBeEncrypted (Just salt) + ApiT <$> toMetadataEncrypted pwd metadataRaw (Just salt) rDecodedTxEncrypted <- request @(ApiDecodedTransaction n) ctx (Link.decodeTransaction @'Shelley wa) Default decodePayloadEncrypted verify rDecodedTxEncrypted diff --git a/lib/primitive/cardano-wallet-primitive.cabal b/lib/primitive/cardano-wallet-primitive.cabal index 67d2f2cc3ad..65cfdc69ca9 100644 --- a/lib/primitive/cardano-wallet-primitive.cabal +++ b/lib/primitive/cardano-wallet-primitive.cabal @@ -40,13 +40,15 @@ library build-depends: , aeson , array + , attoparsec , base + , base16-bytestring , bech32 , bech32-th , binary , bytestring , cardano-addresses - , cardano-api + , cardano-api:{cardano-api, internal} , cardano-binary , cardano-crypto , cardano-crypto-class @@ -112,6 +114,7 @@ library , unliftio , unliftio-core , unordered-containers + , vector exposed-modules: Cardano.Wallet.Orphans @@ -165,6 +168,7 @@ library Cardano.Wallet.Primitive.Types.FeePolicy Cardano.Wallet.Primitive.Types.GenesisParameters Cardano.Wallet.Primitive.Types.Hash + Cardano.Wallet.Primitive.Types.MetadataEncryption Cardano.Wallet.Primitive.Types.NetworkParameters Cardano.Wallet.Primitive.Types.Pool Cardano.Wallet.Primitive.Types.ProtocolMagic diff --git a/lib/primitive/lib/Cardano/Wallet/Primitive/Types/MetadataEncryption.hs b/lib/primitive/lib/Cardano/Wallet/Primitive/Types/MetadataEncryption.hs new file mode 100644 index 00000000000..4f363b616c8 --- /dev/null +++ b/lib/primitive/lib/Cardano/Wallet/Primitive/Types/MetadataEncryption.hs @@ -0,0 +1,428 @@ +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE LambdaCase #-} + +{-# OPTIONS_GHC -Wno-orphans #-} + +module Cardano.Wallet.Primitive.Types.MetadataEncryption + ( metadataPBKDF2Config + , cip20MetadataKey + , cip83EncryptMethodKey + , cip83EncryptPayloadKey + , cip83EncryptPayloadValue + + , ErrMetadataDecryption (..) + , fromMetadataEncrypted + + , ErrMetadataEncryption (..) + , toMetadataEncrypted + ) +where + +import Prelude + +import Cardano.Api + ( TxMetadataJsonSchemaError (..) + , TxMetadataValue (..) + , TxMetadata (..) + , metadataValueToJsonNoSchema + ) +import Cardano.Api.Error + ( displayError + ) +import Control.Applicative + ( (<|>) + ) +import Control.Monad + ( guard + , when + ) +import Cryptography.Cipher.AES256CBC + ( CipherError + , CipherMode (..) + ) +import Cryptography.Hash.Core + ( SHA256 (..) + ) +import Cryptography.KDF.PBKDF2 + ( PBKDF2Config (..) + ) +import Data.Aeson + ( FromJSON (parseJSON) + , ToJSON (toJSON) + ) +import Data.Bifunctor + ( bimap + , first + ) +import Data.ByteArray.Encoding + ( Base (..) + , convertFromBase + , convertToBase + ) +import Data.ByteString + ( ByteString + ) +import Data.Maybe + ( isJust + , fromMaybe + , mapMaybe + ) +import Data.Text + ( Text + ) +import Data.Word + ( Word64 + ) + +import qualified Cardano.Ledger.Binary as CBOR +import qualified Cardano.Ledger.Shelley.TxAuxData as Ledger +import qualified Codec.CBOR.Magic as CBOR +import qualified Cryptography.Cipher.AES256CBC as AES256CBC +import qualified Cryptography.KDF.PBKDF2 as PBKDF2 +import qualified Data.Aeson as Aeson +import qualified Data.Aeson.Key as Aeson +import qualified Data.Aeson.KeyMap as Aeson +import qualified Data.Attoparsec.ByteString.Char8 as Atto +import qualified Data.ByteString as BS +import qualified Data.ByteString.Base16 as B16 +import qualified Data.ByteString.Char8 as B8 +import qualified Data.ByteString.Lazy as BL +import qualified Data.Foldable as F +import qualified Data.List as L +import qualified Data.Map.Strict as Map +import qualified Data.Scientific as Scientific +import qualified Data.Text as T +import qualified Data.Text.Encoding as T +import qualified Data.Vector as V + +-- CIP references: +-- https://github.com/cardano-foundation/CIPs/tree/master/CIP-0020 +-- https://github.com/cardano-foundation/CIPs/tree/master/CIP-0083 + +-- Metadata encryption/decryption config use in accordance to +-- CIP-83. +metadataPBKDF2Config :: PBKDF2Config SHA256 +metadataPBKDF2Config = PBKDF2Config + { hash = SHA256 + , iterations = 10000 + , keyLength = 32 + , ivLength = 16 + } + +-- A key that identifies transaction metadata, defined in CIP-20 and used by +-- CIP-83. +cip20MetadataKey :: Word64 +cip20MetadataKey = 674 + +cip83EncryptMethodKey :: Text +cip83EncryptMethodKey = "enc" + +cip83EncryptPayloadKey :: Text +cip83EncryptPayloadKey = "msg" + +cip83EncryptPayloadValue :: Text +cip83EncryptPayloadValue = "basic" + +data ErrMetadataEncryption = + ErrIncorrectRawMetadata + | ErrCannotEncryptMetadata CipherError + deriving (Show, Eq) + +-- When encryption is enabled we do the following: +-- (a) find field `msg` in the object of "674" label +-- (b) encrypt the 'msg' value if present, if there is neither "674" label +-- nor 'msg' value inside object of it emit error +-- (c) update value of `msg` with the encrypted initial value(s) encoded in +-- base64: +-- [TxMetaText base64_1, TxMetaText base64_2, ..., TxMetaText base64_n] +-- (d) add `enc` field with encryption method value 'basic' +toMetadataEncrypted + :: ByteString + -> TxMetadata + -> Maybe ByteString + -> Either ErrMetadataEncryption TxMetadata +toMetadataEncrypted pwd payload saltM = + fmap updateTxMetadata . encryptMessage =<< extractMessage + where + secretKey, iv :: ByteString + (secretKey, iv) = PBKDF2.generateKey metadataPBKDF2Config pwd saltM + + -- `msg` is embedded at the first level + parseMessage :: TxMetadataValue -> Maybe [TxMetadataValue] + parseMessage = \case + TxMetaMap kvs -> + case mapMaybe getValue kvs of + [ ] -> Nothing + vs -> Just vs + _ -> + Nothing + where + getValue :: (TxMetadataValue, TxMetadataValue) -> Maybe TxMetadataValue + getValue (TxMetaText k, v) = + if k == cip83EncryptPayloadKey then + Just v + else + Nothing + getValue _ = Nothing + + validKeyAndMessage :: Word64 -> TxMetadataValue -> Bool + validKeyAndMessage k v = k == cip20MetadataKey && isJust (parseMessage v) + + extractMessage :: Either ErrMetadataEncryption TxMetadataValue + extractMessage + | [v] <- F.toList filteredMap = + Right v + | otherwise = + Left ErrIncorrectRawMetadata + where + TxMetadata themap = payload + filteredMap = Map.filterWithKey validKeyAndMessage themap + + encryptMessage :: TxMetadataValue -> Either ErrMetadataEncryption TxMetadataValue + encryptMessage = \case + TxMetaMap pairs -> + TxMetaMap . reverse . L.nub . reverse . concat <$> + mapM encryptPairIfQualifies pairs + _ -> + error "encryptMessage should have TxMetaMap value" + where + encryptPairIfQualifies + :: (TxMetadataValue, TxMetadataValue) + -> Either ErrMetadataEncryption [(TxMetadataValue, TxMetadataValue)] + encryptPairIfQualifies = \case + (TxMetaText "msg", m) -> do + bimap ErrCannotEncryptMetadata toPair (encryptValue m) + pair -> + Right [pair] + + encryptValue :: TxMetadataValue -> Either CipherError ByteString + encryptValue + = AES256CBC.encrypt WithPadding secretKey iv saltM + . BL.toStrict + . Aeson.encode + . metadataValueToJsonNoSchema + + toPair :: ByteString -> [(TxMetadataValue, TxMetadataValue)] + toPair encryptedMessage = + [ (TxMetaText cip83EncryptPayloadKey, TxMetaList (toChunks encryptedMessage)) + , (TxMetaText cip83EncryptMethodKey, TxMetaText cip83EncryptPayloadValue) + ] + + toChunks :: ByteString -> [TxMetadataValue] + toChunks + = fmap TxMetaText + . T.chunksOf 64 + . T.decodeUtf8 + . convertToBase Base64 + + updateTxMetadata :: TxMetadataValue -> TxMetadata + updateTxMetadata v = TxMetadata (Map.insert cip20MetadataKey v themap) + where + TxMetadata themap = payload + +data ErrMetadataDecryption = + ErrMissingMetadataKey + | ErrMissingEncryptionMethod + | ErrMissingValidEncryptionPayload + | ErrCannotAesonDecodePayload Text + | ErrMissingSalt + | ErrCannotDecryptPayload CipherError + | ErrEncryptedPayloadWrongBase + deriving (Show, Eq) + +-- When decryption is enabled we do the following: +-- (a) retrieve TxMetaMap under proper key, ie.674, +-- cip20MetadataKey +-- (b) check if there is ("enc", "basic") pair +-- (c) recreate each encrypted payload from chunks that are expected under proper key, ie.msg, +-- cip83EncryptPayloadKey. So +-- expect TxMetaList [TxMetaText chunk1, ..., TxMetaText chunkN] +-- and construct payload=chunk1+chunk2+...+chunkN +-- (d) decrypt payload and decode metadata +-- (e) update structure under msg key and remove ("enc", "basic") pair +fromMetadataEncrypted + :: ByteString + -> TxMetadata + -> Either ErrMetadataDecryption TxMetadata +fromMetadataEncrypted pwd metadata = + composePayload metadata >>= + mapM decrypt >>= + adjust metadata + where + checkPresenceOfMethod value = + let presentPair (TxMetaText k, TxMetaText v) = + k == cip83EncryptMethodKey && v == cip83EncryptPayloadValue + presentPair _ = False + in case value of + TxMetaMap list -> not (any presentPair list) + _ -> True + getEncryptedPayload value = + let presentPair (TxMetaText k, TxMetaList _) = + k == cip83EncryptPayloadKey + presentPair _ = False + in case value of + TxMetaMap list -> snd <$> filter presentPair list + _ -> [] + extractTxt (TxMetaText txt) = txt + extractTxt _ = + error "TxMetaText is expected" + extractPayload (TxMetaList chunks)= + foldl T.append T.empty $ extractTxt <$> chunks + extractPayload _ = T.empty + composePayload (TxMetadata themap) = do + validValue <- case Map.lookup cip20MetadataKey themap of + Nothing -> Left ErrMissingMetadataKey + Just v -> pure v + when (checkPresenceOfMethod validValue) $ + Left ErrMissingEncryptionMethod + let payloads = getEncryptedPayload validValue + if null payloads then + Left ErrMissingValidEncryptionPayload + else do + let extracted = extractPayload <$> payloads + when (T.empty `elem` extracted) $ + Left ErrMissingValidEncryptionPayload + Right extracted + + decodeFromJSON = + first (ErrCannotAesonDecodePayload . T.pack) . + Aeson.eitherDecode . BL.fromStrict + decrypt payload = case convertFromBase Base64 (T.encodeUtf8 payload) of + Right payloadBS -> + case AES256CBC.getSaltFromEncrypted payloadBS of + Nothing -> Left ErrMissingSalt + Just salt -> do + let (secretKey, iv) = + PBKDF2.generateKey metadataPBKDF2Config pwd (Just salt) + decrypted <- bimap ErrCannotDecryptPayload fst + (AES256CBC.decrypt WithPadding secretKey iv payloadBS) + decodeFromJSON decrypted + Left _ -> + Left ErrEncryptedPayloadWrongBase + + adjust (TxMetadata metadata') decodedElems = + pure $ TxMetadata $ + Map.adjust updateMetaMap cip20MetadataKey metadata' + where + updateElem acc@(decryptedList, list) elem' = case elem' of + (TxMetaText k, TxMetaText v) -> + if k == cip83EncryptMethodKey && v == cip83EncryptPayloadValue then + -- omiting this element + acc + else + (decryptedList, list ++ [elem']) + (TxMetaText k, v) -> case decryptedList of + toAdd : rest -> + if k == cip83EncryptPayloadKey then + (rest, list ++ [(TxMetaText k, toAdd)] ) + else + (decryptedList, list ++ [(TxMetaText k, v)] ) + _ -> error "we have checked already in composePayload that there is enough elements in decrypedList" + _ -> error "we have checked already in composePayload that there is pair (TxMetaText, something)" + + updateMetaMap v = case v of + TxMetaMap list -> + TxMetaMap $ snd $ L.foldl updateElem (decodedElems,[]) list + _ -> error "we have checked already in composePayload that there is TxMetaMap" + +instance ToJSON TxMetadataValue where + toJSON = metadataValueToJsonNoSchema + +instance FromJSON TxMetadataValue where + parseJSON = either (fail . displayError) pure . metadataValueFromJsonNoSchema + +-- when cardano-api exports metadataValueFromJsonNoSchema the below could be removed (together with cabal dependencies) +metadataValueFromJsonNoSchema + :: Aeson.Value + -> Either TxMetadataJsonSchemaError TxMetadataValue +metadataValueFromJsonNoSchema = conv + where + conv :: Aeson.Value -> Either TxMetadataJsonSchemaError TxMetadataValue + conv Aeson.Null = Left TxMetadataJsonNullNotAllowed + conv Aeson.Bool{} = Left TxMetadataJsonBoolNotAllowed + + conv (Aeson.Number d) = + case Scientific.floatingOrInteger d :: Either Double Integer of + Left n -> Left (TxMetadataJsonNumberNotInteger n) + Right n -> Right (TxMetaNumber n) + + conv (Aeson.String s) + | Just s' <- T.stripPrefix bytesPrefix s + , let bs' = T.encodeUtf8 s' + , Right bs <- B16.decode bs' + , not (B8.any (\c -> c >= 'A' && c <= 'F') bs') + = Right (TxMetaBytes bs) + + conv (Aeson.String s) = Right (TxMetaText s) + + conv (Aeson.Array vs) = + fmap TxMetaList + . traverse conv + $ V.toList vs + + conv (Aeson.Object kvs) = + fmap + ( TxMetaMap + . sortCanonicalForCbor + ) + . traverse (\(k,v) -> (,) (convKey k) <$> conv v) + . fmap (first Aeson.toText) + $ Aeson.toList kvs + + convKey :: Text -> TxMetadataValue + convKey s = + fromMaybe (TxMetaText s) $ + parseAll ((fmap TxMetaNumber pSigned <* Atto.endOfInput) + <|> (fmap TxMetaBytes pBytes <* Atto.endOfInput)) s + +bytesPrefix :: Text +bytesPrefix = "0x" + +parseAll :: Atto.Parser a -> Text -> Maybe a +parseAll p = + either (const Nothing) Just + . Atto.parseOnly p + . T.encodeUtf8 + +pUnsigned :: Atto.Parser Integer +pUnsigned = do + bs <- Atto.takeWhile1 Atto.isDigit + -- no redundant leading 0s allowed, or we cannot round-trip properly + guard (not (BS.length bs > 1 && B8.head bs == '0')) + return $! BS.foldl' step 0 bs + where + step a w = a * 10 + fromIntegral (w - 48) + +pSigned :: Atto.Parser Integer +pSigned = Atto.signed pUnsigned + +pBytes :: Atto.Parser ByteString +pBytes = do + _ <- Atto.string "0x" + remaining <- Atto.takeByteString + when (B8.any hexUpper remaining) $ fail ("Unexpected uppercase hex characters in " <> show remaining) + case B16.decode remaining of + Right bs -> return bs + _ -> fail ("Expecting base16 encoded string, found: " <> show remaining) + where + hexUpper c = c >= 'A' && c <= 'F' + +sortCanonicalForCbor + :: [(TxMetadataValue, TxMetadataValue)] -> [(TxMetadataValue, TxMetadataValue)] +sortCanonicalForCbor = + map snd + . L.sortOn fst + . map (\e@(k, _) -> (CBOR.uintegerFromBytes $ serialiseKey k, e)) + where + serialiseKey = CBOR.serialize' CBOR.shelleyProtVer . toShelleyMetadatum + +toShelleyMetadatum :: TxMetadataValue -> Ledger.Metadatum +toShelleyMetadatum (TxMetaNumber x) = Ledger.I x +toShelleyMetadatum (TxMetaBytes x) = Ledger.B x +toShelleyMetadatum (TxMetaText x) = Ledger.S x +toShelleyMetadatum (TxMetaList xs) = + Ledger.List [ toShelleyMetadatum x | x <- xs ] +toShelleyMetadatum (TxMetaMap xs) = + Ledger.Map [ (toShelleyMetadatum k, + toShelleyMetadatum v) + | (k,v) <- xs ] diff --git a/lib/unit/test/unit/Cardano/Wallet/Api/TypesSpec.hs b/lib/unit/test/unit/Cardano/Wallet/Api/TypesSpec.hs index 381951ec206..0174aed814c 100644 --- a/lib/unit/test/unit/Cardano/Wallet/Api/TypesSpec.hs +++ b/lib/unit/test/unit/Cardano/Wallet/Api/TypesSpec.hs @@ -304,7 +304,6 @@ import Cardano.Wallet.Api.Types.RestorationMode import Cardano.Wallet.Api.Types.SchemaMetadata ( TxMetadataSchema (..) , TxMetadataWithSchema (..) - , toMetadataEncrypted ) import Cardano.Wallet.Api.Types.Transaction ( ApiAddress (..) @@ -403,6 +402,10 @@ import Cardano.Wallet.Primitive.Types.DRep import Cardano.Wallet.Primitive.Types.Hash ( Hash (..) ) +import Cardano.Wallet.Primitive.Types.MetadataEncryption + ( ErrMetadataEncryption (..) + , toMetadataEncrypted + ) import Cardano.Wallet.Primitive.Types.RewardAccount ( RewardAccount (..) ) @@ -1224,7 +1227,6 @@ spec = do -- vBSywXY+WGcrckHUCyjJcQ== it "short msg - no salt" $ do let schemaBefore = - TxMetadataWithSchema TxMetadataNoSchema $ Cardano.TxMetadata $ Map.fromList [ ( 674 @@ -1264,7 +1266,6 @@ spec = do -- ygjbu25gCdhJh7iEpAJVaA== it "long msg - no salt" $ do let schemaBefore = - TxMetadataWithSchema TxMetadataNoSchema $ Cardano.TxMetadata $ Map.fromList [ ( 674 @@ -1308,7 +1309,6 @@ spec = do -- 7jFsGUK1bCdwsrn8kqI92NccbG8oAtPJUktZTTcO/bg= it "cip msg - no salt" $ do let schemaBefore = - TxMetadataWithSchema TxMetadataNoSchema $ Cardano.TxMetadata $ Map.fromList [ ( 674 @@ -1353,7 +1353,6 @@ spec = do -- U2FsdGVkX18wMDAwMDAwMF0ea/2sHeptB3SvZtgc600= it "short msg - salted" $ do let schemaBefore = - TxMetadataWithSchema TxMetadataNoSchema $ Cardano.TxMetadata $ Map.fromList [ ( 674 , Cardano.TxMetaMap @@ -1394,7 +1393,6 @@ spec = do -- PbR/qyT2q0tejHQmsHdORif5rvZYTzJGsTutA0RIcFU= it "long msg - salted" $ do let schemaBefore = - TxMetadataWithSchema TxMetadataNoSchema $ Cardano.TxMetadata $ Map.fromList [ ( 674 , Cardano.TxMetaMap @@ -1436,7 +1434,6 @@ spec = do -- +SIXXn04a9xkoFHk4ZH281nIfH5lpClsO16p2vRpSsdBDFO78aTPX3bsHsRE0L2A it "cip msg - salted" $ do let schemaBefore = - TxMetadataWithSchema TxMetadataNoSchema $ Cardano.TxMetadata $ Map.fromList [ ( 674 @@ -1480,7 +1477,6 @@ spec = do it "msg wrong label - no salt" $ do let schemaBefore = - TxMetadataWithSchema TxMetadataNoSchema $ Cardano.TxMetadata $ Map.fromList [ ( 675 , Cardano.TxMetaMap @@ -1498,11 +1494,10 @@ spec = do ) ] toMetadataEncrypted "cardano" schemaBefore Nothing - `shouldBe` Left ErrConstructTxIncorrectRawMetadata + `shouldBe` Left ErrIncorrectRawMetadata it "msg without 'msg field' - no salt" $ do let schemaBefore = - TxMetadataWithSchema TxMetadataNoSchema $ Cardano.TxMetadata $ Map.fromList [ ( 674 @@ -1521,7 +1516,7 @@ spec = do ) ] toMetadataEncrypted "cardano" schemaBefore Nothing - `shouldBe` Left ErrConstructTxIncorrectRawMetadata + `shouldBe` Left ErrIncorrectRawMetadata fromHexToM :: Text -> Maybe ByteString fromHexToM = rightToMaybe . convertFromBase Base16 . T.encodeUtf8 diff --git a/lib/wallet/src/Cardano/Wallet.hs b/lib/wallet/src/Cardano/Wallet.hs index fe95c7c5dea..e2b53219c1a 100644 --- a/lib/wallet/src/Cardano/Wallet.hs +++ b/lib/wallet/src/Cardano/Wallet.hs @@ -551,6 +551,10 @@ import Cardano.Wallet.Primitive.Types.DRep import Cardano.Wallet.Primitive.Types.Hash ( Hash (..) ) +import Cardano.Wallet.Primitive.Types.MetadataEncryption + ( ErrMetadataDecryption + , ErrMetadataEncryption + ) import Cardano.Wallet.Primitive.Types.Range ( Range (..) ) @@ -859,7 +863,6 @@ import qualified Cardano.Wallet.Primitive.Types.Tx.TxOut as TxOut import qualified Cardano.Wallet.Primitive.Types.UTxO as UTxO import qualified Cardano.Wallet.Primitive.Types.UTxOStatistics as UTxOStatistics import qualified Cardano.Wallet.Read as Read -import qualified Cryptography.Cipher.AES256CBC as AES256CBC import qualified Data.ByteArray as BA import qualified Data.Delta.Update as Delta import qualified Data.Foldable as F @@ -3755,8 +3758,7 @@ data ErrConstructTx | ErrConstructTxDelegationInvalid | ErrConstructTxVotingInWrongEra | ErrConstructTxWithdrawalWithoutVoting - | ErrConstructTxIncorrectRawMetadata - | ErrConstructTxEncryptMetadata AES256CBC.CipherError + | ErrConstructTxFromMetadataEncryption ErrMetadataEncryption | ErrConstructTxNotImplemented deriving (Show, Eq) @@ -3767,14 +3769,8 @@ data ErrGetPolicyId deriving (Show, Eq) -- | Errors that can occur when decoding a transaction. -data ErrDecodeTx - = ErrDecodeTxMissingMetadataKey - | ErrDecodeTxMissingEncryptionMethod - | ErrDecodeTxMissingValidEncryptionPayload - | ErrDecodeTxDecryptedPayload Text - | ErrDecodeTxMissingSalt - | ErrDecodeTxDecryptPayload AES256CBC.CipherError - | ErrDecodeTxEncryptedPayloadWrongBase +newtype ErrDecodeTx + = ErrDecodeTxFromMetadataDecryption ErrMetadataDecryption deriving (Show, Eq) -- | Errors that can occur when signing a transaction. From 2432ab9ff7cb0d78025e2b172b204175c576620d Mon Sep 17 00:00:00 2001 From: Pawel Jakubas Date: Fri, 26 Jul 2024 17:26:33 +0200 Subject: [PATCH 18/26] add MetadataEncryptionSpec --- lib/primitive/cardano-wallet-primitive.cabal | 3 + .../Primitive/Types/MetadataEncryptionSpec.hs | 339 ++++++++++++++++++ .../test/unit/Cardano/Wallet/Api/TypesSpec.hs | 303 ---------------- 3 files changed, 342 insertions(+), 303 deletions(-) create mode 100644 lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs diff --git a/lib/primitive/cardano-wallet-primitive.cabal b/lib/primitive/cardano-wallet-primitive.cabal index 65cfdc69ca9..10f6d2fafd0 100644 --- a/lib/primitive/cardano-wallet-primitive.cabal +++ b/lib/primitive/cardano-wallet-primitive.cabal @@ -244,6 +244,7 @@ test-suite test , containers , deepseq , delta-types + , either , filepath , fmt , generic-arbitrary @@ -252,6 +253,7 @@ test-suite test , hspec-core , iohk-monitoring , lens + , memory , ouroboros-consensus , ouroboros-consensus-cardano , ouroboros-network-api @@ -280,6 +282,7 @@ test-suite test Cardano.Wallet.Primitive.Types.BlockSummarySpec Cardano.Wallet.Primitive.Types.CoinSpec Cardano.Wallet.Primitive.Types.HashSpec + Cardano.Wallet.Primitive.Types.MetadataEncryptionSpec Cardano.Wallet.Primitive.Types.PoolSpec Cardano.Wallet.Primitive.Types.RangeSpec Cardano.Wallet.Primitive.Types.TokenBundleSpec diff --git a/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs b/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs new file mode 100644 index 00000000000..839e5b43c5e --- /dev/null +++ b/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs @@ -0,0 +1,339 @@ +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE RecordWildCards #-} + +module Cardano.Wallet.Primitive.Types.MetadataEncryptionSpec + ( spec + ) where + +import Prelude + +import Cardano.Wallet.Primitive.Types.MetadataEncryption + ( ErrMetadataDecryption (..) + , ErrMetadataEncryption (..) + , fromMetadataEncrypted + , toMetadataEncrypted + ) +import Data.ByteArray.Encoding + ( Base (..) + , convertFromBase + ) +import Data.ByteString + ( ByteString + ) +import Data.Either.Combinators + ( rightToMaybe + ) +import Data.Text + ( Text + ) +import Test.Hspec + ( Spec + , describe + , it + , shouldBe + ) + +import qualified Cardano.Api as Cardano +import qualified Data.Map.Strict as Map +import qualified Data.Text.Encoding as T + +spec :: Spec +spec = do + describe "toMetadataEncrypted openssl goldens" $ do + -- $ echo -n '"secret data"' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -nosalt + -- vBSywXY+WGcrckHUCyjJcQ== + it "short msg - no salt" $ do + let schemaBefore = + Cardano.TxMetadata $ + Map.fromList + [ ( 674 + , Cardano.TxMetaMap + [ ( Cardano.TxMetaText "field" + , Cardano.TxMetaNumber 123 + ) + , ( Cardano.TxMetaText "msg" + , Cardano.TxMetaText "secret data" + ) + ] + ) + ] + schemaAfter = + Cardano.TxMetadata $ + Map.fromList + [ ( 674 + , Cardano.TxMetaMap + [ ( Cardano.TxMetaText "field" + , Cardano.TxMetaNumber 123 + ) + , ( Cardano.TxMetaText "msg" + , Cardano.TxMetaList + [Cardano.TxMetaText "vBSywXY+WGcrckHUCyjJcQ=="] + ) + , ( Cardano.TxMetaText "enc" + , Cardano.TxMetaText "basic" + ) + ] + ) + ] + toMetadataEncrypted "cardano" schemaBefore Nothing + `shouldBe` Right schemaAfter + + -- $ echo -n '"secret data that is long enough to produce more than 64 bytes"' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -nosalt + -- OLSOdRF+P56rW9gUopHcs0HHcdmPP5ujhSuB+r84VJgvsMOsqmIZx2etosnkyOc8 + -- ygjbu25gCdhJh7iEpAJVaA== + it "long msg - no salt" $ do + let schemaBefore = + Cardano.TxMetadata $ + Map.fromList + [ ( 674 + , Cardano.TxMetaMap + [ ( Cardano.TxMetaText "field" + , Cardano.TxMetaNumber 123 + ) + , ( Cardano.TxMetaText "msg" + , Cardano.TxMetaText + "secret data that is long enough to produce more \ + \than 64 bytes" + ) + ] + ) + ] + schemaAfter = + Cardano.TxMetadata $ + Map.fromList + [ ( 674 + , Cardano.TxMetaMap + [ ( Cardano.TxMetaText "field" + , Cardano.TxMetaNumber 123 + ) + , ( Cardano.TxMetaText "msg" + , Cardano.TxMetaList + [ Cardano.TxMetaText "OLSOdRF+P56rW9gUopHcs0HHcdmPP5ujhSuB+r84VJgvsMOsqmIZx2etosnkyOc8" + , Cardano.TxMetaText "ygjbu25gCdhJh7iEpAJVaA==" + ] + ) + , ( Cardano.TxMetaText "enc" + , Cardano.TxMetaText "basic" + ) + ] + ) + ] + toMetadataEncrypted "cardano" schemaBefore Nothing + `shouldBe` Right schemaAfter + + -- $ echo -n '["Invoice-No: 123456789","Order-No: 7654321","Email: john@doe.com"]' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -nosalt + -- IBcjjGQ7akr/CV2Zb0HtCvEPQNndZujCZ7iaFGMjOX3q3PJg5aRUvHgO3gPnDzYE + -- 7jFsGUK1bCdwsrn8kqI92NccbG8oAtPJUktZTTcO/bg= + it "cip msg - no salt" $ do + let schemaBefore = + Cardano.TxMetadata $ + Map.fromList + [ ( 674 + , Cardano.TxMetaMap + [ ( Cardano.TxMetaText "field" + , Cardano.TxMetaNumber 123 + ) + , ( Cardano.TxMetaText "msg" + , Cardano.TxMetaList + [ Cardano.TxMetaText "Invoice-No: 123456789" + , Cardano.TxMetaText "Order-No: 7654321" + , Cardano.TxMetaText "Email: john@doe.com" + ] + ) + ] + ) + ] + schemaAfter = + Cardano.TxMetadata $ + Map.fromList + [ ( 674 + , Cardano.TxMetaMap + [ ( Cardano.TxMetaText "field" + , Cardano.TxMetaNumber 123 + ) + , ( Cardano.TxMetaText "msg" + , Cardano.TxMetaList + [ Cardano.TxMetaText "IBcjjGQ7akr/CV2Zb0HtCvEPQNndZujCZ7iaFGMjOX3q3PJg5aRUvHgO3gPnDzYE" + , Cardano.TxMetaText "7jFsGUK1bCdwsrn8kqI92NccbG8oAtPJUktZTTcO/bg=" + ] + ) + , ( Cardano.TxMetaText "enc" + , Cardano.TxMetaText "basic" + ) + ] + ) + ] + toMetadataEncrypted "cardano" schemaBefore Nothing + `shouldBe` Right schemaAfter + + -- $ $ echo -n '"secret data"' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -S 3030303030303030 + -- U2FsdGVkX18wMDAwMDAwMF0ea/2sHeptB3SvZtgc600= + it "short msg - salted" $ do + let schemaBefore = + Cardano.TxMetadata $ Map.fromList + [ ( 674 + , Cardano.TxMetaMap + [ ( Cardano.TxMetaText "field" + , Cardano.TxMetaNumber 123 + ) + , ( Cardano.TxMetaText "msg" + , Cardano.TxMetaText "secret data" + ) + ] + ) + ] + schemaAfter = + Cardano.TxMetadata $ + Map.fromList + [ ( 674 + , Cardano.TxMetaMap + [ ( Cardano.TxMetaText "field" + , Cardano.TxMetaNumber 123 + ) + , ( Cardano.TxMetaText "msg" + , Cardano.TxMetaList + [ Cardano.TxMetaText "U2FsdGVkX18wMDAwMDAwMF0ea/2sHeptB3SvZtgc600=" + ] + ) + , ( Cardano.TxMetaText "enc" + , Cardano.TxMetaText "basic" + ) + ] + ) + ] + saltM = fromHexToM "3030303030303030" + toMetadataEncrypted "cardano" schemaBefore saltM + `shouldBe` Right schemaAfter + + -- $ echo -n '"secret data that is long enough to produce more than 64 bytes"' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -S 3030303030303030 + -- U2FsdGVkX18wMDAwMDAwMPNdhZQON/Hlwqvk4+sNRCa90QrAVpIGUlWgZhgNlwKh + -- PbR/qyT2q0tejHQmsHdORif5rvZYTzJGsTutA0RIcFU= + it "long msg - salted" $ do + let schemaBefore = + Cardano.TxMetadata $ Map.fromList + [ ( 674 + , Cardano.TxMetaMap + [ ( Cardano.TxMetaText "field" + , Cardano.TxMetaNumber 123 + ) + , ( Cardano.TxMetaText "msg" + , Cardano.TxMetaText "secret data that is long enough to produce more than 64 bytes" + ) + ] + ) + ] + schemaAfter = + Cardano.TxMetadata $ + Map.fromList + [ ( 674 + , Cardano.TxMetaMap + [ ( Cardano.TxMetaText "field" + , Cardano.TxMetaNumber 123 + ) + , ( Cardano.TxMetaText "msg" + , Cardano.TxMetaList + [ Cardano.TxMetaText "U2FsdGVkX18wMDAwMDAwMPNdhZQON/Hlwqvk4+sNRCa90QrAVpIGUlWgZhgNlwKh" + , Cardano.TxMetaText "PbR/qyT2q0tejHQmsHdORif5rvZYTzJGsTutA0RIcFU=" + ] + ) + , ( Cardano.TxMetaText "enc" + , Cardano.TxMetaText "basic" + ) + ] + ) + ] + saltM = fromHexToM "3030303030303030" + toMetadataEncrypted "cardano" schemaBefore saltM + `shouldBe` Right schemaAfter + + -- $ $ echo -n '["Invoice-No: 123456789","Order-No: 7654321","Email: john@doe.com"]' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -S 3030303030303030 + -- U2FsdGVkX18wMDAwMDAwMFlOS4b0tXrZA7U5aQaHeI/sP74h84EPEjGv0wl4D8Do + -- +SIXXn04a9xkoFHk4ZH281nIfH5lpClsO16p2vRpSsdBDFO78aTPX3bsHsRE0L2A + it "cip msg - salted" $ do + let schemaBefore = + Cardano.TxMetadata $ + Map.fromList + [ ( 674 + , Cardano.TxMetaMap + [ ( Cardano.TxMetaText "field" + , Cardano.TxMetaNumber 123 + ) + , ( Cardano.TxMetaText "msg" + , Cardano.TxMetaList + [ Cardano.TxMetaText "Invoice-No: 123456789" + , Cardano.TxMetaText "Order-No: 7654321" + , Cardano.TxMetaText "Email: john@doe.com" + ] + ) + ] + ) + ] + schemaAfter = + Cardano.TxMetadata $ + Map.fromList + [ ( 674 + , Cardano.TxMetaMap + [ ( Cardano.TxMetaText "field" + , Cardano.TxMetaNumber 123 + ) + , ( Cardano.TxMetaText "msg" + , Cardano.TxMetaList + [ Cardano.TxMetaText "U2FsdGVkX18wMDAwMDAwMFlOS4b0tXrZA7U5aQaHeI/sP74h84EPEjGv0wl4D8Do" + , Cardano.TxMetaText "+SIXXn04a9xkoFHk4ZH281nIfH5lpClsO16p2vRpSsdBDFO78aTPX3bsHsRE0L2A" + ] + ) + , ( Cardano.TxMetaText "enc" + , Cardano.TxMetaText "basic" + ) + ] + ) + ] + saltM = fromHexToM "3030303030303030" + toMetadataEncrypted "cardano" schemaBefore saltM + `shouldBe` Right schemaAfter + + it "msg wrong label - no salt" $ do + let schemaBefore = + Cardano.TxMetadata $ Map.fromList + [ ( 675 + , Cardano.TxMetaMap + [ ( Cardano.TxMetaText "field" + , Cardano.TxMetaNumber 123 + ) + , ( Cardano.TxMetaText "msg" + , Cardano.TxMetaList + [ Cardano.TxMetaText "Invoice-No: 123456789" + , Cardano.TxMetaText "Order-No: 7654321" + , Cardano.TxMetaText "Email: john@doe.com" + ] + ) + ] + ) + ] + toMetadataEncrypted "cardano" schemaBefore Nothing + `shouldBe` Left ErrIncorrectRawMetadata + + it "msg without 'msg field' - no salt" $ do + let schemaBefore = + Cardano.TxMetadata $ + Map.fromList + [ ( 674 + , Cardano.TxMetaMap + [ ( Cardano.TxMetaText "field" + , Cardano.TxMetaNumber 123 + ) + , ( Cardano.TxMetaText "msgs" + , Cardano.TxMetaList + [ Cardano.TxMetaText "Invoice-No: 123456789" + , Cardano.TxMetaText "Order-No: 7654321" + , Cardano.TxMetaText "Email: john@doe.com" + ] + ) + ] + ) + ] + toMetadataEncrypted "cardano" schemaBefore Nothing + `shouldBe` Left ErrIncorrectRawMetadata + +fromHexToM :: Text -> Maybe ByteString +fromHexToM = rightToMaybe . convertFromBase Base16 . T.encodeUtf8 diff --git a/lib/unit/test/unit/Cardano/Wallet/Api/TypesSpec.hs b/lib/unit/test/unit/Cardano/Wallet/Api/TypesSpec.hs index 0174aed814c..90e9b4bd64e 100644 --- a/lib/unit/test/unit/Cardano/Wallet/Api/TypesSpec.hs +++ b/lib/unit/test/unit/Cardano/Wallet/Api/TypesSpec.hs @@ -402,10 +402,6 @@ import Cardano.Wallet.Primitive.Types.DRep import Cardano.Wallet.Primitive.Types.Hash ( Hash (..) ) -import Cardano.Wallet.Primitive.Types.MetadataEncryption - ( ErrMetadataEncryption (..) - , toMetadataEncrypted - ) import Cardano.Wallet.Primitive.Types.RewardAccount ( RewardAccount (..) ) @@ -1222,305 +1218,6 @@ spec = do _ -> "" in counterexample errStr $ res == Success SchemaApiErrorInfo - describe "toMetadataEncrypted openssl goldens" $ do - -- $ echo -n '"secret data"' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -nosalt - -- vBSywXY+WGcrckHUCyjJcQ== - it "short msg - no salt" $ do - let schemaBefore = - Cardano.TxMetadata $ - Map.fromList - [ ( 674 - , Cardano.TxMetaMap - [ ( Cardano.TxMetaText "field" - , Cardano.TxMetaNumber 123 - ) - , ( Cardano.TxMetaText "msg" - , Cardano.TxMetaText "secret data" - ) - ] - ) - ] - schemaAfter = - Cardano.TxMetadata $ - Map.fromList - [ ( 674 - , Cardano.TxMetaMap - [ ( Cardano.TxMetaText "field" - , Cardano.TxMetaNumber 123 - ) - , ( Cardano.TxMetaText "msg" - , Cardano.TxMetaList - [Cardano.TxMetaText "vBSywXY+WGcrckHUCyjJcQ=="] - ) - , ( Cardano.TxMetaText "enc" - , Cardano.TxMetaText "basic" - ) - ] - ) - ] - toMetadataEncrypted "cardano" schemaBefore Nothing - `shouldBe` Right schemaAfter - - -- $ echo -n '"secret data that is long enough to produce more than 64 bytes"' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -nosalt - -- OLSOdRF+P56rW9gUopHcs0HHcdmPP5ujhSuB+r84VJgvsMOsqmIZx2etosnkyOc8 - -- ygjbu25gCdhJh7iEpAJVaA== - it "long msg - no salt" $ do - let schemaBefore = - Cardano.TxMetadata $ - Map.fromList - [ ( 674 - , Cardano.TxMetaMap - [ ( Cardano.TxMetaText "field" - , Cardano.TxMetaNumber 123 - ) - , ( Cardano.TxMetaText "msg" - , Cardano.TxMetaText - "secret data that is long enough to produce more \ - \than 64 bytes" - ) - ] - ) - ] - schemaAfter = - Cardano.TxMetadata $ - Map.fromList - [ ( 674 - , Cardano.TxMetaMap - [ ( Cardano.TxMetaText "field" - , Cardano.TxMetaNumber 123 - ) - , ( Cardano.TxMetaText "msg" - , Cardano.TxMetaList - [ Cardano.TxMetaText "OLSOdRF+P56rW9gUopHcs0HHcdmPP5ujhSuB+r84VJgvsMOsqmIZx2etosnkyOc8" - , Cardano.TxMetaText "ygjbu25gCdhJh7iEpAJVaA==" - ] - ) - , ( Cardano.TxMetaText "enc" - , Cardano.TxMetaText "basic" - ) - ] - ) - ] - toMetadataEncrypted "cardano" schemaBefore Nothing - `shouldBe` Right schemaAfter - - -- $ echo -n '["Invoice-No: 123456789","Order-No: 7654321","Email: john@doe.com"]' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -nosalt - -- IBcjjGQ7akr/CV2Zb0HtCvEPQNndZujCZ7iaFGMjOX3q3PJg5aRUvHgO3gPnDzYE - -- 7jFsGUK1bCdwsrn8kqI92NccbG8oAtPJUktZTTcO/bg= - it "cip msg - no salt" $ do - let schemaBefore = - Cardano.TxMetadata $ - Map.fromList - [ ( 674 - , Cardano.TxMetaMap - [ ( Cardano.TxMetaText "field" - , Cardano.TxMetaNumber 123 - ) - , ( Cardano.TxMetaText "msg" - , Cardano.TxMetaList - [ Cardano.TxMetaText "Invoice-No: 123456789" - , Cardano.TxMetaText "Order-No: 7654321" - , Cardano.TxMetaText "Email: john@doe.com" - ] - ) - ] - ) - ] - schemaAfter = - Cardano.TxMetadata $ - Map.fromList - [ ( 674 - , Cardano.TxMetaMap - [ ( Cardano.TxMetaText "field" - , Cardano.TxMetaNumber 123 - ) - , ( Cardano.TxMetaText "msg" - , Cardano.TxMetaList - [ Cardano.TxMetaText "IBcjjGQ7akr/CV2Zb0HtCvEPQNndZujCZ7iaFGMjOX3q3PJg5aRUvHgO3gPnDzYE" - , Cardano.TxMetaText "7jFsGUK1bCdwsrn8kqI92NccbG8oAtPJUktZTTcO/bg=" - ] - ) - , ( Cardano.TxMetaText "enc" - , Cardano.TxMetaText "basic" - ) - ] - ) - ] - toMetadataEncrypted "cardano" schemaBefore Nothing - `shouldBe` Right schemaAfter - - -- $ $ echo -n '"secret data"' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -S 3030303030303030 - -- U2FsdGVkX18wMDAwMDAwMF0ea/2sHeptB3SvZtgc600= - it "short msg - salted" $ do - let schemaBefore = - Cardano.TxMetadata $ Map.fromList - [ ( 674 - , Cardano.TxMetaMap - [ ( Cardano.TxMetaText "field" - , Cardano.TxMetaNumber 123 - ) - , ( Cardano.TxMetaText "msg" - , Cardano.TxMetaText "secret data" - ) - ] - ) - ] - schemaAfter = - Cardano.TxMetadata $ - Map.fromList - [ ( 674 - , Cardano.TxMetaMap - [ ( Cardano.TxMetaText "field" - , Cardano.TxMetaNumber 123 - ) - , ( Cardano.TxMetaText "msg" - , Cardano.TxMetaList - [ Cardano.TxMetaText "U2FsdGVkX18wMDAwMDAwMF0ea/2sHeptB3SvZtgc600=" - ] - ) - , ( Cardano.TxMetaText "enc" - , Cardano.TxMetaText "basic" - ) - ] - ) - ] - saltM = fromHexToM "3030303030303030" - toMetadataEncrypted "cardano" schemaBefore saltM - `shouldBe` Right schemaAfter - - -- $ echo -n '"secret data that is long enough to produce more than 64 bytes"' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -S 3030303030303030 - -- U2FsdGVkX18wMDAwMDAwMPNdhZQON/Hlwqvk4+sNRCa90QrAVpIGUlWgZhgNlwKh - -- PbR/qyT2q0tejHQmsHdORif5rvZYTzJGsTutA0RIcFU= - it "long msg - salted" $ do - let schemaBefore = - Cardano.TxMetadata $ Map.fromList - [ ( 674 - , Cardano.TxMetaMap - [ ( Cardano.TxMetaText "field" - , Cardano.TxMetaNumber 123 - ) - , ( Cardano.TxMetaText "msg" - , Cardano.TxMetaText "secret data that is long enough to produce more than 64 bytes" - ) - ] - ) - ] - schemaAfter = - Cardano.TxMetadata $ - Map.fromList - [ ( 674 - , Cardano.TxMetaMap - [ ( Cardano.TxMetaText "field" - , Cardano.TxMetaNumber 123 - ) - , ( Cardano.TxMetaText "msg" - , Cardano.TxMetaList - [ Cardano.TxMetaText "U2FsdGVkX18wMDAwMDAwMPNdhZQON/Hlwqvk4+sNRCa90QrAVpIGUlWgZhgNlwKh" - , Cardano.TxMetaText "PbR/qyT2q0tejHQmsHdORif5rvZYTzJGsTutA0RIcFU=" - ] - ) - , ( Cardano.TxMetaText "enc" - , Cardano.TxMetaText "basic" - ) - ] - ) - ] - saltM = fromHexToM "3030303030303030" - toMetadataEncrypted "cardano" schemaBefore saltM - `shouldBe` Right schemaAfter - - -- $ $ echo -n '["Invoice-No: 123456789","Order-No: 7654321","Email: john@doe.com"]' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -S 3030303030303030 - -- U2FsdGVkX18wMDAwMDAwMFlOS4b0tXrZA7U5aQaHeI/sP74h84EPEjGv0wl4D8Do - -- +SIXXn04a9xkoFHk4ZH281nIfH5lpClsO16p2vRpSsdBDFO78aTPX3bsHsRE0L2A - it "cip msg - salted" $ do - let schemaBefore = - Cardano.TxMetadata $ - Map.fromList - [ ( 674 - , Cardano.TxMetaMap - [ ( Cardano.TxMetaText "field" - , Cardano.TxMetaNumber 123 - ) - , ( Cardano.TxMetaText "msg" - , Cardano.TxMetaList - [ Cardano.TxMetaText "Invoice-No: 123456789" - , Cardano.TxMetaText "Order-No: 7654321" - , Cardano.TxMetaText "Email: john@doe.com" - ] - ) - ] - ) - ] - schemaAfter = - Cardano.TxMetadata $ - Map.fromList - [ ( 674 - , Cardano.TxMetaMap - [ ( Cardano.TxMetaText "field" - , Cardano.TxMetaNumber 123 - ) - , ( Cardano.TxMetaText "msg" - , Cardano.TxMetaList - [ Cardano.TxMetaText "U2FsdGVkX18wMDAwMDAwMFlOS4b0tXrZA7U5aQaHeI/sP74h84EPEjGv0wl4D8Do" - , Cardano.TxMetaText "+SIXXn04a9xkoFHk4ZH281nIfH5lpClsO16p2vRpSsdBDFO78aTPX3bsHsRE0L2A" - ] - ) - , ( Cardano.TxMetaText "enc" - , Cardano.TxMetaText "basic" - ) - ] - ) - ] - saltM = fromHexToM "3030303030303030" - toMetadataEncrypted "cardano" schemaBefore saltM - `shouldBe` Right schemaAfter - - it "msg wrong label - no salt" $ do - let schemaBefore = - Cardano.TxMetadata $ Map.fromList - [ ( 675 - , Cardano.TxMetaMap - [ ( Cardano.TxMetaText "field" - , Cardano.TxMetaNumber 123 - ) - , ( Cardano.TxMetaText "msg" - , Cardano.TxMetaList - [ Cardano.TxMetaText "Invoice-No: 123456789" - , Cardano.TxMetaText "Order-No: 7654321" - , Cardano.TxMetaText "Email: john@doe.com" - ] - ) - ] - ) - ] - toMetadataEncrypted "cardano" schemaBefore Nothing - `shouldBe` Left ErrIncorrectRawMetadata - - it "msg without 'msg field' - no salt" $ do - let schemaBefore = - Cardano.TxMetadata $ - Map.fromList - [ ( 674 - , Cardano.TxMetaMap - [ ( Cardano.TxMetaText "field" - , Cardano.TxMetaNumber 123 - ) - , ( Cardano.TxMetaText "msgs" - , Cardano.TxMetaList - [ Cardano.TxMetaText "Invoice-No: 123456789" - , Cardano.TxMetaText "Order-No: 7654321" - , Cardano.TxMetaText "Email: john@doe.com" - ] - ) - ] - ) - ] - toMetadataEncrypted "cardano" schemaBefore Nothing - `shouldBe` Left ErrIncorrectRawMetadata - -fromHexToM :: Text -> Maybe ByteString -fromHexToM = rightToMaybe . convertFromBase Base16 . T.encodeUtf8 - {------------------------------------------------------------------------------- Error type encoding -------------------------------------------------------------------------------} From ca057294d5549f4424a5ff74da0135e620d9220b Mon Sep 17 00:00:00 2001 From: Pawel Jakubas Date: Fri, 26 Jul 2024 17:33:31 +0200 Subject: [PATCH 19/26] extend golden --- .../Wallet/Primitive/Types/MetadataEncryptionSpec.hs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs b/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs index 839e5b43c5e..c3a190f6de7 100644 --- a/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs +++ b/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs @@ -77,6 +77,8 @@ spec = do ] toMetadataEncrypted "cardano" schemaBefore Nothing `shouldBe` Right schemaAfter + fromMetadataEncrypted "cardano" schemaAfter + `shouldBe` Left ErrMissingSalt -- $ echo -n '"secret data that is long enough to produce more than 64 bytes"' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -nosalt -- OLSOdRF+P56rW9gUopHcs0HHcdmPP5ujhSuB+r84VJgvsMOsqmIZx2etosnkyOc8 @@ -120,6 +122,8 @@ spec = do ] toMetadataEncrypted "cardano" schemaBefore Nothing `shouldBe` Right schemaAfter + fromMetadataEncrypted "cardano" schemaAfter + `shouldBe` Left ErrMissingSalt -- $ echo -n '["Invoice-No: 123456789","Order-No: 7654321","Email: john@doe.com"]' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -nosalt -- IBcjjGQ7akr/CV2Zb0HtCvEPQNndZujCZ7iaFGMjOX3q3PJg5aRUvHgO3gPnDzYE @@ -165,6 +169,8 @@ spec = do ] toMetadataEncrypted "cardano" schemaBefore Nothing `shouldBe` Right schemaAfter + fromMetadataEncrypted "cardano" schemaAfter + `shouldBe` Left ErrMissingSalt -- $ $ echo -n '"secret data"' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -S 3030303030303030 -- U2FsdGVkX18wMDAwMDAwMF0ea/2sHeptB3SvZtgc600= @@ -204,6 +210,8 @@ spec = do saltM = fromHexToM "3030303030303030" toMetadataEncrypted "cardano" schemaBefore saltM `shouldBe` Right schemaAfter + fromMetadataEncrypted "cardano" schemaAfter + `shouldBe` Right schemaBefore -- $ echo -n '"secret data that is long enough to produce more than 64 bytes"' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -S 3030303030303030 -- U2FsdGVkX18wMDAwMDAwMPNdhZQON/Hlwqvk4+sNRCa90QrAVpIGUlWgZhgNlwKh @@ -245,6 +253,8 @@ spec = do saltM = fromHexToM "3030303030303030" toMetadataEncrypted "cardano" schemaBefore saltM `shouldBe` Right schemaAfter + fromMetadataEncrypted "cardano" schemaAfter + `shouldBe` Right schemaBefore -- $ $ echo -n '["Invoice-No: 123456789","Order-No: 7654321","Email: john@doe.com"]' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -S 3030303030303030 -- U2FsdGVkX18wMDAwMDAwMFlOS4b0tXrZA7U5aQaHeI/sP74h84EPEjGv0wl4D8Do @@ -291,6 +301,8 @@ spec = do saltM = fromHexToM "3030303030303030" toMetadataEncrypted "cardano" schemaBefore saltM `shouldBe` Right schemaAfter + fromMetadataEncrypted "cardano" schemaAfter + `shouldBe` Right schemaBefore it "msg wrong label - no salt" $ do let schemaBefore = From d3fe6e14cb1f737f1eb28338714a6050b650d6ed Mon Sep 17 00:00:00 2001 From: Pawel Jakubas Date: Fri, 26 Jul 2024 18:45:31 +0200 Subject: [PATCH 20/26] add roundtrip property --- .../Primitive/Types/MetadataEncryptionSpec.hs | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs b/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs index c3a190f6de7..abc5c3c3555 100644 --- a/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs +++ b/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs @@ -20,6 +20,10 @@ import Data.ByteArray.Encoding import Data.ByteString ( ByteString ) +import Data.Either + ( isRight + , fromRight + ) import Data.Either.Combinators ( rightToMaybe ) @@ -32,13 +36,33 @@ import Test.Hspec , it , shouldBe ) +import Test.QuickCheck + ( Arbitrary (..) + , UnicodeString (..) + , chooseInt + , property + , suchThat + , vectorOf + , (===) + , (==>) + ) import qualified Cardano.Api as Cardano +import qualified Data.ByteString as BS import qualified Data.Map.Strict as Map +import qualified Data.Text as T import qualified Data.Text.Encoding as T spec :: Spec spec = do + describe "metadata encrypt/decrypt roundtrip" $ + it "fromMetadataEncrypted . toMetadataEncrypted $ payload == payload" $ property $ + \(TestingSetup payload' pwd' _ salt') -> do + isRight (toMetadataEncrypted pwd' payload' (Just salt')) ==> + (fromMetadataEncrypted pwd' + (fromRight metadataNotValid (toMetadataEncrypted pwd' payload' (Just salt')))) + === Right payload' + describe "toMetadataEncrypted openssl goldens" $ do -- $ echo -n '"secret data"' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -nosalt -- vBSywXY+WGcrckHUCyjJcQ== @@ -349,3 +373,66 @@ spec = do fromHexToM :: Text -> Maybe ByteString fromHexToM = rightToMaybe . convertFromBase Base16 . T.encodeUtf8 + +data TestingSetup = TestingSetup + { payload :: Cardano.TxMetadata + , password :: ByteString + , passwordOther :: ByteString + , salt :: ByteString + } deriving (Eq, Show) + +data Msg = Msg {getMsg :: Text} + +instance Arbitrary Msg where + arbitrary = do + txt <- (T.pack . getUnicodeString <$> arbitrary) `suchThat` (not . T.null) + pure $ Msg txt + +instance Arbitrary TestingSetup where + arbitrary = do + msgNum <- chooseInt (1,10) + txts <- vectorOf msgNum (getMsg <$> arbitrary) + pwdLen1 <- chooseInt (5,10) + pwdLen2 <- chooseInt (5,10) + pwd1 <- BS.pack <$> vectorOf pwdLen1 arbitrary + pwd2 <- (BS.pack <$> vectorOf pwdLen2 arbitrary) `suchThat` (/= pwd1) + salt' <- BS.pack <$> vectorOf 8 arbitrary + let metadata toEncrypt = + Cardano.TxMetadata $ Map.fromList + [ ( 674 + , Cardano.TxMetaMap + [ ( Cardano.TxMetaText "field" + , Cardano.TxMetaNumber 123 + ) + , ( Cardano.TxMetaText "msg" + , Cardano.TxMetaList toEncrypt + ) + ] + ) + ] + pure $ TestingSetup + { payload = metadata $ Cardano.TxMetaText <$> txts + , password = pwd1 + , passwordOther = pwd2 + , salt = salt' + } + +metadataNotValid :: Cardano.TxMetadata +metadataNotValid = + Cardano.TxMetadata $ + Map.fromList + [ ( 674 + , Cardano.TxMetaMap + [ ( Cardano.TxMetaText "field" + , Cardano.TxMetaNumber 123 + ) + , ( Cardano.TxMetaText "msgs" + , Cardano.TxMetaList + [ Cardano.TxMetaText "Invoice-No: 123456789" + , Cardano.TxMetaText "Order-No: 7654321" + , Cardano.TxMetaText "Email: john@doe.com" + ] + ) + ] + ) + ] From 4a8e3053f50176b99fa2187437cc7fb6239a5b36 Mon Sep 17 00:00:00 2001 From: Pawel Jakubas Date: Tue, 30 Jul 2024 12:24:40 +0200 Subject: [PATCH 21/26] another property testes --- lib/primitive/cardano-wallet-primitive.cabal | 1 + .../Primitive/Types/MetadataEncryptionSpec.hs | 21 +++++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/primitive/cardano-wallet-primitive.cabal b/lib/primitive/cardano-wallet-primitive.cabal index 10f6d2fafd0..3b1fef52822 100644 --- a/lib/primitive/cardano-wallet-primitive.cabal +++ b/lib/primitive/cardano-wallet-primitive.cabal @@ -242,6 +242,7 @@ test-suite test , cardano-wallet-primitive:cardano-wallet-primitive , cardano-wallet-test-utils , containers + , crypto-primitives , deepseq , delta-types , either diff --git a/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs b/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs index abc5c3c3555..cc869dbb082 100644 --- a/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs +++ b/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs @@ -21,7 +21,8 @@ import Data.ByteString ( ByteString ) import Data.Either - ( isRight + ( isLeft + , isRight , fromRight ) import Data.Either.Combinators @@ -35,6 +36,7 @@ import Test.Hspec , describe , it , shouldBe + , shouldSatisfy ) import Test.QuickCheck ( Arbitrary (..) @@ -55,14 +57,21 @@ import qualified Data.Text.Encoding as T spec :: Spec spec = do - describe "metadata encrypt/decrypt roundtrip" $ + describe "metadata encrypt/decrypt roundtrips" $ do it "fromMetadataEncrypted . toMetadataEncrypted $ payload == payload" $ property $ - \(TestingSetup payload' pwd' _ salt') -> do - isRight (toMetadataEncrypted pwd' payload' (Just salt')) ==> - (fromMetadataEncrypted pwd' - (fromRight metadataNotValid (toMetadataEncrypted pwd' payload' (Just salt')))) + \(TestingSetup payload' pwd' _ salt') -> do + let encrypted = toMetadataEncrypted pwd' payload' (Just salt') + isRight encrypted ==> + (fromMetadataEncrypted pwd' (fromRight metadataNotValid encrypted)) === Right payload' + it "fromMetadataEncrypted fails for wrong passphrase" $ property $ + \(TestingSetup payload' pwd1 pwd2 salt') -> do + let encrypted = toMetadataEncrypted pwd1 payload' (Just salt') + isRight encrypted ==> + fromMetadataEncrypted pwd2 (fromRight metadataNotValid encrypted) + `shouldSatisfy` isLeft + describe "toMetadataEncrypted openssl goldens" $ do -- $ echo -n '"secret data"' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -nosalt -- vBSywXY+WGcrckHUCyjJcQ== From e4e4346d814c70e121f5a0e234ae7bf40b32210e Mon Sep 17 00:00:00 2001 From: Pawel Jakubas Date: Tue, 30 Jul 2024 12:59:06 +0200 Subject: [PATCH 22/26] make msg value obey being list of txts adjust unit tests another property --- .../Primitive/Types/MetadataEncryption.hs | 12 ++- .../Primitive/Types/MetadataEncryptionSpec.hs | 91 ++++++++++++++----- 2 files changed, 77 insertions(+), 26 deletions(-) diff --git a/lib/primitive/lib/Cardano/Wallet/Primitive/Types/MetadataEncryption.hs b/lib/primitive/lib/Cardano/Wallet/Primitive/Types/MetadataEncryption.hs index 4f363b616c8..602aeb13273 100644 --- a/lib/primitive/lib/Cardano/Wallet/Primitive/Types/MetadataEncryption.hs +++ b/lib/primitive/lib/Cardano/Wallet/Primitive/Types/MetadataEncryption.hs @@ -147,7 +147,8 @@ toMetadataEncrypted pwd payload saltM = secretKey, iv :: ByteString (secretKey, iv) = PBKDF2.generateKey metadataPBKDF2Config pwd saltM - -- `msg` is embedded at the first level + -- `msg` is embedded at the first level with the exact following value structure + -- TxMetaList [TxMetaText txt1, ..., TxMetaText txtN] parseMessage :: TxMetadataValue -> Maybe [TxMetadataValue] parseMessage = \case TxMetaMap kvs -> @@ -157,9 +158,16 @@ toMetadataEncrypted pwd payload saltM = _ -> Nothing where + isText (TxMetaText _ ) = True + isText _ = False + + valueStructure (TxMetaList txts) = + all isText txts + valueStructure _ = False + getValue :: (TxMetadataValue, TxMetadataValue) -> Maybe TxMetadataValue getValue (TxMetaText k, v) = - if k == cip83EncryptPayloadKey then + if k == cip83EncryptPayloadKey && valueStructure v then Just v else Nothing diff --git a/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs b/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs index cc869dbb082..1924f21459f 100644 --- a/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs +++ b/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs @@ -10,6 +10,10 @@ import Prelude import Cardano.Wallet.Primitive.Types.MetadataEncryption ( ErrMetadataDecryption (..) , ErrMetadataEncryption (..) + , cip83EncryptMethodKey + , cip83EncryptPayloadKey + , cip83EncryptPayloadValue + , cip20MetadataKey , fromMetadataEncrypted , toMetadataEncrypted ) @@ -65,16 +69,34 @@ spec = do (fromMetadataEncrypted pwd' (fromRight metadataNotValid encrypted)) === Right payload' - it "fromMetadataEncrypted fails for wrong passphrase" $ property $ + it "fromMetadataEncrypted fails for different passphrase" $ property $ \(TestingSetup payload' pwd1 pwd2 salt') -> do let encrypted = toMetadataEncrypted pwd1 payload' (Just salt') isRight encrypted ==> fromMetadataEncrypted pwd2 (fromRight metadataNotValid encrypted) `shouldSatisfy` isLeft + it "the valid result of toMetadataEncrypted exhibits the expected characteristics" $ property $ + \(TestingSetup payload' pwd' _ salt') -> do + let encrypted = toMetadataEncrypted pwd' payload' (Just salt') + let hasMsgWithList (Cardano.TxMetaText k, Cardano.TxMetaList _) = + k == cip83EncryptPayloadKey + hasMsgWithList _ = False + hasEncPair (Cardano.TxMetaText k, Cardano.TxMetaText v) = + k == cip83EncryptMethodKey && v == cip83EncryptPayloadValue + hasEncPair _ = False + let hasCharacteristics (Cardano.TxMetadata themap) = + case Map.lookup cip20MetadataKey themap of + Just (Cardano.TxMetaMap kvs) -> + any hasMsgWithList kvs && any hasEncPair kvs + _ -> False + isRight encrypted ==> + fromRight metadataNotValid encrypted + `shouldSatisfy` hasCharacteristics + describe "toMetadataEncrypted openssl goldens" $ do - -- $ echo -n '"secret data"' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -nosalt - -- vBSywXY+WGcrckHUCyjJcQ== + -- echo -n '["secret data"]' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -nosalt + -- Fm/+xoZBA24yp8Vz548NAg== it "short msg - no salt" $ do let schemaBefore = Cardano.TxMetadata $ @@ -85,7 +107,8 @@ spec = do , Cardano.TxMetaNumber 123 ) , ( Cardano.TxMetaText "msg" - , Cardano.TxMetaText "secret data" + , Cardano.TxMetaList + [Cardano.TxMetaText "secret data"] ) ] ) @@ -100,7 +123,7 @@ spec = do ) , ( Cardano.TxMetaText "msg" , Cardano.TxMetaList - [Cardano.TxMetaText "vBSywXY+WGcrckHUCyjJcQ=="] + [Cardano.TxMetaText "Fm/+xoZBA24yp8Vz548NAg=="] ) , ( Cardano.TxMetaText "enc" , Cardano.TxMetaText "basic" @@ -113,9 +136,27 @@ spec = do fromMetadataEncrypted "cardano" schemaAfter `shouldBe` Left ErrMissingSalt - -- $ echo -n '"secret data that is long enough to produce more than 64 bytes"' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -nosalt - -- OLSOdRF+P56rW9gUopHcs0HHcdmPP5ujhSuB+r84VJgvsMOsqmIZx2etosnkyOc8 - -- ygjbu25gCdhJh7iEpAJVaA== + it "short msg - no salt wrong value structure" $ do + let schemaBefore = + Cardano.TxMetadata $ + Map.fromList + [ ( 674 + , Cardano.TxMetaMap + [ ( Cardano.TxMetaText "field" + , Cardano.TxMetaNumber 123 + ) + , ( Cardano.TxMetaText "msg" + , Cardano.TxMetaText "secret data" + ) + ] + ) + ] + toMetadataEncrypted "cardano" schemaBefore Nothing + `shouldBe` Left ErrIncorrectRawMetadata + + -- $ echo -n '["secret data that is long enough to produce more than 64 bytes"]' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -nosalt + -- +8ruwpQolMU4wznBR5LYQEyke/SlJ7mkU+1LEXs2vSC8gegvjWESqnWK1Tw59cFt + -- CKO3g/d6fGA2jOU7JDYlC1qf+mdDKlGHbPKCV41Fofs= it "long msg - no salt" $ do let schemaBefore = Cardano.TxMetadata $ @@ -126,9 +167,10 @@ spec = do , Cardano.TxMetaNumber 123 ) , ( Cardano.TxMetaText "msg" - , Cardano.TxMetaText - "secret data that is long enough to produce more \ - \than 64 bytes" + , Cardano.TxMetaList + [ Cardano.TxMetaText + "secret data that is long enough to produce more \ + \than 64 bytes" ] ) ] ) @@ -143,8 +185,8 @@ spec = do ) , ( Cardano.TxMetaText "msg" , Cardano.TxMetaList - [ Cardano.TxMetaText "OLSOdRF+P56rW9gUopHcs0HHcdmPP5ujhSuB+r84VJgvsMOsqmIZx2etosnkyOc8" - , Cardano.TxMetaText "ygjbu25gCdhJh7iEpAJVaA==" + [ Cardano.TxMetaText "+8ruwpQolMU4wznBR5LYQEyke/SlJ7mkU+1LEXs2vSC8gegvjWESqnWK1Tw59cFt" + , Cardano.TxMetaText "CKO3g/d6fGA2jOU7JDYlC1qf+mdDKlGHbPKCV41Fofs=" ] ) , ( Cardano.TxMetaText "enc" @@ -205,8 +247,8 @@ spec = do fromMetadataEncrypted "cardano" schemaAfter `shouldBe` Left ErrMissingSalt - -- $ $ echo -n '"secret data"' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -S 3030303030303030 - -- U2FsdGVkX18wMDAwMDAwMF0ea/2sHeptB3SvZtgc600= + -- $ echo -n '["secret data"]' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -S 3030303030303030 + -- U2FsdGVkX18wMDAwMDAwMKg9+BnuLSqx880pgF+owzo= it "short msg - salted" $ do let schemaBefore = Cardano.TxMetadata $ Map.fromList @@ -216,7 +258,8 @@ spec = do , Cardano.TxMetaNumber 123 ) , ( Cardano.TxMetaText "msg" - , Cardano.TxMetaText "secret data" + , Cardano.TxMetaList + [ Cardano.TxMetaText "secret data" ] ) ] ) @@ -231,8 +274,7 @@ spec = do ) , ( Cardano.TxMetaText "msg" , Cardano.TxMetaList - [ Cardano.TxMetaText "U2FsdGVkX18wMDAwMDAwMF0ea/2sHeptB3SvZtgc600=" - ] + [ Cardano.TxMetaText "U2FsdGVkX18wMDAwMDAwMKg9+BnuLSqx880pgF+owzo=" ] ) , ( Cardano.TxMetaText "enc" , Cardano.TxMetaText "basic" @@ -246,9 +288,9 @@ spec = do fromMetadataEncrypted "cardano" schemaAfter `shouldBe` Right schemaBefore - -- $ echo -n '"secret data that is long enough to produce more than 64 bytes"' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -S 3030303030303030 - -- U2FsdGVkX18wMDAwMDAwMPNdhZQON/Hlwqvk4+sNRCa90QrAVpIGUlWgZhgNlwKh - -- PbR/qyT2q0tejHQmsHdORif5rvZYTzJGsTutA0RIcFU= + -- $ echo -n '["secret data that is long enough to produce more than 64 bytes"]' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -S 3030303030303030 + -- U2FsdGVkX18wMDAwMDAwMK3WTtGcfCw96FEEQJct+JQfvpq824MACKzRPNqul83i + -- Jxd3aOenCM/IBadPmEcDVPyg+f/tszUp0KO8uzRxKTnY1bO4rqEKEQfu1GkAz7wF it "long msg - salted" $ do let schemaBefore = Cardano.TxMetadata $ Map.fromList @@ -258,7 +300,8 @@ spec = do , Cardano.TxMetaNumber 123 ) , ( Cardano.TxMetaText "msg" - , Cardano.TxMetaText "secret data that is long enough to produce more than 64 bytes" + , Cardano.TxMetaList + [ Cardano.TxMetaText "secret data that is long enough to produce more than 64 bytes" ] ) ] ) @@ -273,8 +316,8 @@ spec = do ) , ( Cardano.TxMetaText "msg" , Cardano.TxMetaList - [ Cardano.TxMetaText "U2FsdGVkX18wMDAwMDAwMPNdhZQON/Hlwqvk4+sNRCa90QrAVpIGUlWgZhgNlwKh" - , Cardano.TxMetaText "PbR/qyT2q0tejHQmsHdORif5rvZYTzJGsTutA0RIcFU=" + [ Cardano.TxMetaText "U2FsdGVkX18wMDAwMDAwMK3WTtGcfCw96FEEQJct+JQfvpq824MACKzRPNqul83i" + , Cardano.TxMetaText "Jxd3aOenCM/IBadPmEcDVPyg+f/tszUp0KO8uzRxKTnY1bO4rqEKEQfu1GkAz7wF" ] ) , ( Cardano.TxMetaText "enc" From c667f8cdd194a0fac8cf9571ace399601f51baf2 Mon Sep 17 00:00:00 2001 From: Pawel Jakubas Date: Tue, 30 Jul 2024 16:58:57 +0200 Subject: [PATCH 23/26] adjust integration testing --- .../Test/Integration/Scenario/API/Shelley/TransactionsNew.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/integration/scenarios/Test/Integration/Scenario/API/Shelley/TransactionsNew.hs b/lib/integration/scenarios/Test/Integration/Scenario/API/Shelley/TransactionsNew.hs index 345a5b2d52f..6134e95dfc5 100644 --- a/lib/integration/scenarios/Test/Integration/Scenario/API/Shelley/TransactionsNew.hs +++ b/lib/integration/scenarios/Test/Integration/Scenario/API/Shelley/TransactionsNew.hs @@ -573,7 +573,7 @@ spec = describe "NEW_SHELLEY_TRANSACTIONS" $ do it "TRANS_NEW_CREATE_02d - \ \Correct metadata structure to be encrypted - short" $ \ctx -> runResourceT $ do - let toBeEncrypted = TxMetaText "world" + let toBeEncrypted = TxMetaList [TxMetaText "world"] let metadataRaw = TxMetadata $ Map.fromList [ (0, TxMetaText "hello") From 778b07b32a694dd2da80d1de366332a0eb90daae Mon Sep 17 00:00:00 2001 From: Pawel Jakubas Date: Tue, 30 Jul 2024 17:05:00 +0200 Subject: [PATCH 24/26] code standards hlint rebase cleanup --- lib/api/cardano-wallet-api.cabal | 9 --------- lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs | 1 - .../Wallet/Primitive/Types/MetadataEncryption.hs | 8 ++++---- .../Wallet/Primitive/Types/MetadataEncryptionSpec.hs | 10 +++++----- lib/unit/test/unit/Cardano/Wallet/Api/TypesSpec.hs | 7 ------- 5 files changed, 9 insertions(+), 26 deletions(-) diff --git a/lib/api/cardano-wallet-api.cabal b/lib/api/cardano-wallet-api.cabal index e72eb52b469..1b99f38a124 100644 --- a/lib/api/cardano-wallet-api.cabal +++ b/lib/api/cardano-wallet-api.cabal @@ -38,8 +38,6 @@ library build-depends: , address-derivation-discovery , aeson - , aeson-pretty - , ansi-terminal , base , bech32 , bech32-th @@ -50,19 +48,13 @@ library , cardano-balance-tx:{cardano-balance-tx, internal} , cardano-binary , cardano-crypto - , cardano-ledger-alonzo - , cardano-ledger-babbage - , cardano-ledger-binary - , cardano-ledger-conway , cardano-ledger-core - , cardano-ledger-shelley , cardano-wallet , cardano-wallet-launcher , cardano-wallet-network-layer , cardano-wallet-primitive , cardano-wallet-read , cardano-wallet-secrets - , cborg , containers , contra-tracer , crypto-primitives @@ -85,7 +77,6 @@ library , quiet , random , safe - , scientific , servant , servant-client , servant-server diff --git a/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs b/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs index d8c694592cd..b26caddf5ba 100644 --- a/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs +++ b/lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs @@ -6,7 +6,6 @@ {-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NoMonomorphismRestriction #-} -{-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE StrictData #-} {-# OPTIONS_GHC -Wno-orphans #-} diff --git a/lib/primitive/lib/Cardano/Wallet/Primitive/Types/MetadataEncryption.hs b/lib/primitive/lib/Cardano/Wallet/Primitive/Types/MetadataEncryption.hs index 602aeb13273..06fd4702b60 100644 --- a/lib/primitive/lib/Cardano/Wallet/Primitive/Types/MetadataEncryption.hs +++ b/lib/primitive/lib/Cardano/Wallet/Primitive/Types/MetadataEncryption.hs @@ -21,9 +21,9 @@ where import Prelude import Cardano.Api - ( TxMetadataJsonSchemaError (..) + ( TxMetadata (..) + , TxMetadataJsonSchemaError (..) , TxMetadataValue (..) - , TxMetadata (..) , metadataValueToJsonNoSchema ) import Cardano.Api.Error @@ -63,8 +63,8 @@ import Data.ByteString ( ByteString ) import Data.Maybe - ( isJust - , fromMaybe + ( fromMaybe + , isJust , mapMaybe ) import Data.Text diff --git a/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs b/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs index 1924f21459f..5e70d951ca9 100644 --- a/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs +++ b/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs @@ -1,5 +1,4 @@ {-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE RecordWildCards #-} module Cardano.Wallet.Primitive.Types.MetadataEncryptionSpec ( spec @@ -10,10 +9,10 @@ import Prelude import Cardano.Wallet.Primitive.Types.MetadataEncryption ( ErrMetadataDecryption (..) , ErrMetadataEncryption (..) + , cip20MetadataKey , cip83EncryptMethodKey , cip83EncryptPayloadKey , cip83EncryptPayloadValue - , cip20MetadataKey , fromMetadataEncrypted , toMetadataEncrypted ) @@ -25,9 +24,9 @@ import Data.ByteString ( ByteString ) import Data.Either - ( isLeft + ( fromRight + , isLeft , isRight - , fromRight ) import Data.Either.Combinators ( rightToMaybe @@ -433,7 +432,8 @@ data TestingSetup = TestingSetup , salt :: ByteString } deriving (Eq, Show) -data Msg = Msg {getMsg :: Text} +newtype Msg = Msg {getMsg :: Text} + deriving (Eq, Show) instance Arbitrary Msg where arbitrary = do diff --git a/lib/unit/test/unit/Cardano/Wallet/Api/TypesSpec.hs b/lib/unit/test/unit/Cardano/Wallet/Api/TypesSpec.hs index 90e9b4bd64e..7eba242c605 100644 --- a/lib/unit/test/unit/Cardano/Wallet/Api/TypesSpec.hs +++ b/lib/unit/test/unit/Cardano/Wallet/Api/TypesSpec.hs @@ -77,9 +77,6 @@ import Cardano.Pool.Types , PoolOwner (..) , StakePoolTicker (..) ) -import Cardano.Wallet - ( ErrConstructTx (..) - ) import Cardano.Wallet.Address.Derivation ( Depth (..) , DerivationIndex (..) @@ -516,9 +513,6 @@ import Data.Aeson.QQ import Data.Bifunctor ( Bifunctor (..) ) -import Data.ByteArray.Encoding - ( convertFromBase - ) import Data.ByteString ( ByteString ) @@ -532,7 +526,6 @@ import Data.Either ) import Data.Either.Combinators ( fromRight' - , rightToMaybe ) import Data.FileEmbed ( embedFile From 5217dd0ecf862f21917c37e578dbaeac54570349 Mon Sep 17 00:00:00 2001 From: Pawel Jakubas Date: Mon, 5 Aug 2024 17:09:58 +0200 Subject: [PATCH 25/26] improve on unit tests - part 1 improve on unit tests - part 2 improve on unit tests - part 3 and add better error handling --- .../Primitive/Types/MetadataEncryption.hs | 14 +- .../Primitive/Types/MetadataEncryptionSpec.hs | 127 ++++++++++-------- 2 files changed, 83 insertions(+), 58 deletions(-) diff --git a/lib/primitive/lib/Cardano/Wallet/Primitive/Types/MetadataEncryption.hs b/lib/primitive/lib/Cardano/Wallet/Primitive/Types/MetadataEncryption.hs index 06fd4702b60..6812430139e 100644 --- a/lib/primitive/lib/Cardano/Wallet/Primitive/Types/MetadataEncryption.hs +++ b/lib/primitive/lib/Cardano/Wallet/Primitive/Types/MetadataEncryption.hs @@ -65,6 +65,8 @@ import Data.ByteString import Data.Maybe ( fromMaybe , isJust + , isNothing + , fromJust , mapMaybe ) import Data.Text @@ -271,11 +273,15 @@ fromMetadataEncrypted pwd metadata = in case value of TxMetaMap list -> snd <$> filter presentPair list _ -> [] - extractTxt (TxMetaText txt) = txt - extractTxt _ = - error "TxMetaText is expected" + extractTxt (TxMetaText txt) = Just txt + extractTxt _ = Nothing extractPayload (TxMetaList chunks)= - foldl T.append T.empty $ extractTxt <$> chunks + let extractedTxts = extractTxt <$> chunks + in if any isNothing extractedTxts then + T.empty + else + -- we are sure there is not Nothing in the extractedTxts + foldl T.append T.empty $ fromJust <$> extractedTxts extractPayload _ = T.empty composePayload (TxMetadata themap) = do validValue <- case Map.lookup cip20MetadataKey themap of diff --git a/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs b/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs index 5e70d951ca9..ab3ff045b9b 100644 --- a/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs +++ b/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs @@ -24,18 +24,21 @@ import Data.ByteString ( ByteString ) import Data.Either - ( fromRight - , isLeft - , isRight + ( isLeft ) import Data.Either.Combinators - ( rightToMaybe + ( mapLeft + , rightToMaybe + ) +import Data.Function + ( (&) ) import Data.Text ( Text ) import Test.Hspec - ( Spec + ( Expectation + , Spec , describe , it , shouldBe @@ -43,13 +46,13 @@ import Test.Hspec ) import Test.QuickCheck ( Arbitrary (..) + , Property , UnicodeString (..) , chooseInt , property , suchThat , vectorOf , (===) - , (==>) ) import qualified Cardano.Api as Cardano @@ -61,37 +64,17 @@ import qualified Data.Text.Encoding as T spec :: Spec spec = do describe "metadata encrypt/decrypt roundtrips" $ do - it "fromMetadataEncrypted . toMetadataEncrypted $ payload == payload" $ property $ - \(TestingSetup payload' pwd' _ salt') -> do - let encrypted = toMetadataEncrypted pwd' payload' (Just salt') - isRight encrypted ==> - (fromMetadataEncrypted pwd' (fromRight metadataNotValid encrypted)) - === Right payload' + it "fromMetadataEncrypted . toMetadataEncrypted $ payload == payload" $ + prop_roundtrip + & property - it "fromMetadataEncrypted fails for different passphrase" $ property $ - \(TestingSetup payload' pwd1 pwd2 salt') -> do - let encrypted = toMetadataEncrypted pwd1 payload' (Just salt') - isRight encrypted ==> - fromMetadataEncrypted pwd2 (fromRight metadataNotValid encrypted) - `shouldSatisfy` isLeft + it "fromMetadataEncrypted fails for different passphrase" $ + prop_passphrase + & property - it "the valid result of toMetadataEncrypted exhibits the expected characteristics" $ property $ - \(TestingSetup payload' pwd' _ salt') -> do - let encrypted = toMetadataEncrypted pwd' payload' (Just salt') - let hasMsgWithList (Cardano.TxMetaText k, Cardano.TxMetaList _) = - k == cip83EncryptPayloadKey - hasMsgWithList _ = False - hasEncPair (Cardano.TxMetaText k, Cardano.TxMetaText v) = - k == cip83EncryptMethodKey && v == cip83EncryptPayloadValue - hasEncPair _ = False - let hasCharacteristics (Cardano.TxMetadata themap) = - case Map.lookup cip20MetadataKey themap of - Just (Cardano.TxMetaMap kvs) -> - any hasMsgWithList kvs && any hasEncPair kvs - _ -> False - isRight encrypted ==> - fromRight metadataNotValid encrypted - `shouldSatisfy` hasCharacteristics + it "the valid result of toMetadataEncrypted exhibits the expected characteristics" $ + prop_structure_after_enc + & property describe "toMetadataEncrypted openssl goldens" $ do -- echo -n '["secret data"]' | openssl enc -e -aes-256-cbc -pbkdf2 -iter 10000 -a -k "cardano" -nosalt @@ -422,6 +405,31 @@ spec = do toMetadataEncrypted "cardano" schemaBefore Nothing `shouldBe` Left ErrIncorrectRawMetadata + describe "fromMetadataEncrypted incorrect payload" $ do + it "expecting only TxMetaText in TxMetaList of 'msg'" $ do + let schemaAfter = + Cardano.TxMetadata $ + Map.fromList + [ ( 674 + , Cardano.TxMetaMap + [ ( Cardano.TxMetaText "field" + , Cardano.TxMetaNumber 123 + ) + , ( Cardano.TxMetaText "msg" + , Cardano.TxMetaList + [ Cardano.TxMetaText "Fm/+xoZBA24yp8Vz548NAg==" + , Cardano.TxMetaNumber 123 + ] + ) + , ( Cardano.TxMetaText "enc" + , Cardano.TxMetaText "basic" + ) + ] + ) + ] + fromMetadataEncrypted "cardano" schemaAfter + `shouldBe` Left ErrMissingValidEncryptionPayload + fromHexToM :: Text -> Maybe ByteString fromHexToM = rightToMaybe . convertFromBase Base16 . T.encodeUtf8 @@ -469,22 +477,33 @@ instance Arbitrary TestingSetup where , salt = salt' } -metadataNotValid :: Cardano.TxMetadata -metadataNotValid = - Cardano.TxMetadata $ - Map.fromList - [ ( 674 - , Cardano.TxMetaMap - [ ( Cardano.TxMetaText "field" - , Cardano.TxMetaNumber 123 - ) - , ( Cardano.TxMetaText "msgs" - , Cardano.TxMetaList - [ Cardano.TxMetaText "Invoice-No: 123456789" - , Cardano.TxMetaText "Order-No: 7654321" - , Cardano.TxMetaText "Email: john@doe.com" - ] - ) - ] - ) - ] +prop_roundtrip :: TestingSetup -> Property +prop_roundtrip (TestingSetup payload' pwd' _ salt') = do + ((mapLeft (const ErrMissingValidEncryptionPayload) $ + toMetadataEncrypted pwd' payload' (Just salt')) >>= + fromMetadataEncrypted pwd') + === Right payload' + +prop_passphrase :: TestingSetup -> Expectation +prop_passphrase (TestingSetup payload' pwd1 pwd2 salt') = do + ((mapLeft (const ErrMissingValidEncryptionPayload) $ + toMetadataEncrypted pwd1 payload' (Just salt')) >>= + fromMetadataEncrypted pwd2) + `shouldSatisfy` isLeft + +prop_structure_after_enc :: TestingSetup -> Expectation +prop_structure_after_enc (TestingSetup payload' pwd' _ salt') = do + let hasMsgWithList (Cardano.TxMetaText k, Cardano.TxMetaList _) = + k == cip83EncryptPayloadKey + hasMsgWithList _ = False + hasEncPair (Cardano.TxMetaText k, Cardano.TxMetaText v) = + k == cip83EncryptMethodKey && v == cip83EncryptPayloadValue + hasEncPair _ = False + let hasCharacteristics (Cardano.TxMetadata themap) = + case Map.lookup cip20MetadataKey themap of + Just (Cardano.TxMetaMap kvs) -> + any hasMsgWithList kvs && any hasEncPair kvs + _ -> False + + (hasCharacteristics <$> toMetadataEncrypted pwd' payload' (Just salt')) + `shouldBe` Right True From 3027af84d2f2a9b392c861e742f795035081ad3b Mon Sep 17 00:00:00 2001 From: Pawel Jakubas Date: Mon, 5 Aug 2024 18:36:32 +0200 Subject: [PATCH 26/26] code polishing fix --- .../Wallet/Primitive/Types/MetadataEncryption.hs | 4 ++-- .../Primitive/Types/MetadataEncryptionSpec.hs | 14 ++++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/primitive/lib/Cardano/Wallet/Primitive/Types/MetadataEncryption.hs b/lib/primitive/lib/Cardano/Wallet/Primitive/Types/MetadataEncryption.hs index 6812430139e..0189cbe872e 100644 --- a/lib/primitive/lib/Cardano/Wallet/Primitive/Types/MetadataEncryption.hs +++ b/lib/primitive/lib/Cardano/Wallet/Primitive/Types/MetadataEncryption.hs @@ -63,10 +63,10 @@ import Data.ByteString ( ByteString ) import Data.Maybe - ( fromMaybe + ( fromJust + , fromMaybe , isJust , isNothing - , fromJust , mapMaybe ) import Data.Text diff --git a/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs b/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs index ab3ff045b9b..b7d56203bdd 100644 --- a/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs +++ b/lib/primitive/test/spec/Cardano/Wallet/Primitive/Types/MetadataEncryptionSpec.hs @@ -479,16 +479,18 @@ instance Arbitrary TestingSetup where prop_roundtrip :: TestingSetup -> Property prop_roundtrip (TestingSetup payload' pwd' _ salt') = do - ((mapLeft (const ErrMissingValidEncryptionPayload) $ - toMetadataEncrypted pwd' payload' (Just salt')) >>= - fromMetadataEncrypted pwd') + (mapLeft + (const ErrMissingValidEncryptionPayload) + (toMetadataEncrypted pwd' payload' (Just salt')) + >>= fromMetadataEncrypted pwd') === Right payload' prop_passphrase :: TestingSetup -> Expectation prop_passphrase (TestingSetup payload' pwd1 pwd2 salt') = do - ((mapLeft (const ErrMissingValidEncryptionPayload) $ - toMetadataEncrypted pwd1 payload' (Just salt')) >>= - fromMetadataEncrypted pwd2) + (mapLeft + (const ErrMissingValidEncryptionPayload) + (toMetadataEncrypted pwd1 payload' (Just salt')) + >>= fromMetadataEncrypted pwd2) `shouldSatisfy` isLeft prop_structure_after_enc :: TestingSetup -> Expectation