Skip to content

Commit

Permalink
Merge pull request #83 from purejava/hello
Browse files Browse the repository at this point in the history
Add Windows Hello support
  • Loading branch information
infeo authored Dec 19, 2024
2 parents 7b3c84c + 3ac569d commit 5478dbc
Show file tree
Hide file tree
Showing 29 changed files with 1,316 additions and 354 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
*.class
*.jar
*.dll
*.obj
*.lib
*.exp

# Maven #
target/
Expand Down
5 changes: 5 additions & 0 deletions .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ install:
$(HEADERS) $(SOURCES) \
-link -NXCOMPAT -DYNAMICBASE \
-implib:target/integrations.lib \
crypt32.lib shell32.lib ole32.lib uuid.lib user32.lib Advapi32.lib
crypt32.lib shell32.lib ole32.lib uuid.lib user32.lib Advapi32.lib windowsapp.lib
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Windows-specific implementations of [integrations-api](https://github.com/crypto

This project uses the following JVM properties:
* `cryptomator.integrationsWin.autoStartShellLinkName` - Name of the shell link, which is placed in the Windows startup folder to start application on user login
* `cryptomator.integrationsWin.windowsHelloKeyId` - Identifier for the Windows Hello keypair
* `cryptomator.integrationsWin.windowsHelloKeychainPaths` - Locations of the file-based windowsHello keychain
* `cryptomator.integrationsWin.keychainPaths` - List of file paths, which are checked for data encrypted with the Windows data protection api

## Building
Expand Down
4 changes: 3 additions & 1 deletion integrations-win.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -116,13 +116,15 @@
<ItemGroup>
<ClInclude Include="src\main\headers\org_cryptomator_windows_autostart_WinShellLinks_Native.h" />
<ClInclude Include="src\main\headers\org_cryptomator_windows_keychain_WinDataProtection_Native.h" />
<ClInclude Include="src\main\headers\org_cryptomator_windows_keychain_WindowsHello_Native.h" />
<ClInclude Include="src\main\headers\org_cryptomator_windows_uiappearance_WinAppearance_Native.h" />
<ClInclude Include="src\main\resources\ktmw32_helper.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="src\main\native\org_cryptomator_windows_autostart_WinShellLinks_Native.cpp" />
<ClCompile Include="src\main\native\org_cryptomator_windows_keychain_WinDataProtection_Native.cpp" />
<ClCompile Include="src\main\native\org_cryptomator_windows_uiappearance_WinAppearance_Native.cpp" />
<ClCompile Include="src\main\native\org_cryptomator_windows_keychain_WindowsHello_Native.cpp" />
<ClCompile Include="src\main\native\org_cryptomator_windows_uiappearnce_WinAppearance_Native.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.cryptomator.integrations.revealpath.RevealPathService;
import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
import org.cryptomator.windows.autostart.WindowsAutoStart;
import org.cryptomator.windows.keychain.WindowsHelloKeychainAccess;
import org.cryptomator.windows.keychain.WindowsProtectedKeychainAccess;
import org.cryptomator.windows.quickaccess.ExplorerQuickAccessService;
import org.cryptomator.windows.revealpath.ExplorerRevealPathService;
Expand All @@ -19,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
16 changes: 4 additions & 12 deletions src/main/java/org/cryptomator/windows/autostart/WinShellLinks.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package org.cryptomator.windows.autostart;

import org.cryptomator.windows.common.NativeLibLoader;
import org.cryptomator.windows.common.WinStrings;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;

/**
* Interface to the native Windows shell link interface.
Expand All @@ -22,9 +21,9 @@ public class WinShellLinks {
*/
public int createShortcut(String target, String storagePath, String description) {
return Native.INSTANCE.createShortcut(
getNullTerminatedUTF16Representation(target),
getNullTerminatedUTF16Representation(storagePath),
getNullTerminatedUTF16Representation(description)
WinStrings.getNullTerminatedUTF16Representation(target),
WinStrings.getNullTerminatedUTF16Representation(storagePath),
WinStrings.getNullTerminatedUTF16Representation(description)
);
}

Expand All @@ -36,13 +35,6 @@ public int createShortcut(String target, String storagePath, String description)
public String getPathToStartupFolder() {
return Native.INSTANCE.createAndGetStartupFolderPath();
}

// visible for testing
byte[] getNullTerminatedUTF16Representation(String source) {
byte[] bytes = source.getBytes(StandardCharsets.UTF_16LE);
return Arrays.copyOf(bytes, bytes.length + 2); // add double-width null terminator 0x00 0x00
}

private static class Native {
static final Native INSTANCE = new Native();

Expand Down
14 changes: 14 additions & 0 deletions src/main/java/org/cryptomator/windows/common/WinStrings.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.cryptomator.windows.common;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;

public class WinStrings {

// visible for testing
public static byte[] getNullTerminatedUTF16Representation(String source) {
byte[] bytes = source.getBytes(StandardCharsets.UTF_16LE);
return Arrays.copyOf(bytes, bytes.length + 2); // add double-width null terminator 0x00 0x00
}

}
166 changes: 166 additions & 0 deletions src/main/java/org/cryptomator/windows/keychain/FileKeychain.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package org.cryptomator.windows.keychain;

import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
* A file-based keychain. It's content is a utf-8 encoded JSON object.
*/
class FileKeychain implements WindowsKeychainAccessBase.Keychain {

private final static Logger LOG = LoggerFactory.getLogger(FileKeychain.class);
private static final ObjectMapper JSON_MAPPER = new ObjectMapper();

private final List<Path> keychainPaths;

private Map<String, KeychainEntry> cache;
private volatile boolean loaded;

FileKeychain(String keychainPathsProperty) {
keychainPaths = parsePaths(System.getProperty(keychainPathsProperty, ""), System.getProperty("path.separator"));
cache = new ConcurrentHashMap<>();
}

//testing
FileKeychain(List<Path> paths) {
keychainPaths = paths;
cache = new ConcurrentHashMap<>();
}

synchronized void load() throws KeychainAccessException {
if (!loaded) {
loadInternal();
loaded = true;
}
}

//for testing
void loadInternal() throws KeychainAccessException {
if (keychainPaths.isEmpty()) {
throw new KeychainAccessException("No path specified to store keychain");
}
//Note: We are trying out all keychainPaths to see, if we have to migrate an old keychain file to a new location
boolean useExisting = false;
for (Path keychainPath : keychainPaths) {
Optional<Map<String, KeychainEntry>> maybeKeychain = parse(keychainPath);
if (maybeKeychain.isPresent()) {
cache = maybeKeychain.get();
useExisting = true;
break;
}
}
if (!useExisting) {
LOG.debug("Keychain file not found or not parsable. Using new keychain.");
}

}

//visible for testing
Optional<Map<String, KeychainEntry>> parse(Path keychainPath) throws KeychainAccessException {
LOG.debug("Loading keychain from {}", keychainPath);
TypeReference<Map<String, KeychainEntry>> type = new TypeReference<>() {
};
try (InputStream in = Files.newInputStream(keychainPath, StandardOpenOption.READ); //
Reader reader = new InputStreamReader(in, UTF_8)) {
return Optional.ofNullable(JSON_MAPPER.readValue(reader, type));
} catch (NoSuchFileException e) {
return Optional.empty();
} catch (JacksonException je) {
LOG.warn("Ignoring existing keychain file {}: Parsing failed.", keychainPath);
return Optional.empty();
} catch (IOException e) {
//TODO: we could ignore this
throw new KeychainAccessException("Failed to read keychain from path " + keychainPath, e);
}
}

//visible for testing
synchronized void save() throws KeychainAccessException {
var keychainFile = keychainPaths.getFirst(); //Note: we are always storing the keychain to the first entry to use the 'newest' keychain path and thus migrate old data
LOG.debug("Writing keychain to {}", keychainFile);
try (OutputStream out = Files.newOutputStream(keychainFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); //
Writer writer = new OutputStreamWriter(out, UTF_8)) {
JSON_MAPPER.writeValue(writer, cache);
} catch (IOException e) {
throw new KeychainAccessException("Could not write keychain to path " + keychainFile, e);
}
}

static List<Path> parsePaths(String listOfPaths, String pathSeparator) {
return Arrays.stream(listOfPaths.split(pathSeparator))
.filter(Predicate.not(String::isEmpty))
.map(s -> {
try {
return Path.of(s);
} catch (InvalidPathException e) {
LOG.info("Ignoring string {} for keychain file path: Cannot be converted to a path.", s);
return null;
}
})
.filter(Objects::nonNull)
.map(Util::resolveHomeDir)
.toList();
}

@Override
public KeychainEntry put(String id, KeychainEntry value) throws KeychainAccessException {
load();
var result = cache.put(id, value);
save();
return result;
}

@Override
public KeychainEntry get(String id) throws KeychainAccessException {
load();
return cache.get(id);
}

@Override
public KeychainEntry remove(String id) throws KeychainAccessException {
load();
var result = cache.remove(id);
save();
return result;
}

@Override
public KeychainEntry change(String id, KeychainEntry newEntry) throws KeychainAccessException {
load();
var result = cache.computeIfPresent(id, (_, _) -> newEntry);
save();
return result;
}

@Override
public boolean isSupported() {
//TODO: actually, we would like the location to be writable as well
return !keychainPaths.isEmpty();
}
}
51 changes: 51 additions & 0 deletions src/main/java/org/cryptomator/windows/keychain/Util.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package org.cryptomator.windows.keychain;

import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;

import static java.nio.charset.StandardCharsets.UTF_8;

public class Util {
private static final Path USER_HOME_REL = Path.of("~");
private static final Path USER_HOME = Path.of(System.getProperty("user.home"));

static Path resolveHomeDir(Path path) {
if (path.startsWith(USER_HOME_REL)) {
return USER_HOME.resolve(USER_HOME_REL.relativize(path));
} else {
return path;
}
}

static byte[] generateSalt() {
byte[] result = new byte[2 * Long.BYTES];
UUID uuid = UUID.randomUUID();
ByteBuffer buf = ByteBuffer.wrap(result);
buf.putLong(uuid.getMostSignificantBits());
buf.putLong(uuid.getLeastSignificantBits());
return result;
}

}
Loading

0 comments on commit 5478dbc

Please sign in to comment.