Skip to content

secure storage windows#672

Open
mistic0xb wants to merge 1 commit intoblock-core:mainfrom
mistic0xb:secure-storage
Open

secure storage windows#672
mistic0xb wants to merge 1 commit intoblock-core:mainfrom
mistic0xb:secure-storage

Conversation

@mistic0xb
Copy link
Collaborator

@mistic0xb mistic0xb commented Feb 24, 2026

Secure Wallet Storage Implementation (Windows)

Overview

This PR implements passwordless wallet encryption using Windows DPAPI (Data Protection API) and AES-256-GCM authenticated encryption. Users no longer need to enter or remember encryption passwords when creating or importing wallets.

How It Works

Two-Layer Encryption Architecture

The implementation uses an envelope encryption pattern with two distinct layers:

  1. OS-Level Protection (DPAPI)

    • A unique 256-bit master key is generated per wallet
    • The master key is encrypted by Windows DPAPI bound to the current user account
    • DPAPI-encrypted keys are stored in %AppData%\Angor\secure\{walletId}.key
    • Keys can only be decrypted by the same Windows user on the same machine
  2. Wallet Data Encryption (AES-GCM)

    • The master key encrypts wallet seed words and metadata using AES-256-GCM
    • GCM mode provides authenticated encryption (AEAD) — detects tampering automatically
    • Encrypted wallet data is stored in the existing LiteDB database

Wallet Creation Flow

  1. User enters seed words -> no password prompt
  2. System generates a random 256-bit master key
  3. Master key is encrypted with DPAPI -> saved to %AppData%\Angor\secure\{walletId}.key
  4. Master key encrypts wallet data with AES-GCM
  5. Master key bytes are cleared from memory

Wallet Access Flow

  1. Application loads encrypted wallet
  2. System reads DPAPI-encrypted key file
  3. DPAPI decrypts the master key silently (no user interaction)
  4. Master key decrypts wallet data with AES-GCM
  5. Seed words returned to application

Security Properties

What's Protected:

  • Master keys are bound to Windows user + machine (DPAPI CurrentUser scope)
  • Additional entropy per wallet prevents key reuse attacks
  • AES-GCM authentication tags detect any tampering with encrypted wallet data
  • Sensitive data cleared from memory after use
  • Key files marked as hidden system files

Platform Support

Current: Windows only (DPAPI)
Future: Cross-platform support planned via platform-specific secure storage:

  • macOS: Keychain
  • Linux
  • Android: Android Keystore
  • iOS: iOS Keychain

The AES-GCM encryption layer remains identical across platforms — only the master key storage mechanism changes.

Breaking Changes

  • IWalletFactory.CreateWallet signature changed — removed encryptionKey parameter
  • UI wizard flows updated — removed encryption password step
  • Existing wallets encrypted with user passwords are not migrated automatically — users must re-import from seed words

Why Two Layers?

This approach separates concerns:

  • DPAPI layer: Where the secret lives (OS-managed, platform-specific)
  • AES-GCM layer: What it protects (portable, platform-independent)

Benefits:

  • Easy to swap DPAPI for Apple-Keychain/Android-Keystore/Linux when going cross-platform

Testing

Manual testing performed on Windows 11. Verify:

  • Wallet creation completes without password prompt
  • Wallet loads on app restart without password
  • .key files created in %AppData%\Angor\secure\
  • Multiple wallets each get unique keys
  • Import wallet flow works without password

added secure storage (windows)

secure-storage for windows woking
Task<Result<Address>> GetNextReceiveAddress(WalletId id);
Task<Result<WalletId>> CreateWallet(string name, string seedwords, Maybe<string> passphrase, string encryptionKey, BitcoinNetwork network);
Task<Result<WalletId>> CreateWallet(string name, string encryptionKey, BitcoinNetwork network);
Task<Result<WalletId>> CreateWallet(string name, string seedwords, Maybe<string> passphrase, BitcoinNetwork network);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should leave the existing way as well?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, this should be an overload.

.Bind(wallet => walletEncryption.Decrypt(wallet, "DEFAULT")) // Try to decrypt the wallet with the default encryption key ("DEFAULT")
.Map(data => (data.SeedWords, Maybe<string>.None)) // On success, return its seedwords (assume no passphrase)
.Compensate(_ => provider.RequestSensitiveData(walletId)); // On failure, delegate to the flow to the inner provider. This is, the default encryption key failed to decrypt the wallet didn't work.
.Bind(wallets => wallets.TryFirst().ToResult("Wallet not found"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

has something changed here? or only removed comments?

// Set file attributes to hidden and system for additional security
File.SetAttributes(keyFilePath, FileAttributes.ReadOnly | FileAttributes.System);

Console.WriteLine($"[SecureStorage] Master key stored for wallet: {walletId}");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have loggers so use that instead of console

/// Stores a master encryption key securely using Windows DPAPI.
/// If no key is provided, generates a new one.
public async Task<Result<string>> StoreMasterKeyAsync(
string walletId,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the masterkey is per wallet? I am not sure we need that level of separation we can just use one master key for all wallets I suppose?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it is not such a big deal but it doeas put a dependency on the database, we must know the wallet id in order to decrypt a wallet.

if (encryptResult.IsFailure)
return Result.Failure<Domain.Wallet>(encryptResult.Error);

// remove existing entry with same ID before saving — safe on retry
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove existing entry with same ID before saving — safe on retry

what is this comment exactly not sure it is even correct?


private async Task<Result> SaveEncryptedWalletToStoreAsync(string name, string encryptionKey, WalletData walletData,
WalletId walletId)
private async Task<Result> SaveEncryptedWalletToStoreAsync(string name, string encryptionKey, WalletData walletData, WalletId walletId)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here we have an encryptionKey?

services.AddSingleton<ITransactionHistory, TransactionHistory>();
services.TryAddSingleton<IWalletAccountBalanceService, WalletAccountBalanceService>();
services.AddHttpClient();
services.AddSingleton<ISecureStorage, DpapiSecureStorage>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about none windows devices?


private static string GenerateMasterKey()
{
using var rng = RandomNumberGenerator.Create();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you need to use the cryptographically secure random number generator here

public async Task<Result<WalletId>> CreateWallet(string name, string seedWords, Maybe<string> passphrase , BitcoinNetwork network)
{
var wallet = await walletFactory.CreateWallet(name ?? SingleWalletName, seedWords, passphrase, encryptionKey, network);
var wallet = await walletFactory.CreateWallet(name ?? SingleWalletName, seedWords, passphrase, network);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you remove this we will only support Windows. you need to have a check if we can create without a password perhaps.

public interface IWalletFactory
{
Task<Result<Domain.Wallet>> CreateWallet(string name, string seedwords, Maybe<string> passphrase, string encryptionKey, BitcoinNetwork network);
Task<Result<Domain.Wallet>> CreateWallet(string name, string seedwords, Maybe<string> passphrase, BitcoinNetwork network);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have both options here.

services.AddSingleton<ITransactionHistory, TransactionHistory>();
services.TryAddSingleton<IWalletAccountBalanceService, WalletAccountBalanceService>();
services.AddHttpClient();
services.AddSingleton<ISecureStorage, DpapiSecureStorage>();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should have a way to check the OS so we know it works as well

public Task<Result<IWallet>> GetOrCreate()
{
return walletAppService.CreateWallet("<default>", GetUniqueId(), bitcoinNetwork())
return walletAppService.CreateWallet("<default>", bitcoinNetwork())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete the method GetUniqueId as well

Copy link
Collaborator

@DavidGershony DavidGershony left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs some changes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants