From 432c55a91933be258ea9a253a59ad9e91c7d03f8 Mon Sep 17 00:00:00 2001 From: Lucas Ontivero Date: Tue, 2 Jul 2024 14:26:20 +0200 Subject: [PATCH] `Kitchen` is a component that keeps the password encrypted in memory. It (#13218) implicitly assumes that an attacker with access to the process memory cannot extract the password. That assumption is incorrect and Wasabi has to decrypt the password multiple times to pass it to components that expect it as string. --- .../Rpc/WasabiJsonRpcService.cs | 4 +- .../Models/Wallets/WalletAuthModel.cs | 4 +- .../Models/Wallets/WalletCoinsModel.cs | 2 +- .../Models/Wallets/WalletInfoModel.cs | 4 +- .../Wallets/Send/PrivacyControlViewModel.cs | 2 +- .../Send/TransactionPreviewViewModel.cs | 4 +- WalletWasabi.Tests/Helpers/WabiSabiFactory.cs | 4 +- .../RegressionTests/CancelTests.cs | 2 +- .../RegressionTests/MaxFeeTests.cs | 2 +- .../RegressionTests/ReceiveSpeedupTests.cs | 2 +- .../RegressionTests/SelfSpendSpeedupTests.cs | 2 +- .../RegressionTests/SendSpeedupTests.cs | 2 +- .../UnitTests/Crypto/StringCipherTests.cs | 83 ------------- .../WabiSabi/Backend/AliceTimeoutTests.cs | 3 +- .../WabiSabi/Client/ArenaClientTests.cs | 2 +- .../WabiSabi/Client/BobClientTests.cs | 2 +- .../WabiSabi/Client/KeyChainTests.cs | 2 +- .../TransactionBuilderWalletExtensions.cs | 6 +- .../TransactionModifierWalletExtensions.cs | 2 +- WalletWasabi/Crypto/StringCipher.cs | 110 ------------------ WalletWasabi/WabiSabi/Client/KeyChain.cs | 12 +- WalletWasabi/Wallets/Kitchen.cs | 61 ---------- WalletWasabi/Wallets/Wallet.cs | 19 +-- 23 files changed, 37 insertions(+), 299 deletions(-) delete mode 100644 WalletWasabi.Tests/UnitTests/Crypto/StringCipherTests.cs delete mode 100644 WalletWasabi/Crypto/StringCipher.cs delete mode 100644 WalletWasabi/Wallets/Kitchen.cs diff --git a/WalletWasabi.Daemon/Rpc/WasabiJsonRpcService.cs b/WalletWasabi.Daemon/Rpc/WasabiJsonRpcService.cs index 2a8212c05ff..418e64d7df3 100644 --- a/WalletWasabi.Daemon/Rpc/WasabiJsonRpcService.cs +++ b/WalletWasabi.Daemon/Rpc/WasabiJsonRpcService.cs @@ -372,7 +372,7 @@ public string BuildCancelTransaction(uint256 txId, string password = "") { Guard.NotNull(nameof(txId), txId); var activeWallet = Guard.NotNull(nameof(ActiveWallet), ActiveWallet); - activeWallet.Kitchen.Cook(password); + activeWallet.TryLogin(password, out _); var mempoolStore = Global.BitcoinStore.TransactionStore.MempoolStore; if (!mempoolStore.TryGetTransaction(txId, out var smartTransactionToCancel)) { @@ -389,7 +389,7 @@ public string SpeedUpTransaction(uint256 txId, string password = "") { Guard.NotNull(nameof(txId), txId); var activeWallet = Guard.NotNull(nameof(ActiveWallet), ActiveWallet); - activeWallet.Kitchen.Cook(password); + activeWallet.TryLogin(password, out _); var mempoolStore = Global.BitcoinStore.TransactionStore.MempoolStore; if (!mempoolStore.TryGetTransaction(txId, out var smartTransactionToSpeedUp)) { diff --git a/WalletWasabi.Fluent/Models/Wallets/WalletAuthModel.cs b/WalletWasabi.Fluent/Models/Wallets/WalletAuthModel.cs index 3c0e61e05b4..2bf630708c7 100644 --- a/WalletWasabi.Fluent/Models/Wallets/WalletAuthModel.cs +++ b/WalletWasabi.Fluent/Models/Wallets/WalletAuthModel.cs @@ -20,7 +20,7 @@ public WalletAuthModel(IWalletModel walletModel, Wallet wallet) _wallet = wallet; } - public bool HasPassword => !string.IsNullOrEmpty(_wallet.Kitchen.SaltSoup()); + public bool HasPassword => !string.IsNullOrEmpty(_wallet.Password); public async Task LoginAsync(string password) { @@ -61,7 +61,7 @@ public void Logout() public bool VerifyRecoveryWords(Mnemonic mnemonic) { - var saltSoup = _wallet.Kitchen.SaltSoup(); + var saltSoup = _wallet.Password; var recovered = KeyManager.Recover( mnemonic, diff --git a/WalletWasabi.Fluent/Models/Wallets/WalletCoinsModel.cs b/WalletWasabi.Fluent/Models/Wallets/WalletCoinsModel.cs index afaf5b2bdcc..12ae9c2c479 100644 --- a/WalletWasabi.Fluent/Models/Wallets/WalletCoinsModel.cs +++ b/WalletWasabi.Fluent/Models/Wallets/WalletCoinsModel.cs @@ -39,7 +39,7 @@ public List GetSpentCoins(BuildTransactionResult? transaction) public bool AreEnoughToCreateTransaction(TransactionInfo transactionInfo, IEnumerable coins) { - return TransactionHelpers.TryBuildTransactionWithoutPrevTx(Wallet.KeyManager, transactionInfo, Wallet.Coins, coins.GetSmartCoins(), Wallet.Kitchen.SaltSoup(), out _); + return TransactionHelpers.TryBuildTransactionWithoutPrevTx(Wallet.KeyManager, transactionInfo, Wallet.Coins, coins.GetSmartCoins(), Wallet.Password, out _); } protected override Pocket[] GetPockets() diff --git a/WalletWasabi.Fluent/Models/Wallets/WalletInfoModel.cs b/WalletWasabi.Fluent/Models/Wallets/WalletInfoModel.cs index 7571441b0fb..f3970f66949 100644 --- a/WalletWasabi.Fluent/Models/Wallets/WalletInfoModel.cs +++ b/WalletWasabi.Fluent/Models/Wallets/WalletInfoModel.cs @@ -13,14 +13,14 @@ public WalletInfoModel(Wallet wallet) var network = wallet.Network; if (!wallet.KeyManager.IsWatchOnly) { - var secret = PasswordHelper.GetMasterExtKey(wallet.KeyManager, wallet.Kitchen.SaltSoup(), out _); + var secret = PasswordHelper.GetMasterExtKey(wallet.KeyManager, wallet.Password, out _); ExtendedMasterPrivateKey = secret.GetWif(network).ToWif(); ExtendedAccountPrivateKey = secret.Derive(wallet.KeyManager.SegwitAccountKeyPath).GetWif(network).ToWif(); ExtendedMasterZprv = secret.ToZPrv(network); // TODO: Should work for every type of wallet, temporarily disabling it. - WpkhOutputDescriptors = wallet.KeyManager.GetOutputDescriptors(wallet.Kitchen.SaltSoup(), network); + WpkhOutputDescriptors = wallet.KeyManager.GetOutputDescriptors(wallet.Password, network); } SegWitExtendedAccountPublicKey = wallet.KeyManager.SegwitExtPubKey.ToString(network); diff --git a/WalletWasabi.Fluent/ViewModels/Wallets/Send/PrivacyControlViewModel.cs b/WalletWasabi.Fluent/ViewModels/Wallets/Send/PrivacyControlViewModel.cs index 84553a25e6a..1a48ab1d6dd 100644 --- a/WalletWasabi.Fluent/ViewModels/Wallets/Send/PrivacyControlViewModel.cs +++ b/WalletWasabi.Fluent/ViewModels/Wallets/Send/PrivacyControlViewModel.cs @@ -35,7 +35,7 @@ public PrivacyControlViewModel(Wallet wallet, SendFlowModel sendFlow, Transactio _isSilent = isSilent; _usedCoins = usedCoins; - LabelSelection = new LabelSelectionViewModel(wallet.KeyManager, wallet.Kitchen.SaltSoup(), _transactionInfo, isSilent); + LabelSelection = new LabelSelectionViewModel(wallet.KeyManager, wallet.Password, _transactionInfo, isSilent); SetupCancel(enableCancel: false, enableCancelOnEscape: true, enableCancelOnPressed: false); EnableBack = true; diff --git a/WalletWasabi.Fluent/ViewModels/Wallets/Send/TransactionPreviewViewModel.cs b/WalletWasabi.Fluent/ViewModels/Wallets/Send/TransactionPreviewViewModel.cs index 09454d7a550..489e4278a71 100644 --- a/WalletWasabi.Fluent/ViewModels/Wallets/Send/TransactionPreviewViewModel.cs +++ b/WalletWasabi.Fluent/ViewModels/Wallets/Send/TransactionPreviewViewModel.cs @@ -64,7 +64,7 @@ public TransactionPreviewViewModel(UiContext uiContext, IWalletModel walletModel ]; DisplayedTransactionSummary = CurrentTransactionSummary; - + SetupCancel(enableCancel: true, enableCancelOnEscape: true, enableCancelOnPressed: false); EnableBack = true; @@ -479,7 +479,7 @@ private async Task CheckChangePocketAvailableAsync(BuildTransactionResult transa var usedCoins = transaction.SpentCoins; var pockets = _sendFlow.GetPockets(); - var labelSelection = new LabelSelectionViewModel(_wallet.KeyManager, _wallet.Kitchen.SaltSoup(), _info, isSilent: true); + var labelSelection = new LabelSelectionViewModel(_wallet.KeyManager, _wallet.Password, _info, isSilent: true); await labelSelection.ResetAsync(pockets, coinsToExclude: cjManager.CoinsInCriticalPhase[_wallet.WalletId].ToList()); _info.IsOtherPocketSelectionPossible = labelSelection.IsOtherSelectionPossible(usedCoins, _info.Recipient); diff --git a/WalletWasabi.Tests/Helpers/WabiSabiFactory.cs b/WalletWasabi.Tests/Helpers/WabiSabiFactory.cs index 8068886bccd..26049fb8e94 100644 --- a/WalletWasabi.Tests/Helpers/WabiSabiFactory.cs +++ b/WalletWasabi.Tests/Helpers/WabiSabiFactory.cs @@ -307,7 +307,7 @@ public static BlameRound CreateBlameRound(Round round, WabiSabiConfig cfg) public static (IKeyChain, SmartCoin, SmartCoin) CreateCoinKeyPairs(KeyManager? keyManager = null) { var km = keyManager ?? ServiceFactory.CreateKeyManager(""); - var keyChain = new KeyChain(km, new Kitchen("")); + var keyChain = new KeyChain(km,""); var smartCoin1 = BitcoinFactory.CreateSmartCoin(BitcoinFactory.CreateHdPubKey(km), Money.Coins(1m)); var smartCoin2 = BitcoinFactory.CreateSmartCoin(BitcoinFactory.CreateHdPubKey(km), Money.Coins(2m)); @@ -321,7 +321,7 @@ public static CoinJoinClient CreateTestCoinJoinClient( { return CreateTestCoinJoinClient( httpClientFactory, - new KeyChain(keyManager, new Kitchen("")), + new KeyChain(keyManager,""), new OutputProvider(new InternalDestinationProvider(keyManager)), roundStateUpdater, keyManager.RedCoinIsolation); diff --git a/WalletWasabi.Tests/RegressionTests/CancelTests.cs b/WalletWasabi.Tests/RegressionTests/CancelTests.cs index 51f29100544..c15bd2f712b 100644 --- a/WalletWasabi.Tests/RegressionTests/CancelTests.cs +++ b/WalletWasabi.Tests/RegressionTests/CancelTests.cs @@ -100,7 +100,7 @@ public async Task CancelTestsAsync() // Wait until the filter our previous transaction is present. var blockCount = await rpc.GetBlockCountAsync(); await setup.WaitForFiltersToBeProcessedAsync(TimeSpan.FromSeconds(120), blockCount); - wallet.Kitchen.Cook(password); + wallet.Password = password; TransactionBroadcaster broadcaster = new(network, bitcoinStore, httpClientFactory, walletManager); broadcaster.Initialize(nodes, rpc); diff --git a/WalletWasabi.Tests/RegressionTests/MaxFeeTests.cs b/WalletWasabi.Tests/RegressionTests/MaxFeeTests.cs index 41b81a0cc6d..5ae4662d3b9 100644 --- a/WalletWasabi.Tests/RegressionTests/MaxFeeTests.cs +++ b/WalletWasabi.Tests/RegressionTests/MaxFeeTests.cs @@ -107,7 +107,7 @@ public async Task CalculateMaxFeeTestAsync() // Wait until the filter our previous transaction is present. var blockCount = await rpc.GetBlockCountAsync(); await setup.WaitForFiltersToBeProcessedAsync(TimeSpan.FromSeconds(120), blockCount); - wallet.Kitchen.Cook(password); + wallet.Password = password; TransactionBroadcaster broadcaster = new(network, bitcoinStore, httpClientFactory, walletManager); broadcaster.Initialize(nodes, rpc); diff --git a/WalletWasabi.Tests/RegressionTests/ReceiveSpeedupTests.cs b/WalletWasabi.Tests/RegressionTests/ReceiveSpeedupTests.cs index 193f89d80d0..5535b344de5 100644 --- a/WalletWasabi.Tests/RegressionTests/ReceiveSpeedupTests.cs +++ b/WalletWasabi.Tests/RegressionTests/ReceiveSpeedupTests.cs @@ -97,7 +97,7 @@ public async Task ReceiveSpeedupTestsAsync() var blockCount = await rpc.GetBlockCountAsync(); await setup.WaitForFiltersToBeProcessedAsync(TimeSpan.FromSeconds(120), blockCount); - wallet.Kitchen.Cook(password); + wallet.Password = password; TransactionBroadcaster broadcaster = new(network, bitcoinStore, httpClientFactory, walletManager); broadcaster.Initialize(nodes, rpc); diff --git a/WalletWasabi.Tests/RegressionTests/SelfSpendSpeedupTests.cs b/WalletWasabi.Tests/RegressionTests/SelfSpendSpeedupTests.cs index e676e91a223..c6f9b3527be 100644 --- a/WalletWasabi.Tests/RegressionTests/SelfSpendSpeedupTests.cs +++ b/WalletWasabi.Tests/RegressionTests/SelfSpendSpeedupTests.cs @@ -104,7 +104,7 @@ public async Task SelfSpendSpeedupTestsAsync() var blockCount = await rpc.GetBlockCountAsync(); await setup.WaitForFiltersToBeProcessedAsync(TimeSpan.FromSeconds(120), blockCount); - wallet.Kitchen.Cook(password); + wallet.Password = password; TransactionBroadcaster broadcaster = new(network, bitcoinStore, httpClientFactory, walletManager); broadcaster.Initialize(nodes, rpc); diff --git a/WalletWasabi.Tests/RegressionTests/SendSpeedupTests.cs b/WalletWasabi.Tests/RegressionTests/SendSpeedupTests.cs index d1569fc23f0..dcc56d6269c 100644 --- a/WalletWasabi.Tests/RegressionTests/SendSpeedupTests.cs +++ b/WalletWasabi.Tests/RegressionTests/SendSpeedupTests.cs @@ -102,7 +102,7 @@ public async Task SendSpeedupTestsAsync() // Wait until the filter our previous transaction is present. var blockCount = await rpc.GetBlockCountAsync(); await setup.WaitForFiltersToBeProcessedAsync(TimeSpan.FromSeconds(120), blockCount); - wallet.Kitchen.Cook(password); + wallet.Password = password; TransactionBroadcaster broadcaster = new(network, bitcoinStore, httpClientFactory, walletManager); broadcaster.Initialize(nodes, rpc); diff --git a/WalletWasabi.Tests/UnitTests/Crypto/StringCipherTests.cs b/WalletWasabi.Tests/UnitTests/Crypto/StringCipherTests.cs deleted file mode 100644 index 87127882d81..00000000000 --- a/WalletWasabi.Tests/UnitTests/Crypto/StringCipherTests.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Security.Cryptography; -using System.Text; -using WalletWasabi.Crypto; -using Xunit; - -namespace WalletWasabi.Tests.UnitTests.Crypto; - -public class StringCipherTests -{ - /// - /// Tests that we can decrypt encrypted text correctly. - /// - [Theory] - [InlineData("hello", "password")] - [InlineData("hello world, how are you doing today?", "password")] - [InlineData("01234567890123456789012345678901234567890123456789012345678901234567890123456789", "password")] - [InlineData("foo@éóüö", "")] - [InlineData("foo@éóüöhellohellohellohellohello", "passwordpassword3232")] - public void RoundTripCipherTests(string toEncrypt, string password) - { - string encrypted = StringCipher.Encrypt(toEncrypt, password); - Assert.NotEqual(toEncrypt, encrypted); - - string decrypted = StringCipher.Decrypt(encrypted, password); - Assert.Equal(toEncrypt, decrypted); - } - - [Fact] - public void EncryptLongTextTest() - { - StringBuilder builder = new(); - - for (int i = 0; i < 1000000; i++) // 10MB - { - builder.Append("0123456789"); - } - - string password = "password"; - string toEncrypt = builder.ToString(); - string encrypted = StringCipher.Encrypt(toEncrypt, password); - Assert.NotEqual(toEncrypt, encrypted); - - string decrypted = StringCipher.Decrypt(encrypted, password); - Assert.Equal(toEncrypt, decrypted); - } - - [Fact] - public void InvalidDecryptTest() - { - string encrypted = StringCipher.Encrypt("123456789", "password"); - Assert.Throws(() => StringCipher.Decrypt(encrypted, "wrong-password")); - } - - [Fact] - public void AuthenticateMessageTest() - { - int count = 0; - int errorCount = 0; - - while (count < 3) - { - string password = "password"; - string plainText = "juan carlos"; - string encrypted = StringCipher.Encrypt(plainText, password); - - try - { - // This must fail because the password is wrong - var t = StringCipher.Decrypt(encrypted, "WRONG-PASSWORD"); - errorCount++; - } - catch (CryptographicException ex) - { - Assert.StartsWith("Message Authentication failed", ex.Message); - } - - count++; - } - - double rate = errorCount / (double)count; - Assert.True(rate is < 0.000001 and > (-0.000001)); - } -} diff --git a/WalletWasabi.Tests/UnitTests/WabiSabi/Backend/AliceTimeoutTests.cs b/WalletWasabi.Tests/UnitTests/WabiSabi/Backend/AliceTimeoutTests.cs index 2e117750b4a..f5971f5022a 100644 --- a/WalletWasabi.Tests/UnitTests/WabiSabi/Backend/AliceTimeoutTests.cs +++ b/WalletWasabi.Tests/UnitTests/WabiSabi/Backend/AliceTimeoutTests.cs @@ -8,7 +8,6 @@ using WalletWasabi.WabiSabi.Client.CoinJoin.Client; using WalletWasabi.WabiSabi.Client.RoundStateAwaiters; using WalletWasabi.WabiSabi.Models; -using WalletWasabi.Wallets; using Xunit; namespace WalletWasabi.Tests.UnitTests.WabiSabi.Backend; @@ -35,7 +34,7 @@ public async Task AliceRegistrationTimesOutAsync() await roundStateUpdater.StartAsync(testDeadlineCts.Token); // Register Alices. - KeyChain keyChain = new(km, new Kitchen(ingredients: "")); + KeyChain keyChain = new(km, ""); using CancellationTokenSource registrationCts = new(); Task task = AliceClient.CreateRegisterAndConfirmInputAsync(RoundState.FromRound(round), arenaClient, smartCoin, keyChain, roundStateUpdater, registrationCts.Token, registrationCts.Token, confirmationCancellationToken: testDeadlineCts.Token); diff --git a/WalletWasabi.Tests/UnitTests/WabiSabi/Client/ArenaClientTests.cs b/WalletWasabi.Tests/UnitTests/WabiSabi/Client/ArenaClientTests.cs index a67357a3c5a..cac817f770f 100644 --- a/WalletWasabi.Tests/UnitTests/WabiSabi/Client/ArenaClientTests.cs +++ b/WalletWasabi.Tests/UnitTests/WabiSabi/Client/ArenaClientTests.cs @@ -80,7 +80,7 @@ public async Task SignTransactionAsync() var password = "satoshi"; var km = ServiceFactory.CreateKeyManager(password); - var keyChain = new KeyChain(km, new Kitchen(password)); + var keyChain = new KeyChain(km, password); var destinationProvider = new InternalDestinationProvider(km); var coins = destinationProvider.GetNextDestinations(2, false) diff --git a/WalletWasabi.Tests/UnitTests/WabiSabi/Client/BobClientTests.cs b/WalletWasabi.Tests/UnitTests/WabiSabi/Client/BobClientTests.cs index 8e1ee4843b5..306b3c6fb40 100644 --- a/WalletWasabi.Tests/UnitTests/WabiSabi/Client/BobClientTests.cs +++ b/WalletWasabi.Tests/UnitTests/WabiSabi/Client/BobClientTests.cs @@ -68,7 +68,7 @@ public async Task RegisterOutputTestAsync() using RoundStateUpdater roundStateUpdater = new(TimeSpan.FromSeconds(2), wabiSabiApi); await roundStateUpdater.StartAsync(token); - var keyChain = new KeyChain(km, new Kitchen("")); + var keyChain = new KeyChain(km,""); var task = AliceClient.CreateRegisterAndConfirmInputAsync(RoundState.FromRound(round), aliceArenaClient, coin1, keyChain, roundStateUpdater, token, token, token); do diff --git a/WalletWasabi.Tests/UnitTests/WabiSabi/Client/KeyChainTests.cs b/WalletWasabi.Tests/UnitTests/WabiSabi/Client/KeyChainTests.cs index a7cbcd4e94c..6f0e8d8d9d0 100644 --- a/WalletWasabi.Tests/UnitTests/WabiSabi/Client/KeyChainTests.cs +++ b/WalletWasabi.Tests/UnitTests/WabiSabi/Client/KeyChainTests.cs @@ -15,7 +15,7 @@ public void SignTransactionTest() { var keyManager = KeyManager.CreateNew(out _, "", Network.Main); var destinationProvider = new InternalDestinationProvider(keyManager); - var keyChain = new KeyChain(keyManager, new Kitchen("")); + var keyChain = new KeyChain(keyManager,""); var coinDestination = destinationProvider.GetNextDestinations(1, false).First(); var coin = new Coin(BitcoinFactory.CreateOutPoint(), new TxOut(Money.Coins(1.0m), coinDestination)); diff --git a/WalletWasabi/Blockchain/TransactionBuilding/TransactionBuilderWalletExtensions.cs b/WalletWasabi/Blockchain/TransactionBuilding/TransactionBuilderWalletExtensions.cs index 81c78274efc..ab74a7c52b5 100644 --- a/WalletWasabi/Blockchain/TransactionBuilding/TransactionBuilderWalletExtensions.cs +++ b/WalletWasabi/Blockchain/TransactionBuilding/TransactionBuilderWalletExtensions.cs @@ -85,7 +85,7 @@ public static BuildTransactionResult BuildChangelessTransaction( label); var txRes = wallet.BuildTransaction( - wallet.Kitchen.SaltSoup(), + wallet.Password, intent, FeeStrategy.CreateFromFeeRate(feeRate), allowUnconfirmed: true, @@ -119,7 +119,7 @@ public static BuildTransactionResult BuildTransaction( label: label); var txRes = wallet.BuildTransaction( - password: wallet.Kitchen.SaltSoup(), + password: wallet.Password, payments: intent, feeStrategy: FeeStrategy.CreateFromFeeRate(feeRate), allowUnconfirmed: true, @@ -172,7 +172,7 @@ public static BuildTransactionResult BuildTransactionForSIB( label: label); var txRes = wallet.BuildTransaction( - password: wallet.Kitchen.SaltSoup(), + password: wallet.Password, payments: intent, feeStrategy: FeeStrategy.CreateFromConfirmationTarget(2), allowUnconfirmed: true, diff --git a/WalletWasabi/Blockchain/TransactionBuilding/TransactionModifierWalletExtensions.cs b/WalletWasabi/Blockchain/TransactionBuilding/TransactionModifierWalletExtensions.cs index c1a3d4e3acf..cda11d0012a 100644 --- a/WalletWasabi/Blockchain/TransactionBuilding/TransactionModifierWalletExtensions.cs +++ b/WalletWasabi/Blockchain/TransactionBuilding/TransactionModifierWalletExtensions.cs @@ -193,7 +193,7 @@ ownOutput is null var allowedInputs = transactionToSpeedUp.WalletInputs.Select(coin => coin.Outpoint); BuildTransactionResult rbf = wallet.BuildTransaction( - password: wallet.Kitchen.SaltSoup(), + password: wallet.Password, payments: new PaymentIntent(payments), feeStrategy: FeeStrategy.CreateFromFeeRate(rbfFeeRate), allowUnconfirmed: true, diff --git a/WalletWasabi/Crypto/StringCipher.cs b/WalletWasabi/Crypto/StringCipher.cs deleted file mode 100644 index 11423e8a03a..00000000000 --- a/WalletWasabi/Crypto/StringCipher.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using WalletWasabi.Crypto.Randomness; - -namespace WalletWasabi.Crypto; - -public static class StringCipher -{ - // This code is strongly inspired by https://tomrucki.com/posts/aes-encryption-in-csharp/ and - // https://netnix.org/2015/04/19/aes-encryption-with-hmac-integrity-in-java/ - private const int KeyByteSize = 128 / 8; - - private const int AesBlockByteSize = 128 / 8; - private const int PasswordSaltByteSize = 128 / 8; - private const int SignatureByteSize = 256 / 8; - - // This constant determines the number of iterations for the password bytes generation function. - private const int DerivationIterations = 1000; - - private const int MinimumEncryptedMessageByteSize = - PasswordSaltByteSize + // Auth salt - PasswordSaltByteSize + // Key salt - AesBlockByteSize + // IV - AesBlockByteSize + // Cipher text min length - SignatureByteSize; // Signature tag - - public static string Encrypt(string plainText, string passPhrase) - { - // Salt is randomly generated each time, but is prepended to encrypted cipher text - // so that the same Salt value can be used when decrypting. - byte[] iv = SecureRandom.Instance.GetBytes(AesBlockByteSize); - byte[] encryptionKeySalt = SecureRandom.Instance.GetBytes(PasswordSaltByteSize); - byte[] encryptionKey = DerivateKey(passPhrase, encryptionKeySalt); - - // Encrypt the plain text. - using var aes = CreateAES(encryptionKey); - byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText); - byte[] cipherTextBytes = aes.EncryptCbc(plainTextBytes, iv); - - // Authenticate. - byte[] authKeySalt = SecureRandom.Instance.GetBytes(PasswordSaltByteSize); - byte[] authKey = DerivateKey(passPhrase, authKeySalt); - byte[] result = MergeArrays(additionalCapacity: SignatureByteSize, encryptionKeySalt, iv, authKeySalt, cipherTextBytes); - - byte[] authCode = HMACSHA256.HashData(authKey, result[..^SignatureByteSize]); - authCode.CopyTo(result, result.Length - SignatureByteSize); - - return Convert.ToBase64String(result); - } - - public static string Decrypt(string encryptedString, string passPhrase) - { - byte[] encryptedData = Convert.FromBase64String(encryptedString); - - if (encryptedData is null || encryptedData.Length < MinimumEncryptedMessageByteSize) - { - throw new ArgumentException("Invalid length of encrypted data."); - } - - Span encryptionKeySalt = encryptedData.AsSpan(0, PasswordSaltByteSize); - Span iv = encryptedData.AsSpan(PasswordSaltByteSize, AesBlockByteSize); - Span authKeySalt = encryptedData.AsSpan(PasswordSaltByteSize + AesBlockByteSize, PasswordSaltByteSize); - Span authCode = encryptedData.AsSpan(encryptedData.Length - SignatureByteSize, SignatureByteSize); - - // Authenticate. - byte[] authKey = DerivateKey(passPhrase, authKeySalt); - - byte[] expectedAuthCode = HMACSHA256.HashData(authKey, encryptedData[..^SignatureByteSize]); - - if (!authCode.SequenceEqual(expectedAuthCode)) - { - throw new CryptographicException("Message Authentication failed. Message has been modified or wrong password."); - } - - // Decrypt. - int cipherTextIndex = authKeySalt.Length + encryptionKeySalt.Length + iv.Length; - int cipherTextLength = encryptedData.Length - cipherTextIndex - authCode.Length; - - byte[] encryptionKey = DerivateKey(passPhrase, encryptionKeySalt); - using var aes = CreateAES(encryptionKey); - byte[] plainTextBytes = aes.DecryptCbc(encryptedData.AsSpan(cipherTextIndex, cipherTextLength), iv); - - return Encoding.UTF8.GetString(plainTextBytes); - } - - private static byte[] DerivateKey(string passPhrase, Span salt) => - Rfc2898DeriveBytes.Pbkdf2(passPhrase, salt, DerivationIterations, HashAlgorithmName.SHA256, KeyByteSize); - - private static Aes CreateAES(byte[] encryptionKey) - { - Aes aes = Aes.Create(); - aes.Key = encryptionKey; - return aes; - } - - private static byte[] MergeArrays(int additionalCapacity, params byte[][] arrays) - { - byte[] merged = new byte[arrays.Sum(a => a.Length) + additionalCapacity]; - int mergeIndex = 0; - - for (int i = 0; i < arrays.GetLength(0); i++) - { - arrays[i].CopyTo(merged, mergeIndex); - mergeIndex += arrays[i].Length; - } - - return merged; - } -} diff --git a/WalletWasabi/WabiSabi/Client/KeyChain.cs b/WalletWasabi/WabiSabi/Client/KeyChain.cs index 327505e3bf2..9ee93ac8051 100644 --- a/WalletWasabi/WabiSabi/Client/KeyChain.cs +++ b/WalletWasabi/WabiSabi/Client/KeyChain.cs @@ -9,7 +9,7 @@ namespace WalletWasabi.WabiSabi.Client; public class KeyChain : IKeyChain { - public KeyChain(KeyManager keyManager, Kitchen kitchen) + public KeyChain(KeyManager keyManager, string password) { if (keyManager.IsWatchOnly) { @@ -17,20 +17,20 @@ public KeyChain(KeyManager keyManager, Kitchen kitchen) } KeyManager = keyManager; - Kitchen = kitchen; + Password = password; } private KeyManager KeyManager { get; } - private Kitchen Kitchen { get; } + private string Password { get; } private Key GetMasterKey() { - return KeyManager.GetMasterExtKey(Kitchen.SaltSoup()).PrivateKey; + return KeyManager.GetMasterExtKey(Password).PrivateKey; } public OwnershipProof GetOwnershipProof(IDestination destination, CoinJoinInputCommitmentData commitmentData) { - ExtKey hdKey = KeyManager.GetSecrets(Kitchen.SaltSoup(), destination.ScriptPubKey).SingleOrDefault() + ExtKey hdKey = KeyManager.GetSecrets(Password, destination.ScriptPubKey).SingleOrDefault() ?? throw new InvalidOperationException($"The signing key for '{destination.ScriptPubKey}' was not found."); Key masterKey = GetMasterKey(); BitcoinSecret secret = hdKey.GetBitcoinSecret(KeyManager.GetNetwork(), destination.ScriptPubKey); @@ -49,7 +49,7 @@ public Transaction Sign(Transaction transaction, Coin coin, PrecomputedTransacti var txInput = transaction.Inputs.AsIndexedInputs().FirstOrDefault(input => input.PrevOut == coin.Outpoint) ?? throw new InvalidOperationException("Missing input."); - ExtKey hdKey = KeyManager.GetSecrets(Kitchen.SaltSoup(), coin.ScriptPubKey).SingleOrDefault() + ExtKey hdKey = KeyManager.GetSecrets(Password, coin.ScriptPubKey).SingleOrDefault() ?? throw new InvalidOperationException($"The signing key for '{coin.ScriptPubKey}' was not found."); BitcoinSecret secret = hdKey.GetBitcoinSecret(KeyManager.GetNetwork(), coin.ScriptPubKey); diff --git a/WalletWasabi/Wallets/Kitchen.cs b/WalletWasabi/Wallets/Kitchen.cs deleted file mode 100644 index ba9603d06a8..00000000000 --- a/WalletWasabi/Wallets/Kitchen.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using WalletWasabi.Crypto; -using WalletWasabi.Crypto.Randomness; - -namespace WalletWasabi.Wallets; - -public class Kitchen -{ - public Kitchen(string? ingredients = null) - { - if (ingredients is { }) - { - Cook(ingredients); - } - } - - private string? Salt { get; set; } = null; - private string? Soup { get; set; } = null; - private object RefrigeratorLock { get; } = new(); - - [MemberNotNullWhen(returnValue: true, nameof(Salt), nameof(Soup))] - public bool HasIngredients => Salt is not null && Soup is not null; - - public string SaltSoup() - { - if (!HasIngredients) - { - throw new InvalidOperationException("Ingredients are missing."); - } - - string res; - lock (RefrigeratorLock) - { - res = StringCipher.Decrypt(Soup, Salt); - } - - Cook(res); - - return res; - } - - public void Cook(string ingredients) - { - lock (RefrigeratorLock) - { - ingredients ??= ""; - - Salt = RandomString.AlphaNumeric(21, secureRandom: true); - Soup = StringCipher.Encrypt(ingredients, Salt); - } - } - - public void CleanUp() - { - lock (RefrigeratorLock) - { - Salt = null; - Soup = null; - } - } -} diff --git a/WalletWasabi/Wallets/Wallet.cs b/WalletWasabi/Wallets/Wallet.cs index 7b5080596b0..3574acd0795 100644 --- a/WalletWasabi/Wallets/Wallet.cs +++ b/WalletWasabi/Wallets/Wallet.cs @@ -54,11 +54,6 @@ public Wallet( RuntimeParams.SetDataDir(dataDir); - if (!KeyManager.IsWatchOnly) - { - KeyChain = new KeyChain(KeyManager, Kitchen); - } - DestinationProvider = new InternalDestinationProvider(KeyManager); TransactionProcessor = transactionProcessor; @@ -112,10 +107,9 @@ private set public FilterModel? LastProcessedFilter => WalletFilterProcessor.LastProcessedFilter; public bool IsLoggedIn { get; private set; } + public string Password { get; set; } - public Kitchen Kitchen { get; } = new(); - - public IKeyChain? KeyChain { get; } + public IKeyChain? KeyChain { get; private set; } public IDestinationProvider DestinationProvider { get; } @@ -127,8 +121,7 @@ private set public bool IsMixable => State == WalletState.Started // Only running wallets - && !KeyManager.IsWatchOnly // that are not watch-only wallets - && Kitchen.HasIngredients; + && !KeyManager.IsWatchOnly; // that are not watch-only wallets public TimeSpan FeeRateMedianTimeFrame => TimeSpan.FromHours(KeyManager.FeeRateMedianTimeFrameHours); @@ -253,12 +246,13 @@ public bool TryLogin(string password, out string? compatibilityPasswordUsed) if (KeyManager.IsWatchOnly) { IsLoggedIn = true; - Kitchen.Cook(""); + Password = ""; } else if (PasswordHelper.TryPassword(KeyManager, password, out compatibilityPasswordUsed)) { IsLoggedIn = true; - Kitchen.Cook(compatibilityPasswordUsed ?? Guard.Correct(password)); + Password = compatibilityPasswordUsed ?? Guard.Correct(password); + KeyChain = new KeyChain(KeyManager, Password); } return IsLoggedIn; @@ -266,7 +260,6 @@ public bool TryLogin(string password, out string? compatibilityPasswordUsed) public void Logout() { - Kitchen.CleanUp(); IsLoggedIn = false; }