-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #83 from purejava/hello
Add Windows Hello support
- Loading branch information
Showing
29 changed files
with
1,316 additions
and
354 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,9 @@ | ||
*.class | ||
*.jar | ||
*.dll | ||
*.obj | ||
*.lib | ||
*.exp | ||
|
||
# Maven # | ||
target/ | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
37 changes: 37 additions & 0 deletions
37
src/main/headers/org_cryptomator_windows_keychain_WindowsHello_Native.h
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 14 additions & 0 deletions
14
src/main/java/org/cryptomator/windows/common/WinStrings.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
166
src/main/java/org/cryptomator/windows/keychain/FileKeychain.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
|
||
} |
Oops, something went wrong.