Skip to content

Commit

Permalink
Decrypt encrypted metadata (#4667)
Browse files Browse the repository at this point in the history
<!--
Detail in a few bullet points the work accomplished in this PR.

Before you submit, don't forget to:

* Make sure the GitHub PR fields are correct:
   ✓ Set a good Title for your PR.
   ✓ Assign yourself to the PR.
   ✓ Assign one or more reviewer(s).
   ✓ Link to a Jira issue, and/or other GitHub issues or PRs.
   ✓ In the PR description delete any empty sections
     and all text commented in <!--, so that this text does not appear
     in merge commit messages.

* Don't waste reviewers' time:
   ✓ If it's a draft, select the Create Draft PR option.
✓ Self-review your changes to make sure nothing unexpected slipped
through.

* Try to make your intent clear:
   ✓ Write a good Description that explains what this PR is meant to do.
   ✓ Jira will detect and link to this PR once created, but you can also
     link this PR in the description of the corresponding Jira ticket.
   ✓ Highlight what Testing you have done.
   ✓ Acknowledge any changes required to the Documentation.
-->

Idea is to enable decryption of already encrypted metadata in
decodeTransaction. The user specifies passphrase in which metadata was
encrypted and the metadata located as in CIP83 is decrypted. The change
is added in non-intrusive way. Due to lack of JSON instances exposure in
cardano-api the needed functions were added (they are about to be erased
when cardano-api exposes `metadataValueFromJsonNoSchema` in next node
versions).

The heart of solution is `fromMetadataEncrypted`. Api spec was updated.
Integration tests expanded to show the case.

### Comments

<!-- Additional comments, links, or screenshots to attach, if any. -->

### Issue Number
adp-3348

<!-- Reference the Jira/GitHub issue that this PR relates to, and which
requirements it tackles.
  Note: Jira issues of the form ADP- will be auto-linked. -->
  • Loading branch information
paweljakubas authored Aug 6, 2024
2 parents 4659ef6 + 3027af8 commit b887956
Show file tree
Hide file tree
Showing 12 changed files with 1,178 additions and 539 deletions.
49 changes: 47 additions & 2 deletions lib/api/src/Cardano/Wallet/Api/Http/Server/Error.hs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import Cardano.Wallet
, ErrConstructTx (..)
, ErrCreateMigrationPlan (..)
, ErrCreateRandomAddress (..)
, ErrDecodeTx (..)
, ErrDerivePublicKey (..)
, ErrFetchRewards (..)
, ErrGetPolicyId (..)
Expand Down Expand Up @@ -136,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)
)
Expand Down Expand Up @@ -478,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: "
Expand All @@ -493,6 +498,46 @@ instance IsServerError ErrConstructTx where
apiError err501 NotImplemented
"This feature is not yet implemented."

instance IsServerError ErrDecodeTx where
toServerError = \case
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."
]
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."
]
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."
]
ErrDecodeTxFromMetadataDecryption (ErrCannotAesonDecodePayload err) ->
apiError err403 InvalidMetadataDecryption $ mconcat
[ "It looks like the decrypted metadata cannot be decoded. "
, "The exact error is: "
, err
]
ErrDecodeTxFromMetadataDecryption ErrMissingSalt ->
apiError err403 InvalidMetadataDecryption $ mconcat
[ "It looks like the decrypted metadata can be decoded, but "
, "misses salt."
]
ErrDecodeTxFromMetadataDecryption (ErrCannotDecryptPayload cryptoError) ->
apiError err403 InvalidMetadataDecryption $ mconcat
[ "It looks like the encrypted metadata cannot be decrypted. "
, "The exact error is: "
, T.pack (show cryptoError)
]
ErrDecodeTxFromMetadataDecryption ErrEncryptedPayloadWrongBase ->
apiError err403 InvalidMetadataDecryption $ mconcat
[ "It looks like the encrypted metadata is not represented as a list of Base64 "
, "- see CIP83."
]

instance IsServerError ErrGetPolicyId where
toServerError = \case
ErrGetPolicyIdReadPolicyPubliKey e -> toServerError e
Expand Down
173 changes: 28 additions & 145 deletions lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,6 @@ module Cardano.Wallet.Api.Http.Shelley.Server
, rndStateChange
, withWorkerCtx
, getCurrentEpoch
, toMetadataEncrypted
, metadataPBKDF2Config

