Skip to content

Add a secure cache to Windows Hello to make it usable (amount of prompts) #105

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

Open
wants to merge 17 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
opens org.cryptomator.windows.quickaccess to org.cryptomator.integrations.api;

provides AutoStartProvider with WindowsAutoStart;
provides KeychainAccessProvider with WindowsProtectedKeychainAccess;
provides KeychainAccessProvider with WindowsProtectedKeychainAccess, WindowsHelloKeychainAccess;
provides UiAppearanceProvider with WinUiAppearanceProvider;
provides RevealPathService with ExplorerRevealPathService;
provides QuickAccessService with ExplorerQuickAccessService;
Expand Down
10 changes: 8 additions & 2 deletions src/main/java/org/cryptomator/windows/keychain/WindowsHello.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,25 @@
class WindowsHello implements WindowsKeychainAccessBase.PassphraseCryptor {

private final byte[] keyId;
private final byte[] fixedChallenge = new byte[] {
'1', '2', '3', '4',
'5', '6', '7', '8',
'9', '0', 'A', 'B',
'C', 'D', 'E', 'F'
};

public WindowsHello(String keyId) {
this.keyId = WinStrings.getNullTerminatedUTF16Representation(keyId);
}

@Override
public byte[] encrypt(byte[] cleartext, byte[] challenge) {
return Native.INSTANCE.setEncryptionKey(keyId, cleartext, challenge);
return Native.INSTANCE.setEncryptionKey(keyId, cleartext, fixedChallenge);
}

@Override
public byte[] decrypt(byte[] ciphertext, byte[] challenge) {
return Native.INSTANCE.getEncryptionKey(keyId, ciphertext, challenge);
return Native.INSTANCE.getEncryptionKey(keyId, ciphertext, fixedChallenge);
}

public boolean isSupported() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
#include <winrt/Windows.Security.Cryptography.Core.h>
#include <winrt/Windows.Storage.Streams.h>
#include <windows.h>
#include <wincrypt.h>
#include <unordered_map>
#include <mutex>
#include <thread>
#include <chrono>
#include <string>
Expand All @@ -21,8 +24,33 @@ using namespace Windows::Security::Cryptography::Core;
using namespace Windows::Storage::Streams;

static std::atomic<int> g_promptFocusCount{ 0 };
static std::mutex cacheMutex;
static std::unordered_map<std::wstring, std::vector<uint8_t>> keyCache;
static auto HKDF_INFO = L"org.cryptomator.windows.keychain.windowsHello";

// Helper methods to handle KeyCredential
bool ProtectMemory(std::vector<uint8_t>& data) {
if (data.empty()) return false;
if (data.size() % CRYPTPROTECTMEMORY_BLOCK_SIZE != 0) {
throw std::runtime_error("Data size must be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE (16 bytes).");
}
if (!CryptProtectMemory(data.data(), static_cast<DWORD>(data.size()), CRYPTPROTECTMEMORY_SAME_PROCESS)) {
return false;
}
return true;
}

bool UnprotectMemory(std::vector<uint8_t>& data) {
if (data.empty()) return false;
if (data.size() % CRYPTPROTECTMEMORY_BLOCK_SIZE != 0) {
throw std::runtime_error("Data size must be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE (16 bytes).");
}
if (!CryptUnprotectMemory(data.data(), static_cast<DWORD>(data.size()), CRYPTPROTECTMEMORY_SAME_PROCESS)) {
return false;
}
return true;
}

// Helper methods for conversion
std::vector<uint8_t> jbyteArrayToVector(JNIEnv* env, jbyteArray array) {
if (array == nullptr) {
Expand Down Expand Up @@ -126,37 +154,81 @@ IBuffer DeriveKeyUsingHKDF(const IBuffer& inputData, const IBuffer& salt, uint32
}


bool deriveEncryptionKey(const std::wstring keyId, const std::vector<uint8_t>& challenge, IBuffer& key) {
// Sign the challenge with the user's Windows Hello credentials
bool retrieveAndCacheSignatureData(const std::wstring& keyId, const IBuffer& challengeBuffer, std::vector<uint8_t>& signatureData) {
auto result = KeyCredentialManager::RequestCreateAsync(keyId, KeyCredentialCreationOption::FailIfExists).get();

if (result.Status() == KeyCredentialStatus::CredentialAlreadyExists) {
result = KeyCredentialManager::OpenAsync(keyId).get();
} else if (result.Status() != KeyCredentialStatus::Success) {
std::cerr << "Failed to retrieve Windows Hello credential." << std::endl;
return false;
}

const auto signature = result.Credential().RequestSignAsync(challengeBuffer).get();

if (signature.Status() != KeyCredentialStatus::Success) {
if (signature.Status() != KeyCredentialStatus::UserCanceled) {
std::cerr << "Failed to sign challenge using Windows Hello." << std::endl;
}
return false;
}

signatureData = iBufferToVector(signature.Result());
std::vector<uint8_t> protectedCopy = signatureData;

if (!ProtectMemory(protectedCopy)) {
throw std::runtime_error("Failed to protect memory.");
}

// Store in cache
{
std::lock_guard<std::mutex> lock(cacheMutex);
keyCache[keyId] = protectedCopy;
}

std::fill(protectedCopy.begin(), protectedCopy.end(), 0);
return true;
}


bool deriveEncryptionKey(const std::wstring& keyId, const std::vector<uint8_t>& challenge, IBuffer& key) {

auto challengeBuffer = CryptographicBuffer::CreateFromByteArray(
array_view<const uint8_t>(challenge.data(), challenge.size()));

try {
// The first time this is used a key-pair will be generated using the common name
auto result = KeyCredentialManager::RequestCreateAsync(keyId,
KeyCredentialCreationOption::FailIfExists).get();

if (result.Status() == KeyCredentialStatus::CredentialAlreadyExists) {
result = KeyCredentialManager::OpenAsync(keyId).get();
}
else if (result.Status() != KeyCredentialStatus::Success) {
std::cerr << "Failed to retrieve Windows Hello credential." << std::endl;
return false;
std::vector<uint8_t> signatureData;
bool foundInCache = false;

{
// Lock for thread safety
std::lock_guard<std::mutex> lock(cacheMutex);
auto it = keyCache.find(keyId);
if (it != keyCache.end()) {
signatureData = it->second;
if (!UnprotectMemory(signatureData)) {
throw std::runtime_error("Failed to unprotect memory.");
}
foundInCache = true;
}
}

const auto signature = result.Credential().RequestSignAsync(challengeBuffer).get();
if (signature.Status() != KeyCredentialStatus::Success) {
if (signature.Status() != KeyCredentialStatus::UserCanceled) {
std::cerr << "Failed to sign challenge using Windows Hello." << std::endl;
if (!foundInCache) {
if (!retrieveAndCacheSignatureData(keyId, challengeBuffer, signatureData)) {
return false;
}
return false;
}

auto signatureBuffer = CryptographicBuffer::CreateFromByteArray(
array_view<const uint8_t>(signatureData.data(), signatureData.size()));

// Derive the encryption/decryption key using HKDF
const auto response = signature.Result();
IBuffer info = CryptographicBuffer::ConvertStringToBinary(HKDF_INFO, BinaryStringEncoding::Utf8);
key = DeriveKeyUsingHKDF(response, challengeBuffer, 32, info); // needs to be 32 bytes for SHA256
return true;
key = DeriveKeyUsingHKDF(signatureBuffer, challengeBuffer, 32, info); // needs to be 32 bytes for SHA256
std::fill(signatureData.begin(), signatureData.end(), 0);

return true;
}
catch (winrt::hresult_error const& ex) {
std::cerr << winrt::to_string(ex.message()) << std::endl;
Expand Down