Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ADP-322] Add metadata encryption to HTTP API. #4555

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
b1c6cb6
update swagger
paweljakubas Apr 10, 2024
a14746a
introduce EncryptMetadataMethod
paweljakubas Apr 11, 2024
1c4b42c
add ErrConstructTxIncorrectRawMetadata
paweljakubas Apr 11, 2024
ec5a54e
more logic in toMetadataEncrypted
paweljakubas Apr 16, 2024
a32d76d
even more logic in toMetadataEncrypted
paweljakubas Apr 17, 2024
96f7ae2
add golden 1
paweljakubas Apr 17, 2024
932661a
add golden 2
paweljakubas Apr 17, 2024
7c81c7e
the rest of golden - salt used 2
paweljakubas Apr 18, 2024
0929907
be in line with 674 label
paweljakubas Apr 19, 2024
b86afe2
error testing
paweljakubas Apr 19, 2024
439ed17
typos
paweljakubas Apr 19, 2024
f05659f
integration tests 1
paweljakubas Apr 23, 2024
f7d3292
hlint
paweljakubas Apr 23, 2024
9af7c7e
add genSalt and use it in constructTransaction
paweljakubas Apr 23, 2024
0b65233
integration tests 2
paweljakubas Apr 23, 2024
1a925fd
getSaltFromEncrypted
paweljakubas Apr 25, 2024
53e4891
metadata integration test with long msg
paweljakubas Apr 25, 2024
8fe0d14
better code reuse in integration metadata encryption tests
paweljakubas Apr 25, 2024
e3f93c9
Miscellaneous formatting fixes.
jonathanknowles May 7, 2024
d8924ec
Extract out common constant `saltLengthBytes`.
jonathanknowles May 8, 2024
5fc875d
Use `stripPrefix` to simplify `getSaltFromEncrypted`.
jonathanknowles May 8, 2024
1ce36df
Use pattern guards in `getSaltEncrypted`.
jonathanknowles May 8, 2024
db0df0d
Move salt-related constants and functions together.
jonathanknowles May 8, 2024
54f1232
Use `stripPrefix` to simplify `decrypt`.
jonathanknowles May 8, 2024
e019fb3
Rename type to `ApiEncryptMetadataMethod`.
jonathanknowles May 14, 2024
bac1c72
Add `Arbitrary` instance for `ApiEncryptMetadataMethod`.
jonathanknowles May 14, 2024
4da3465
Simplify `Arbitrary` instance for `ApiEncryptMetadata`.
jonathanknowles May 14, 2024
993fc13
Add JSON golden test for `ApiEncryptMetadata`.
jonathanknowles May 14, 2024
983cd78
Rename `ApiEncryptMetadataMethod` constructor to `Basic`.
jonathanknowles May 14, 2024
d89f5ef
Derive `{To,From}JSON` instances for `ApiEncryptMetadataMethod`.
jonathanknowles May 14, 2024
5137303
Rename function `method` to `stdMethod`.
jonathanknowles May 14, 2024
f217d22
Change name of field from `enc` to `method`.
jonathanknowles May 14, 2024
5f3dfe7
Fix up imports of `ApiEra`.
jonathanknowles Jun 17, 2024
e225ea5
Remove unused `getMetadata` functions.
jonathanknowles Jun 17, 2024
929ff52
Fix formatting of comment for `toMetadataEncrypted`.
jonathanknowles Jun 18, 2024
edca5e0
Add type signatures to inner definitions of `toMetadataEncrypted`.
jonathanknowles Jun 18, 2024
079c7fc
Use unqualified imports for `TxMeta` and `TxMetadataValue`.
jonathanknowles Jun 18, 2024
d4f556b
Shorten definition of `updateTxMetadata`.
jonathanknowles Jun 18, 2024
59462a8
Use `LambdaCase` in `encryptingMsg`.
jonathanknowles Jun 18, 2024
2f2a584
Simplify definition of `encryptPairIfQualifies`.
jonathanknowles Jun 18, 2024
0959022
Use `LambdaCase` in `inspectMetaPair`.
jonathanknowles Jun 18, 2024
5cf22d1
Simplify definition of `getMsgValue`.
jonathanknowles Jun 18, 2024
97ab262
Create definition for `flip toTextChunks []`.
jonathanknowles Jun 18, 2024
4ec41bc
Use `toTextChunks` to refer to outer function.
jonathanknowles Jun 18, 2024
710e889
Extract out hard-coded `chunkSize` constant within `toTextChunks`.
jonathanknowles Jun 18, 2024
ddee2fb
Replace `toTextChunks` with `Text.chunksOf`.
jonathanknowles Jun 18, 2024
89e7ae8
Use `where` instead of `let` and `in` for `encryptPairIfQualifies`.
jonathanknowles Jun 18, 2024
36c44ab
Make `getMsgValue` an inner function of `inspectMetaPair`.
jonathanknowles Jun 19, 2024
319befb
Make `merge` an inner function of `inspectMetaPair`.
jonathanknowles Jun 19, 2024
e2f9f36
Simplify function `inspectMetaPair`.
jonathanknowles Jun 19, 2024
8530b62
Further simplify function `getMsgValue`.
jonathanknowles Jun 19, 2024
916c14c
Extract out constant `cip20MetadataKey`.
jonathanknowles Jun 20, 2024
56e68cd
Restrict return type of `findMsgValue`.
jonathanknowles Jun 20, 2024
ab5a49b
Use pattern guards in function `findMsgValue`.
jonathanknowles Jun 20, 2024
390c859
Further restrict return type of `findMsgValue`.
jonathanknowles Jun 20, 2024
e375cd6
Defer creation of (redundant) singleton list.
jonathanknowles Jun 20, 2024
e886518
Further defer creation of (redundant) singleton list.
jonathanknowles Jun 20, 2024
586dccd
Eliminate creation of redundant singleton list.
jonathanknowles Jun 20, 2024
2f5f939
Stop unnecessarily passing `cip20MetadataKey` around.
jonathanknowles Jun 20, 2024
5773f24
Remove unnecessary alias of "msg".
jonathanknowles Jun 21, 2024
fcd9087
Inline definition of `encMethodEntry`.
jonathanknowles Jun 21, 2024
61c35a8
Extract out definition `encryptedChunks`.
jonathanknowles Jun 21, 2024
9112310
Inline definition of `encrypted`.
jonathanknowles Jun 21, 2024
d03219e
Replace `mapBoth` with `bimap`.
jonathanknowles Jun 21, 2024
4d02b32
Inline definition of `toBase64Text`.
jonathanknowles Jun 21, 2024
a0ddb4a
Rename function to `parseMessage`.
jonathanknowles Jun 26, 2024
b9d5340
Rename function to `getValue`.
jonathanknowles Jun 26, 2024
7432aaa
Rename function to `validKeyAndMessage`.
jonathanknowles Jun 26, 2024
bbeec71
Rename function to `extractMessage`.
jonathanknowles Jun 26, 2024
9874963
Rename function to `encryptMessage`.
jonathanknowles Jun 26, 2024
f897727
Simplify definition of function `encryptMessage`.
jonathanknowles Jun 26, 2024
d9ff553
Make `encryptPairIfQualifies` an inner function.
jonathanknowles Jun 26, 2024
faa9827
Extract out function `encryptValue`.
jonathanknowles Jun 26, 2024
b9a66a0
Flatten `where` hierarchy for function `encryptMessage`.
jonathanknowles Jun 26, 2024
745c0ab
Add type signatures to inner functions of `toMetadataEncrypted`.
jonathanknowles Jun 26, 2024
2fd4565
Simplify definition of `toMetadataEncrypted`.
jonathanknowles Jun 26, 2024
a83c7e1
swagger improve
paweljakubas Jun 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions lib/api/src/Cardano/Wallet/Api/Http/Server/Error.hs
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,17 @@ 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 ->
apiError err403 InvalidMetadataEncryption $ mconcat
[ "It looks like the metadata does not "
, "have `msg` field that is supposed to be encrypted."
]
ErrConstructTxEncryptMetadata cryptoError ->
apiError err403 InvalidMetadataEncryption $ mconcat
[ "It looks like the metadata cannot be encrypted. "
, "The exact error is: "
, T.pack (show cryptoError)
]
ErrConstructTxNotImplemented ->
apiError err501 NotImplemented
"This feature is not yet implemented."
Expand Down
157 changes: 151 additions & 6 deletions lib/api/src/Cardano/Wallet/Api/Http/Shelley/Server.hs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ module Cardano.Wallet.Api.Http.Shelley.Server
, rndStateChange
, withWorkerCtx
, getCurrentEpoch
, toMetadataEncrypted
, metadataPBKDF2Config

-- * Workers
, manageRewardBalance
Expand Down Expand Up @@ -159,6 +161,8 @@ import Cardano.Address.Script
import Cardano.Api
( NetworkId
, SerialiseAsCBOR (..)
, TxMetadata (TxMetadata)
, TxMetadataValue (TxMetaList, TxMetaMap, TxMetaText)
, toNetworkMagic
, unNetworkMagic
)
Expand Down Expand Up @@ -365,6 +369,7 @@ import Cardano.Wallet.Api.Types
, ApiConstructTransactionData (..)
, ApiDecodeTransactionPostData (..)
, ApiDecodedTransaction (..)
, ApiEncryptMetadata (..)
, ApiExternalInput (..)
, ApiFee (..)
, ApiForeignStakeKey (..)
Expand Down Expand Up @@ -726,8 +731,26 @@ 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
( first
( bimap
, first
)
import Data.ByteArray.Encoding
( Base (..)
, convertToBase
)
import Data.ByteString
( ByteString
Expand Down Expand Up @@ -818,6 +841,7 @@ import Data.Traversable
)
import Data.Word
( Word32
, Word64
)
import Fmt
( pretty
Expand Down Expand Up @@ -943,13 +967,19 @@ 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 @@ -2735,8 +2765,17 @@ constructTransaction api knownPools poolStatus apiWalletId body = do
when (isJust (body ^. #encryptMetadata) && isNothing (body ^. #metadata) ) $
liftHandler $ throwE ErrConstructTxWrongPayload

when (isJust (body ^. #encryptMetadata)) $
liftHandler $ throwE ErrConstructTxNotImplemented
metadata <- case (body ^. #encryptMetadata, body ^. #metadata) of
(Just apiEncrypt, Just metadataWithSchema) -> do
salt <- liftIO $ genSalt 8
toMetadataEncrypted apiEncrypt metadataWithSchema (Just salt)
& \case
Left err ->
liftHandler $ throwE err
Right meta ->
pure $ Just meta
_ ->
pure $ body ^? #metadata . traverse . #txMetadataWithSchema_metadata

validityInterval <-
liftHandler $ parseValidityInterval ti $ body ^. #validityInterval
Expand All @@ -2750,9 +2789,6 @@ constructTransaction api knownPools poolStatus apiWalletId body = do
delegationRequest <-
liftHandler $ traverse parseDelegationRequest $ body ^. #delegations

let metadata =
body ^? #metadata . traverse . #txMetadataWithSchema_metadata

withWorkerCtx api walletId liftE liftE $ \wrk -> do
let db = wrk ^. dbLayer
netLayer = wrk ^. networkLayer
Expand Down Expand Up @@ -3129,6 +3165,115 @@ 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 not embedded beyond the first level
parseMessage :: TxMetadataValue -> Maybe TxMetadataValue
parseMessage = \case
TxMetaMap kvs ->
case mapMaybe getValue kvs of
[ ] -> Nothing
[v] -> Just v
_vs -> error "only one 'msg' field expected"
_ ->
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 . 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
12 changes: 10 additions & 2 deletions lib/api/src/Cardano/Wallet/Api/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ module Cardano.Wallet.Api.Types
, ApiCoinSelectionOutput (..)
, ApiCoinSelectionWithdrawal (..)
, ApiEncryptMetadata (..)
, ApiEncryptMetadataMethod (..)
, ApiConstructTransaction (..)
, ApiConstructTransactionData (..)
, ApiCosignerIndex (..)
Expand Down Expand Up @@ -1235,8 +1236,15 @@ data ApiMultiDelegationAction
deriving (Eq, Generic, Show)
deriving anyclass NFData

newtype ApiEncryptMetadata = ApiEncryptMetadata
{ passphrase :: ApiT (Passphrase "lenient") }
data ApiEncryptMetadataMethod = Basic
deriving (Bounded, Enum, Eq, Generic, Show)
deriving anyclass NFData
deriving (FromJSON, ToJSON) via DefaultSum ApiEncryptMetadataMethod

data ApiEncryptMetadata = ApiEncryptMetadata
{ passphrase :: ApiT (Passphrase "lenient")
, method :: Maybe ApiEncryptMetadataMethod
}
deriving (Eq, Generic, Show)
deriving (FromJSON, ToJSON) via DefaultRecord ApiEncryptMetadata
deriving anyclass NFData
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
| InvalidMetadataEncryption
| InvalidValidityBounds
| InvalidWalletType
| KeyNotFoundForAddress
Expand Down
41 changes: 25 additions & 16 deletions lib/crypto-primitives/src/Cryptography/Cipher/AES256CBC.hs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ module Cryptography.Cipher.AES256CBC
, CipherError (..)
, encrypt
, decrypt
, getSaltFromEncrypted
) where

import Prelude
Expand Down Expand Up @@ -106,7 +107,7 @@ encrypt
-- ^ Payload: must be a multiple of a block size, ie., 16 bytes.
-> Either CipherError ByteString
encrypt mode keyBytes ivBytes saltM msg
| any ((/= 8) . BS.length) saltM =
| any ((/= saltLengthBytes) . BS.length) saltM =
Left WrongSaltSize
| mode == WithoutPadding && BS.length msg `mod` 16 /= 0 =
Left WrongPayloadSize
Expand All @@ -128,9 +129,6 @@ encrypt mode keyBytes ivBytes saltM msg
WithoutPadding -> id
WithPadding -> PKCS7.pad

saltPrefix :: ByteString
saltPrefix = "Salted__"

-- | Decrypt using AES256 using CBC mode.
decrypt
:: CipherMode
Expand All @@ -146,20 +144,31 @@ decrypt mode key iv msg = do
when (mode == WithoutPadding && BS.length msg `mod` 16 /= 0) $
Left WrongPayloadSize
initedIV <- first FromCryptonite (createIV iv)
let (prefix,rest) = BS.splitAt 8 msg
let saltDetected = prefix == saltPrefix
if saltDetected then
second (, Just $ BS.take 8 rest) $
bimap FromCryptonite
(\c -> cbcDecrypt c initedIV (BS.drop 8 rest)) (initCipher key) >>=
unpad
else
second (, Nothing) $
bimap FromCryptonite
(\c -> cbcDecrypt c initedIV msg) (initCipher key) >>=
unpad
case BS.stripPrefix saltPrefix msg of
Just rest ->
second (, Just $ BS.take saltLengthBytes rest) $
bimap FromCryptonite
(\c -> cbcDecrypt c initedIV (BS.drop saltLengthBytes rest))
(initCipher key) >>=
unpad
Nothing ->
second (, Nothing) $
bimap FromCryptonite
(\c -> cbcDecrypt c initedIV msg) (initCipher key) >>=
unpad
where
unpad :: ByteString -> Either CipherError ByteString
unpad p = case mode of
WithoutPadding -> Right p
WithPadding -> maybeToEither EmptyPayload (PKCS7.unpad p)

saltLengthBytes :: Int
saltLengthBytes = 8

saltPrefix :: ByteString
saltPrefix = "Salted__"

getSaltFromEncrypted :: ByteString -> Maybe ByteString
getSaltFromEncrypted msg
| BS.length msg < 32 = Nothing
| otherwise = BS.take saltLengthBytes <$> BS.stripPrefix saltPrefix msg
9 changes: 9 additions & 0 deletions lib/crypto-primitives/src/Cryptography/Core.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ module Cryptography.Core
, eitherCryptoError

, MonadRandom (..)
, genSalt
) where

import Prelude

import Crypto.Error
( CryptoError (..)
, CryptoFailable (..)
Expand All @@ -14,3 +17,9 @@ import Crypto.Error
import Crypto.Random.Types
( MonadRandom (..)
)
import Data.ByteString
( ByteString
)

genSalt :: MonadRandom m => Int -> m ByteString
genSalt = getRandomBytes
1 change: 1 addition & 0 deletions lib/integration/cardano-wallet-integration.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ library scenarios
, cardano-wallet-test-utils
, command
, containers
, crypto-primitives
, either
, extra
, faucet
Expand Down
Loading
Loading