-
Notifications
You must be signed in to change notification settings - Fork 3
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
base: develop
Are you sure you want to change the base?
Conversation
comes up with the RequestSignAsync call
This reverts commit 234a3fa.
Needed for more than one vault Fixes HMAC verification failure
""" WalkthroughThe changes update two areas of the codebase. In the Java module descriptor ( In the native code ( In the Java class Possibly related PRs
Suggested reviewers
Note ⚡️ AI Code Reviews for VS Code, Cursor, WindsurfCodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback. Note ⚡️ Faster reviews with cachingCodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 30th. To opt out, configure ✨ Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (2)
src/main/native/org_cryptomator_windows_keychain_WindowsHello_Native.cpp (2)
28-28
: Concurrent caching strategy is sound but consider cache eviction.
Defining a global cache (keyCache
) guarded bycacheMutex
is correct to avoid data races. However, you may want to periodically invalidate or limit the cache size to prevent indefinite growth or stale entries persisting in memory.Also applies to: 30-47, 49-53, 55-55
200-234
: Signing the challenge and populating the cache is correct.
The logic for creating or opening the Windows Hello credential, signing the challenge, and then protecting/storing it in the cache is appropriate. Consider clarifying behavior for user-canceled prompts (line 215) if enhanced UX is desired.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/main/java/module-info.java
(1 hunks)src/main/native/org_cryptomator_windows_keychain_WindowsHello_Native.cpp
(3 hunks)
🧰 Additional context used
🧠 Learnings (1)
src/main/native/org_cryptomator_windows_keychain_WindowsHello_Native.cpp (1)
Learnt from: purejava
PR: cryptomator/integrations-win#83
File: src/main/native/org_cryptomator_windows_keychain_WinHello_Native.cpp:0-0
Timestamp: 2025-04-03T12:51:14.634Z
Learning: In `src/main/native/org_cryptomator_windows_keychain_WinHello_Native.cpp`, additional error handling and input validation in the `deriveEncryptionKey` function may not be necessary unless specific issues are identified.
🔇 Additional comments (8)
src/main/java/module-info.java (1)
23-23
: Provision of multiple keychain providers looks good.
AddingWindowsHelloKeychainAccess
alongsideWindowsProtectedKeychainAccess
appears consistent with the PR’s objective to provide both functionalities for keychain access.src/main/native/org_cryptomator_windows_keychain_WindowsHello_Native.cpp (7)
9-9
: Includes for cryptographic operations and hashing are appropriate.
These newly added headers (<wincrypt.h>
,<unordered_map>
,<mutex>
, and<functional>
) are necessary to implement the secure caching and memory protection functionalities.Also applies to: 10-11, 19-19
58-58
: Memory protection functions are correctly implemented.
UsingCryptProtectMemory
andCryptUnprotectMemory
to protect sensitive data is a reputable approach. Ensure these functions remain compatible with your targeted Windows versions.Also applies to: 59-65, 67-73
178-178
: Passing keyId by reference is a performance and clarity improvement.
Changingbool deriveEncryptionKey(const std::wstring keyId, ...)
tobool deriveEncryptionKey(const std::wstring& keyId, ...)
avoids unnecessary copies.
184-185
: Securely retrieving and unprotecting data from cache is well-handled.
The lock-based retrieval ensures thread-safe access, and the explicit unprotection step prevents storing plaintext in memory. This flow aligns with secure caching requirements.Also applies to: 187-198
236-238
: Converting signature data into an IBuffer is implemented correctly.
This step is straightforward and consistent with your subsequent HKDF usage.
241-242
: Zeroing out sensitive data helps prevent lingering in memory.
OverwritingsignatureData
after deriving the key is a good security practice, reducing the window of exposure.
244-244
: Final return statement concludes the flow gracefully.
No issues noted; the function clearly returns success at this point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/main/native/org_cryptomator_windows_keychain_WindowsHello_Native.cpp (2)
165-169
: Consider adding cache invalidation mechanism.While the current implementation works well for caching Windows Hello signatures, there's no mechanism to invalidate or expire cached entries. Consider adding time-based expiration or a way to clear the cache when certain security events occur.
static std::mutex cacheMutex; -static std::unordered_map<std::wstring, std::vector<uint8_t>> keyCache; +// Cache entry with timestamp for potential expiration +struct CacheEntry { + std::vector<uint8_t> data; + std::chrono::system_clock::time_point timestamp; +}; +static std::unordered_map<std::wstring, CacheEntry> keyCache; + +// Helper to clear cache (can be exposed via JNI if needed) +void clearCache() { + std::lock_guard<std::mutex> lock(cacheMutex); + keyCache.clear(); +}And then in the cache lookup code:
auto it = keyCache.find(keyId); if (it != keyCache.end()) { - signatureData = it->second; + // Optional: Check if entry has expired (e.g., after 30 minutes) + auto now = std::chrono::system_clock::now(); + if (now - it->second.timestamp > std::chrono::minutes(30)) { + keyCache.erase(it); + } else { + signatureData = it->second.data; + if (!UnprotectMemory(signatureData)) { + throw std::runtime_error("Failed to unprotect memory."); + } + foundInCache = true; + } - if (!UnprotectMemory(signatureData)) { - throw std::runtime_error("Failed to unprotect memory."); - } - foundInCache = true; }And when storing:
{ std::lock_guard<std::mutex> lock(cacheMutex); - keyCache[keyId] = protectedCopy; + keyCache[keyId] = {protectedCopy, std::chrono::system_clock::now()}; }
173-174
: Consider adding a JNI method to pre-cache keys.Since the core functionality already supports caching, consider exposing a JNI method that allows pre-caching keys during application startup or at strategic points in the user workflow to further reduce prompts.
This would allow the Java layer to proactively trigger the Windows Hello prompt at the most appropriate time for the user, further improving usability while maintaining security.
// Example JNI method to pre-cache a key jboolean JNICALL Java_org_cryptomator_windows_keychain_WindowsHello_00024Native_preCacheKey (JNIEnv* env, jobject obj, jbyteArray keyId, jbyteArray challenge) { queueSecurityPromptFocus(); try { std::vector<uint8_t> challengeVec = jbyteArrayToVector(env, challenge); winrt::init_apartment(winrt::apartment_type::single_threaded); auto toReleaseKeyId = (LPCWSTR)env->GetByteArrayElements(keyId, NULL); const std::wstring keyIdentifier(toReleaseKeyId); IBuffer key; bool result = deriveEncryptionKey(keyIdentifier, challengeVec, key); env->ReleaseByteArrayElements(keyId, (jbyte*)toReleaseKeyId, JNI_ABORT); return result ? JNI_TRUE : JNI_FALSE; } catch (...) { return JNI_FALSE; } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/main/native/org_cryptomator_windows_keychain_WindowsHello_Native.cpp
(3 hunks)
🧰 Additional context used
🧠 Learnings (1)
src/main/native/org_cryptomator_windows_keychain_WindowsHello_Native.cpp (2)
Learnt from: purejava
PR: cryptomator/integrations-win#83
File: src/main/native/org_cryptomator_windows_keychain_WinHello_Native.cpp:0-0
Timestamp: 2024-11-16T17:26:11.536Z
Learning: In `src/main/native/org_cryptomator_windows_keychain_WinHello_Native.cpp`, additional error handling and input validation in the `deriveEncryptionKey` function may not be necessary unless specific issues are identified.
Learnt from: purejava
PR: cryptomator/integrations-win#83
File: src/main/native/org_cryptomator_windows_keychain_WinHello_Native.cpp:0-0
Timestamp: 2024-11-18T20:38:43.547Z
Learning: In `deriveEncryptionKey` in `src/main/native/org_cryptomator_windows_keychain_WinHello_Native.cpp`, additional null checks on `result.Credential()` are unnecessary because the Windows API guarantees it is non-null or throws an exception otherwise.
🔇 Additional comments (9)
src/main/native/org_cryptomator_windows_keychain_WindowsHello_Native.cpp (9)
9-11
: Appropriate includes added for new functionality.The additions of
wincrypt.h
,unordered_map
, andmutex
are appropriate for implementing the secure caching mechanism with thread safety.
27-28
: Thread-safe cache implementation.Good implementation of a thread-safe cache using a mutex for synchronization and an unordered map for storage. The key design is appropriate - using the Windows Hello
keyId
as the map key and storing the protected signature data as the value.
31-38
: Memory protection implementation looks good.The
ProtectMemory
function properly usesCryptProtectMemory
with the correct protection level (CRYPTPROTECTMEMORY_SAME_PROCESS
). The function includes appropriate error handling and empty data validation.
40-46
: Memory unprotection implementation looks good.The
UnprotectMemory
function correctly implements the inverse operation usingCryptUnprotectMemory
with consistent protection level and error handling.
151-151
: Function signature improvement.Good change to pass the
keyId
parameter by const reference, which avoids unnecessary copying of the string.
157-171
: Thread-safe cache retrieval implementation.Well-structured implementation that:
- Uses a lock guard for thread safety
- Attempts to find the cached key
- Properly unprotects the memory if found
- Throws an exception with a clear error message if memory unprotection fails
This approach effectively reduces the number of Windows Hello prompts by reusing cached credentials.
173-206
: Well-implemented cache population logic.This code segment appropriately:
- Falls back to Windows Hello credential retrieval when not found in cache
- Handles different status codes from Windows Hello API
- Creates a protected copy of sensitive data
- Uses a lock guard for thread-safe cache updates
- Cleans up sensitive data by zeroing the buffer after use
The approach aligns well with the PR objective of reducing Windows Hello prompts while maintaining security.
208-215
: Good security practice for handling sensitive data.The code properly:
- Creates a signature buffer from the retrieved data
- Derives the encryption key using HKDF
- Clears the sensitive signature data by zeroing the buffer after use
This follows the principle of minimizing the lifetime of sensitive data in memory.
194-198
: Memory protection exception handling.Good practice to throw an exception when memory protection fails. This is a critical security operation and failing silently would be inappropriate. The exception with a descriptive message helps with troubleshooting.
bool ProtectMemory(std::vector<uint8_t>& data) { | ||
if (data.empty()) return false; | ||
if (!CryptProtectMemory(data.data(), static_cast<DWORD>(data.size()), CRYPTPROTECTMEMORY_SAME_PROCESS)) { | ||
return false; | ||
} | ||
return true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Verify memory alignment for CryptProtectMemory.
CryptProtectMemory
requires that the data length is a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE
(16 bytes). While the current implementation works with the signature data, ensure that all data passed to this function meets this requirement.
🏁 Script executed:
#!/bin/bash
# Verify if the signature data length is always a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE
# Search for signature data extraction
rg "signatureData = " -A 3 -B 3
Length of output: 1618
Ensure Block Alignment for CryptProtectMemory Input
CryptProtectMemory
mandates that the length of the buffer be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE
(16). Currently, ProtectMemory
is invoked directly on the raw data vector, which may not satisfy this requirement and can cause failures or silent padding issues.
Please update the code to pad or validate the data size before calling CryptProtectMemory
. For example:
bool ProtectMemory(std::vector<uint8_t>& data) {
if (data.empty()) return false;
// enforce 16-byte alignment
const size_t block = CRYPTPROTECTMEMORY_BLOCK_SIZE;
size_t pad = (block - (data.size() % block)) % block;
if (pad) {
data.insert(data.end(), pad, 0);
}
if (!CryptProtectMemory(data.data(), static_cast<DWORD>(data.size()), CRYPTPROTECTMEMORY_SAME_PROCESS)) {
return false;
}
return true;
}
• File: src/main/native/org_cryptomator_windows_keychain_WindowsHello_Native.cpp
Function: ProtectMemory
(around lines 32–37)
• Call sites: ensure any buffer passed in (e.g., signatureData
) is block-aligned or padded.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@purejava Since the we hand over the signature, which has not a documented defined length, the input data should be validated to be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE bytes. If this is not the case, throw an runtime_error with a more specific error message.
This is for the case Microsoft changes the signature size to a not multiple of 16
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tested this on a laptop with WindowsHello (fingerprint). Looks good, also tested it in combination with Cryptomator.
When i looked at the code, i think i found some issues about not zero'd arrays, but this is not part of this PR.
bool ProtectMemory(std::vector<uint8_t>& data) { | ||
if (data.empty()) return false; | ||
if (!CryptProtectMemory(data.data(), static_cast<DWORD>(data.size()), CRYPTPROTECTMEMORY_SAME_PROCESS)) { | ||
return false; | ||
} | ||
return true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@purejava Since the we hand over the signature, which has not a documented defined length, the input data should be validated to be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE bytes. If this is not the case, throw an runtime_error with a more specific error message.
This is for the case Microsoft changes the signature size to a not multiple of 16
if (!foundInCache) { | ||
auto result = KeyCredentialManager::RequestCreateAsync(keyId, KeyCredentialCreationOption::FailIfExists).get(); | ||
|
||
if (result.Status() == KeyCredentialStatus::CredentialAlreadyExists) { | ||
result = KeyCredentialManager::OpenAsync(keyId).get(); | ||
} | ||
return false; | ||
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move this to its own method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@infeo I implemented the changes:
- CRYPTPROTECTMEMORY_BLOCK_SIZE
- Move this to its own method
I could not spot any places, where zeroing vectors has been forgotten.
On testing, I noticed a missing piece on the cache implementation: when a signature is cached and a second vault created afterwards (with the cached signature), HMAC verification failed for the second vault, when you end the app, start it again and try to unlock the second vault first. This was due to a newly created challenge for the second vault.
This is fixed now too by using a fixed challenge in the WindowsHello
class, as discussed for this last part of the cache implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (3)
src/main/java/org/cryptomator/windows/keychain/WindowsHello.java (3)
9-14
: Document security considerations for using a fixed challengeUsing a fixed challenge rather than a dynamic one is a deliberate design choice to enable caching Windows Hello authentication results. While this improves usability by reducing authentication prompts, it's worth documenting this security trade-off directly in the code.
Consider adding a comment explaining the purpose and security implications of using a fixed challenge:
private final byte[] fixedChallenge = new byte[] { + // Using a fixed challenge enables caching of Windows Hello authentication results, + // reducing the number of prompts. The actual encryption key is still protected + // in memory using CryptProtectMemory in the native implementation. '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'A', 'B', 'C', 'D', 'E', 'F' };
21-22
: Document that the challenge parameter is ignoredThe method now uses the fixed challenge instead of the parameter, but the method signature remains unchanged for compatibility. This behavior should be documented to avoid confusion.
Consider adding a comment or updating the method signature:
@Override +// The challenge parameter is ignored and fixedChallenge is used instead to enable caching public byte[] encrypt(byte[] cleartext, byte[] challenge) { return Native.INSTANCE.setEncryptionKey(keyId, cleartext, fixedChallenge); }
26-27
: Document that the challenge parameter is ignoredThe decrypt method also now uses the fixed challenge instead of the parameter. As with the encrypt method, this behavior should be documented.
Consider adding a comment similar to the one suggested for encrypt:
@Override +// The challenge parameter is ignored and fixedChallenge is used instead to enable caching public byte[] decrypt(byte[] ciphertext, byte[] challenge) { return Native.INSTANCE.getEncryptionKey(keyId, ciphertext, fixedChallenge); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/main/java/org/cryptomator/windows/keychain/WindowsHello.java
(1 hunks)src/main/native/org_cryptomator_windows_keychain_WindowsHello_Native.cpp
(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/native/org_cryptomator_windows_keychain_WindowsHello_Native.cpp
🧰 Additional context used
🧠 Learnings (1)
src/main/java/org/cryptomator/windows/keychain/WindowsHello.java (1)
Learnt from: infeo
PR: cryptomator/integrations-win#83
File: src/main/java/org/cryptomator/windows/keychain/WinHello.java:29-41
Timestamp: 2024-11-11T15:58:12.072Z
Learning: In `src/main/java/org/cryptomator/windows/keychain/WinHello.java`, the `Native` class is private and only accessible within its enclosing class. Therefore, making `INSTANCE` private and adding a getter method is unnecessary.
🧬 Code Graph Analysis (1)
src/main/java/org/cryptomator/windows/keychain/WindowsHello.java (2)
src/main/java/org/cryptomator/windows/common/WinStrings.java (1)
WinStrings
(6-14)src/main/java/org/cryptomator/windows/keychain/WinDataProtection.java (1)
Native
(23-33)
🔇 Additional comments (1)
src/main/java/org/cryptomator/windows/keychain/WindowsHello.java (1)
6-48
: Implementation aligns well with PR objectives for secure cachingThe changes to use a fixed challenge instead of dynamic ones effectively support the goal of caching Windows Hello authentication results to reduce the number of prompts. This improves usability while maintaining security through secure memory protection.
For future maintenance, consider documenting the purpose of the implementation pattern and potentially refactoring the method signatures to remove unused parameters if backward compatibility becomes less of a concern.
I am glad to request this change to finalize the Windows Hello integration.
Based on an idea from @infeo
I implemented this as natively as possible. Knowing, that caching secure data is a tradeoff to security, this makes Windows Hello usable, as it reduces the prompts to a required minimum, e.g. on changing the password for a vault.
Basically, the Windows Hello prompt comes up once for every vault used.The sensitive information is keep in memory, but nevertheless protected byCryptProtectMemory
.So, after all, this is a gain in security and fits well to the new Touch ID feature on Mac.
This PR depends on a small change in Cryptomator, that the Hello keychain path property needs to be added to the
Environment
. I'll open a separate PR for this.Edit: depends on cryptomator/cryptomator#3808
Edit 2: The Windows Hello prompt does not come up for every vault used, this was changed by 6bda0f9.