-- * Workers
, manageRewardBalance
Expand Down Expand Up @@ -154,8 +152,6 @@ import Cardano.Address.Script
import Cardano.Api
( NetworkId
, SerialiseAsCBOR (..)
, TxMetadata (TxMetadata)
, TxMetadataValue (TxMetaList, TxMetaMap, TxMetaText)
, toNetworkMagic
, unNetworkMagic
)
Expand All @@ -181,6 +177,7 @@ import Cardano.Wallet
, ErrConstructSharedWallet (..)
, ErrConstructTx (..)
, ErrCreateMigrationPlan (..)
, ErrDecodeTx (..)
, ErrGetPolicyId (..)
, ErrNoSuchWallet (..)
, ErrReadRewardAccount (..)
Expand Down Expand Up @@ -359,7 +356,6 @@ import Cardano.Wallet.Api.Types
, ApiDRepSpecifier (..)
, ApiDecodeTransactionPostData (..)
, ApiDecodedTransaction (..)
, ApiEncryptMetadata (..)
, ApiExternalInput (..)
, ApiFee (..)
, ApiForeignStakeKey (..)
Expand Down Expand Up @@ -611,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 (..)
)
Expand Down Expand Up @@ -721,26 +721,11 @@ import Control.Tracer
( Tracer
, contramap
)
import Cryptography.Cipher.AES256CBC
( CipherError
, CipherMode (..)
)
import Cryptography.Core
( genSalt
)
import Cryptography.Hash.Core
( SHA256 (..)
)
import Cryptography.KDF.PBKDF2
( PBKDF2Config (..)
)
import Data.Bifunctor
( bimap
, first
)
import Data.ByteArray.Encoding
( Base (..)
, convertToBase
( first
)
import Data.ByteString
( ByteString
Expand Down Expand Up @@ -822,7 +807,6 @@ import Data.Traversable
)
import Data.Word
( Word32
, Word64
)
import Fmt
( pretty
Expand Down Expand Up @@ -919,19 +903,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
Expand Down Expand Up @@ -2607,12 +2586,16 @@ 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
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

Expand Down Expand Up @@ -3004,115 +2987,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

-- 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 "msg", v) = Just v
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) ->
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 "msg", TxMetaList (toChunks encryptedMessage))
, (TxMetaText "enc", TxMetaText "basic")
]

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

metadataPBKDF2Config :: PBKDF2Config SHA256
metadataPBKDF2Config = PBKDF2Config
{ hash = SHA256
, iterations = 10000
, keyLength = 32
, ivLength = 16
}

toUsignedTxWdrl
:: c -> ApiWithdrawalGeneral n -> Maybe (RewardAccount, Coin, c)
toUsignedTxWdrl p = \case
Expand Down Expand Up @@ -3558,13 +3432,12 @@ 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
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
Expand All @@ -3573,7 +3446,17 @@ decodeTransaction
, metadata
, scriptValidity
} = walletTx
let db = wrk ^. dbLayer
db = wrk ^. dbLayer
metadata' <- case (decryptMetadata, metadata) of
(Just apiDecrypt, Just meta) -> do
let pwd = BA.convert $ unPassphrase $
getApiT $ apiDecrypt ^. #passphrase
case fromMetadataEncrypted pwd meta of
Left err ->
liftHandler $ throwE $ ErrDecodeTxFromMetadataDecryption err
Right txmetadata ->
pure . Just . ApiT $ txmetadata
_ -> pure $ ApiT <$> metadata
(acct, _, acctPath) <-
liftHandler $ W.shelleyOnlyReadRewardAccount @s db
inputPaths <-
Expand Down Expand Up @@ -3606,7 +3489,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
Expand Down
1 change: 1 addition & 0 deletions lib/api/src/Cardano/Wallet/Api/Types/Error.hs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ data ApiErrorInfo
| InputsDepleted
| InsufficientCollateral
| InvalidCoinSelection
| InvalidMetadataDecryption
| InvalidMetadataEncryption
| InvalidValidityBounds
| InvalidWalletType
Expand Down
5 changes: 4 additions & 1 deletion lib/api/src/Cardano/Wallet/Api/Types/SchemaMetadata.hs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE StrictData #-}

{-# OPTIONS_GHC -Wno-orphans #-}

-- |
-- Copyright: © 2018-2022 IOHK, 2023 Cardano Foundation
-- License: Apache-2.0
Expand All @@ -25,7 +27,7 @@ import Cardano.Api.Error
( displayError
)
import Cardano.Wallet.Primitive.Types.Tx
( TxMetadata
( TxMetadata (..)
)
import Control.Applicative
( (<|>)
Expand All @@ -40,6 +42,7 @@ import Data.Aeson
import GHC.Generics
( Generic
)

import Prelude

-- | A tag to select the json codec
Expand Down
Loading

0 comments on commit b887956

Please sign in to comment.