Address rows and the address detail sheet no longer carry the WIF of every listed address for the whole session. The key is derived after the password gate and dropped when hidden, when the sheet closes or when the wallet closes. See ADR 0037.
258 lines
10 KiB
C#
258 lines
10 KiB
C#
using PalladiumWallet.App.Localization;
|
|
using PalladiumWallet.App.ViewModels;
|
|
using PalladiumWallet.Core.Chain;
|
|
using PalladiumWallet.Core.Crypto;
|
|
using PalladiumWallet.Core.Storage;
|
|
using PalladiumWallet.Core.Wallet;
|
|
|
|
namespace PalladiumWallet.Tests.App;
|
|
|
|
/// <summary>
|
|
/// Secret-handling gates in the UI layer: the seed and private keys must stay hidden until the
|
|
/// wallet password is supplied, and must be hidden again on close. Also settings, wallet info
|
|
/// and the remaining wizard flows.
|
|
/// </summary>
|
|
[Collection("AppPaths")]
|
|
public sealed class MainWindowViewModelSecurityTests : VmTestBase
|
|
{
|
|
[Fact]
|
|
public Task Seed_StaysHidden_UntilTheRightPasswordIsGiven() => UiSession.Run(() =>
|
|
{
|
|
var vm = new MainWindowViewModel();
|
|
RestoreFlow(vm, Mnemonic, "pw", "s1");
|
|
vm.OpenWalletInfoCommand.Execute(null);
|
|
Assert.True(vm.WalletInfoHasSeed);
|
|
Assert.True(vm.WalletInfoSeedNeedsPassword);
|
|
Assert.Equal("", vm.WalletInfoSeedText);
|
|
|
|
vm.WalletInfoSeedPasswordInput = "nope";
|
|
vm.RevealSeedCommand.Execute(null);
|
|
Assert.False(vm.IsWalletInfoSeedRevealed);
|
|
Assert.Equal(Loc.Tr("msg.wrongpassword"), vm.WalletInfoSeedError);
|
|
Assert.Equal("", vm.WalletInfoSeedText);
|
|
|
|
vm.WalletInfoSeedPasswordInput = "pw";
|
|
vm.RevealSeedCommand.Execute(null);
|
|
Assert.True(vm.IsWalletInfoSeedRevealed);
|
|
Assert.Equal(Mnemonic, vm.WalletInfoSeedText);
|
|
|
|
vm.CloseWalletInfoCommand.Execute(null); // closing wipes the revealed state
|
|
Assert.False(vm.IsWalletInfoSeedRevealed);
|
|
Assert.Equal("", vm.WalletInfoSeedText);
|
|
});
|
|
|
|
[Fact]
|
|
public Task WatchOnlyWallet_HasNoSeedToReveal() => UiSession.Run(() =>
|
|
{
|
|
var vm = new MainWindowViewModel();
|
|
var address = HdAccount.FromMnemonic(ParseMnemonic(), null, ScriptKind.NativeSegwit, ChainProfiles.Mainnet)
|
|
.GetReceiveAddress(0).ToString();
|
|
vm.WizardStartImportAddressCommand.Execute(null);
|
|
vm.ImportAddressInput = address;
|
|
vm.WizardNextFromImportAddressCommand.Execute(null);
|
|
vm.EncryptWallet = false;
|
|
vm.CreateOrRestoreCommand.Execute(null);
|
|
vm.OpenWalletInfoCommand.Execute(null);
|
|
|
|
Assert.False(vm.WalletInfoHasSeed);
|
|
vm.RevealSeedCommand.Execute(null);
|
|
Assert.False(vm.IsWalletInfoSeedRevealed);
|
|
});
|
|
|
|
[Fact]
|
|
public Task PrivateKey_IsRevealedOnlyAfterThePassword_AndOnlyWhenTheWalletHasKeys() => UiSession.Run(async () =>
|
|
{
|
|
var account = HdAccount.FromMnemonic(ParseMnemonic(), null, ScriptKind.NativeSegwit, ChainProfiles.Mainnet);
|
|
WireFundedChain(account.GetReceiveAddress(0), 1_000_000);
|
|
var vm = new MainWindowViewModel();
|
|
RestoreFlow(vm, Mnemonic, "pw", "pk");
|
|
await Eventually(() => !vm.IsSyncing && vm.Addresses.Count > 0);
|
|
|
|
var row = vm.Addresses.First(a => !a.IsChange && a.Indice == 0);
|
|
Assert.True(row.HasPrivKey);
|
|
var wif = account.GetPrivateKey(false, 0)!.GetWif(PalladiumNetworks.For(NetKind.Mainnet)).ToString();
|
|
|
|
// No row holds a private key before the reveal (reflection over every string property).
|
|
Assert.All(vm.Addresses, r => Assert.DoesNotContain(wif, StringValues(r)));
|
|
Assert.DoesNotContain(typeof(AddressRow).GetProperties(), p => p.Name.Contains("PrivKey") && p.PropertyType == typeof(string));
|
|
|
|
vm.ShowAddressInfo(row);
|
|
Assert.DoesNotContain(wif, StringValues(vm.AddressInfo!));
|
|
Assert.Equal("", vm.RevealedPrivKey);
|
|
|
|
vm.RequestPrivKeyRevealCommand.Execute(null);
|
|
Assert.True(vm.IsPrivKeyPromptOpen);
|
|
Assert.False(vm.IsAddressPrivKeyRevealed);
|
|
Assert.Equal("", vm.RevealedPrivKey);
|
|
|
|
vm.PrivKeyPromptPassword = "wrong";
|
|
vm.ConfirmPrivKeyPasswordCommand.Execute(null);
|
|
Assert.False(vm.IsAddressPrivKeyRevealed);
|
|
Assert.Equal(Loc.Tr("msg.wrongpassword"), vm.PrivKeyPromptError);
|
|
|
|
vm.PrivKeyPromptPassword = "pw";
|
|
vm.ConfirmPrivKeyPasswordCommand.Execute(null);
|
|
Assert.True(vm.IsAddressPrivKeyRevealed);
|
|
Assert.False(vm.IsPrivKeyPromptOpen);
|
|
Assert.Equal(wif, vm.RevealedPrivKey);
|
|
Assert.DoesNotContain(wif, StringValues(vm.AddressInfo!)); // still not stored in the info
|
|
Assert.All(vm.Addresses, r => Assert.DoesNotContain(wif, StringValues(r)));
|
|
|
|
vm.HideAddressPrivKeyCommand.Execute(null); // hiding drops the key
|
|
Assert.False(vm.IsAddressPrivKeyRevealed);
|
|
Assert.Equal("", vm.RevealedPrivKey);
|
|
|
|
vm.RequestPrivKeyRevealCommand.Execute(null); // reveal again, then close the sheet
|
|
vm.PrivKeyPromptPassword = "pw";
|
|
vm.ConfirmPrivKeyPasswordCommand.Execute(null);
|
|
Assert.Equal(wif, vm.RevealedPrivKey);
|
|
vm.CloseAddressInfoCommand.Execute(null); // closing the sheet re-hides everything
|
|
Assert.Equal("", vm.RevealedPrivKey);
|
|
vm.ShowAddressInfo(row);
|
|
Assert.False(vm.IsAddressPrivKeyRevealed);
|
|
});
|
|
|
|
[Fact]
|
|
public Task ClosingTheWallet_DropsARevealedPrivateKey() => UiSession.Run(async () =>
|
|
{
|
|
var account = HdAccount.FromMnemonic(ParseMnemonic(), null, ScriptKind.NativeSegwit, ChainProfiles.Mainnet);
|
|
WireFundedChain(account.GetReceiveAddress(0), 1_000_000);
|
|
var vm = new MainWindowViewModel();
|
|
RestoreFlow(vm, Mnemonic, null, "pk2");
|
|
await Eventually(() => !vm.IsSyncing && vm.Addresses.Count > 0);
|
|
|
|
vm.ShowAddressInfo(vm.Addresses.First(a => !a.IsChange && a.Indice == 0));
|
|
vm.RequestPrivKeyRevealCommand.Execute(null); // no password: revealed directly
|
|
Assert.NotEqual("", vm.RevealedPrivKey);
|
|
|
|
vm.CloseWalletCommand.Execute(null);
|
|
Assert.Equal("", vm.RevealedPrivKey);
|
|
Assert.False(vm.IsAddressPrivKeyRevealed);
|
|
Assert.Null(vm.AddressInfo);
|
|
});
|
|
|
|
private static IEnumerable<string> StringValues(object o) =>
|
|
o.GetType().GetProperties()
|
|
.Where(p => p.PropertyType == typeof(string) && p.GetIndexParameters().Length == 0)
|
|
.Select(p => (string?)p.GetValue(o) ?? "");
|
|
|
|
[Fact]
|
|
public Task WatchOnlyAddressRows_NeverCarryAPrivateKey() => UiSession.Run(async () =>
|
|
{
|
|
var account = HdAccount.FromMnemonic(ParseMnemonic(), null, ScriptKind.NativeSegwit, ChainProfiles.Mainnet);
|
|
WireFundedChain(account.GetReceiveAddress(0), 1_000_000);
|
|
var vm = new MainWindowViewModel();
|
|
vm.WizardStartImportXkeyCommand.Execute(null);
|
|
vm.ImportXkeyInput = account.ToSlip132();
|
|
vm.WizardNextFromImportXkeyCommand.Execute(null);
|
|
vm.EncryptWallet = false;
|
|
vm.CreateOrRestoreCommand.Execute(null);
|
|
await Eventually(() => !vm.IsSyncing && vm.Addresses.Count > 0);
|
|
|
|
Assert.All(vm.Addresses, a => Assert.False(a.HasPrivKey));
|
|
vm.ShowAddressInfo(vm.Addresses[0]);
|
|
vm.RequestPrivKeyRevealCommand.Execute(null);
|
|
Assert.False(vm.IsAddressPrivKeyRevealed);
|
|
Assert.False(vm.IsPrivKeyPromptOpen);
|
|
});
|
|
|
|
[Fact]
|
|
public Task Units_AndLanguages_AreAppliedAndPersisted() => UiSession.Run(() =>
|
|
{
|
|
var vm = new MainWindowViewModel();
|
|
|
|
vm.SetUnitCommand.Execute("sat");
|
|
Assert.True(vm.IsUnitSat);
|
|
Assert.Equal("sat", vm.UnitLabel);
|
|
Assert.Equal("sat", AppConfig.Load(AppPaths.ConfigPath()).Unit);
|
|
|
|
foreach (var lang in Loc.Languages)
|
|
{
|
|
vm.SetLanguageCommand.Execute(lang);
|
|
Assert.Equal(lang, vm.Loc.Language);
|
|
}
|
|
Assert.True(vm.IsLangZh);
|
|
Assert.False(vm.IsLangIt);
|
|
});
|
|
|
|
[Fact]
|
|
public Task SettingsFromDisk_AreRestoredByANewViewModel() => UiSession.Run(() =>
|
|
{
|
|
new MainWindowViewModel().SetLanguageCommand.Execute("de");
|
|
|
|
var vm2 = new MainWindowViewModel();
|
|
|
|
Assert.Equal("de", vm2.Loc.Language);
|
|
Assert.True(vm2.IsLangDe);
|
|
});
|
|
|
|
[Fact]
|
|
public Task Wizard_BackNavigation_ReturnsToThePreviousStep() => UiSession.Run(() =>
|
|
{
|
|
var vm = new MainWindowViewModel();
|
|
vm.WizardStartRestoreCommand.Execute(null);
|
|
vm.MnemonicInput = Mnemonic;
|
|
vm.WizardNextFromWordsCommand.Execute(null);
|
|
Assert.True(vm.IsStepPassphrase);
|
|
|
|
vm.WizardBackCommand.Execute(null);
|
|
Assert.True(vm.IsStepWords);
|
|
vm.WizardBackCommand.Execute(null);
|
|
Assert.True(vm.IsStepStart);
|
|
});
|
|
|
|
[Fact]
|
|
public Task ImportWif_CreatesAnImportedKeyWallet_AndRejectsGarbage() => UiSession.Run(() =>
|
|
{
|
|
var key = new NBitcoin.Key();
|
|
var wif = key.GetWif(PalladiumNetworks.Mainnet).ToString();
|
|
var vm = new MainWindowViewModel();
|
|
|
|
vm.WizardStartImportWifCommand.Execute(null);
|
|
vm.ImportWifInput = "definitely-not-a-wif";
|
|
vm.WizardNextFromImportWifCommand.Execute(null);
|
|
Assert.True(vm.IsStepImportWif);
|
|
|
|
vm.ImportWifInput = wif;
|
|
vm.WizardNextFromImportWifCommand.Execute(null);
|
|
vm.EncryptWallet = true;
|
|
vm.PasswordInput = vm.ConfirmPasswordInput = "pw";
|
|
vm.CreateOrRestoreCommand.Execute(null);
|
|
|
|
Assert.True(vm.IsWalletOpen);
|
|
Assert.Contains("imported", vm.NetworkInfo);
|
|
var stored = WalletStore.Load(Assert.Single(AppPaths.WalletFiles(NetKind.Mainnet)), "pw");
|
|
Assert.Equal(wif, Assert.Single(stored.WifKeys!));
|
|
});
|
|
|
|
[Fact]
|
|
public Task ImportXprv_CreatesASpendableWallet() => UiSession.Run(() =>
|
|
{
|
|
var account = HdAccount.FromMnemonic(ParseMnemonic(), null, ScriptKind.NativeSegwit, ChainProfiles.Mainnet);
|
|
var vm = new MainWindowViewModel();
|
|
vm.WizardStartImportXkeyCommand.Execute(null);
|
|
vm.ImportXkeyInput = account.ToSlip132Private();
|
|
vm.WizardNextFromImportXkeyCommand.Execute(null);
|
|
vm.EncryptWallet = false;
|
|
vm.CreateOrRestoreCommand.Execute(null);
|
|
|
|
Assert.True(vm.IsWalletOpen);
|
|
Assert.False(vm.IsWatchOnlyAccount);
|
|
});
|
|
|
|
[Fact]
|
|
public Task ClosingTheWallet_ReleasesTheLock_SoItCanBeReopened() => UiSession.Run(() =>
|
|
{
|
|
var vm = new MainWindowViewModel();
|
|
RestoreFlow(vm, Mnemonic, "pw", "relock");
|
|
var path = Assert.Single(AppPaths.WalletFiles(NetKind.Mainnet));
|
|
Assert.Null(WalletLock.TryAcquire(path)); // held while open
|
|
|
|
vm.CloseWalletCommand.Execute(null);
|
|
|
|
using var l = WalletLock.TryAcquire(path); // released on close
|
|
Assert.NotNull(l);
|
|
Assert.False(vm.IsWalletOpen);
|
|
});
|
|
}
|