GithubHelp home page GithubHelp logo

cternes / openkeepass Goto Github PK

View Code? Open in Web Editor NEW
133.0 25.0 39.0 990 KB

[Deprecated] A java library for reading and writing KeePass databases. It is an intuitive java library that supports KeePass 2.x database files.

License: Apache License 2.0

Java 100.00%
java keepass android keepass-database java-library

openkeepass's Introduction

DEPRECATED - unfortunately no longer actively maintained, because I don't have time

Build Status

openkeepass

openkeepass is a java library for reading and writing KeePass databases. It is an intuitive java library that supports KeePass 2.x database files.

Only KeePass files created with version 2.x are supported. KeePass files created with version 1.x are NOT supported.

Features included so far:

  • Reading and writing support for KeePass 2.x
  • Password or Keyfile credentials: openkeepass can open password protected databases as well as keyfile protected databases.
  • Android Support: Will run on Android devices.
  • Easy to learn API: openkeepass has a simple API with convenient methods that makes it easy to read data from a KeePass database.
  • Very lean: openkeepass tries to keep the necessary dependencies to an absolute minimum.
  • Backward compatible until Java 6

Installation

The easiest way is to add openkeepass as a maven dependency.

<dependency>
    <groupId>de.slackspace</groupId>
	<artifactId>openkeepass</artifactId>
    <version>0.8.1</version>
</dependency>

Prerequisites

Before using this library make sure that you have the Java Cryptography Extension (JCE) installed on your system.

You can download JCE here:

Android

Android users should apply the following dependency to avoid an error regarding build-in xml libraries:

compile ('de.slackspace:openkeepass:0.6.0') {
    exclude module: 'stax'
    exclude module: 'stax-api'
    exclude module: 'xpp3'
}

Examples for reading

The basic usage is very simple. This example will show you how to retrieve all entries and the top groups of the KeePass database.

    // Open Database
	KeePassFile database = KeePassDatabase.getInstance("Database.kdbx").openDatabase("MasterPassword");
		
	// Retrieve all entries
	List<Entry> entries = database.getEntries();
	for (Entry entry : entries) {
		System.out.println("Title: " + entry.getTitle() + " Password: " + entry.getPassword());
	}

	// Retrieve all top groups
	List<Group> groups = database.getTopGroups();
	for (Group group : groups) {
		System.out.println(group.getName());
	}

You can also search for specific entries in the database:

	// Search for single entry
	Entry sampleEntry = database.getEntryByTitle("Sample Entry");
	System.out.println("Title: " + sampleEntry.getTitle() + " Password: " + sampleEntry.getPassword());

	// Search for all entries that contain 'Sample' in title
	List<Entry> entriesByTitle = database.getEntriesByTitle("Sample", false);
	for (Entry entry : entriesByTitle) {
		System.out.println("Title: " + entry.getTitle() + " Password: " + entry.getPassword());
	}

Open a database with a key file:

	// Open database with keyfile
	KeePassFile database = KeePassDatabase.getInstance("DatabaseProtectedByKeyfile.kdbx").openDatabase(new File("Keyfile.key"));
		
	// Print all entries		
	List<Entry> entries = database.getEntries();
	for (Entry entry : entries) {
		System.out.println(entry.getTitle() + ":" + entry.getPassword());
	}

Retrieve custom string fields (Advanced tab) from a database:

	// Retrieve all properties including custom string fields of an entry
	Set<Property> properties = database.getEntryByTitle("1st Entry").getProperties();
	for (Property property : properties) {
		System.out.println(property.getKey() + ":" + property.getValue());
	}

For more usages have a look into the unit test classes.

Examples for writing

If you want to start writing a new KeePass file from scratch you first have to build up your database model. This will be done using the provided builders. After the model has been constructed you can use the KeePassDatabase class to write the KeePass database to a stream.

	// Build KeePass model
	Group root = new GroupBuilder()
					.addEntry(new EntryBuilder("First entry").username("Peter").password("Peters secret").build())
					.addGroup(new GroupBuilder("Banking")
							.addEntry(new EntryBuilder("Second entry").username("Paul").password("secret").build())
							.build())
					.build();
				
	KeePassFile keePassFile = new KeePassFileBuilder("writingDB")
					.addTopGroups(root)
					.build();
				
	// Write KeePass file to disk
	KeePassDatabase.write(keePassFile, "MasterPassword", new FileOutputStream("Database.kdbx"));

For more usages have a look into the unit test classes.

openkeepass's People

Contributors

cternes avatar dpishchukhin avatar frozenice avatar rafaelrpinto avatar ronsmits avatar tomfitzhenry avatar tunous avatar vhschlenker avatar yososs avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

openkeepass's Issues

Can't read other app's files

i create a file using KeepassDroid or KeePassFX. Then i added a group there. All works great. Decode successfully. Then I added an Entry in KeePassDroid and i have got an error when decoding:

de.slackspace.openkeepass.exception.KeePassDatabaseUnreadableException: Could not deserialize object to xml

I think, ok, maybe i can create database using this library. I did that. Encode, decode, all works great, but when I try to open this database in KeePassDroid i am getting an error:

Wrong event type
(position: TEXT 49@27:17 in java.io.inputStream ...)

whats wrong with me or with this library

Entries created with KeeFox 1.4.7 extension are not decrypted correctly

Hi,

I created a brand new database on KeePass 2.28 and I am able to see the entries that I create manually.

But when I save an entry using KeeFox 1.4.7 on firefox, the password is not decrypted correctly using openkeepass:

Title: Sample Entry #2 Password: 12345
Title: Sign in - Google Accounts Password: 1�

The first is an entry created on KeePass and the second created on KeeFox addon.

Cheers,

Could not deserialize object to XML

Hi, I opened sorz/TinyKeePass#9 and then realized that TinyKeePass relies on this library, which is actually throwing the error.

I have a KeePass file that openkeepass cannot parse. It says "Could not deserialize object to XML." I wish I could provide the kdbx, but it contains real passwords.

The same kdbx can be opened properly with KeeWeb, KeePassDroid, and KeePass Tusk. It's possible the file is corrupted and these other tools are just very lenient when parsing the XML. I'm not sure how to check.

Invalid Place Holder

%i -> %d

throw new IllegalArgumentException(String.format("Value %i is not a valid CrsAlgorithm", value));

public enum CrsAlgorithm {

    Null, ArcFourVariant, Salsa20;

    public static CrsAlgorithm parseValue(int value) {
        switch (value) {
        case 0:
            return Null;
        case 1:
            return ArcFourVariant;
        case 2:
            return Salsa20;
        default:
            throw new IllegalArgumentException(String.format("Value %i is not a valid CrsAlgorithm", value));
        }
    }

JCE workaround stopped worked from 1.8.0_101+

de.slackspace.openkeepass.exception.KeePassDatabaseUnreadable: The key has the wrong size. Have you installed Java Cryptography Extension (JCE)? Is the master key correct?
        at de.slackspace.openkeepass.crypto.Aes.transformKey(Aes.java:130)
        at de.slackspace.openkeepass.crypto.Decrypter.createAesKey(Decrypter.java:49)
        at de.slackspace.openkeepass.crypto.Decrypter.decryptDatabase(Decrypter.java:15)
        at de.slackspace.openkeepass.KeePassDatabase.decryptAndParseDatabase(KeePassDatabase.java:316)
        at de.slackspace.openkeepass.KeePassDatabase.openDatabase(KeePassDatabase.java:307)
        at de.slackspace.openkeepass.KeePassDatabase.openDatabase(KeePassDatabase.java:280)

Some improvement ideas

Hello cternes,

I stumbled over your great library while searching for a solution for a KeePass "Automatic Password Changer".
Found nothing, so I am writing one on my own now, using your library for storage in KeePass files.

Doing so I found some issues:

  • Add a generated "private static final long serialVersionUID" to all of your own Exception classes (Eclipse shows warnings in case they are missing)
  • Use char[] as password representation for security reasons (Passwords as String in memory are unsafe in JAVA, see also "String uniqueness in JAVA")
  • Do not keep full file data in byte[] "KeePassDatabase.keepassFile" (might crash on huge file input streams, also this might be unlikely with password files)
  • Make "KeePassDatabaseWriter.writeKeePassFile" and dependend methods static (they do not use any instance member variables of KeePassDatabaseWriter)
  • Why are String constants like "Entry.PASSWORD" and others private? (Why not use a public enum for that constants?)
  • Why not add a method "Entry.setPassword(String newPassword)" and other setter methods for the existing getter methods? (Workaround with .getProperties and .remove/.add seems inconvenient)

Maybe I come around with some more issues when I am done with my project, if you want to hear about them.

with kind regards
HuDeanY

Correction to Project Description and Readme

Congratulations on this project. Would you mind, though, correcting the project description and Readme which incorrectly state

"A java library for reading and writing KeePass databases. It is the only java library that supports KeePass 2.x database files."

There are several projects here on GitHub that similarly offer Java support for Keepass 2.x data files, mine, KeepassJava2, is one of them :-)

Usecase: editing an entry

With the current setup, editing an entry is a challenge. One needs the entry to show the current information but you need an entry builder to actually update the entry. GUI wise this is kind of a hell toggling between two different objects to handle this.

Is this usecase not in view for this project? or is there another pattern in use here?

java 9 warning

Thanks for providing this library.

I am using java 9 and get this warning.

"C:\Program Files\Java\jdk-9.0.1\bin\java" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.2.5\lib\idea_rt.jar=36002:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.2.5\bin" -Dfile.encoding=UTF-8 -classpath C:\Dev\Fox\out\production\classes;C:\Dev\Fox\out\production\resources;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.apache.logging.log4j\log4j-slf4j-impl\2.5\d1e34a4525e08873703fdaad6c6284f944f8ca8f\log4j-slf4j-impl-2.5.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.slf4j\jcl-over-slf4j\1.7.19\44a8aec96a518fc7728a6b1d2fa5aa83b944d586\jcl-over-slf4j-1.7.19.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.slf4j\slf4j-api\1.7.19\3bf03cd91f15d55330e916cff9a1ebe236725bdd\slf4j-api-1.7.19.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.apache.logging.log4j\log4j-core\2.5\7ed845de1dfe070d43511fab321784e6c4118398\log4j-core-2.5.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.apache.logging.log4j\log4j-api\2.5\e7fd981408caba8a0c0fb276413562468d260160\log4j-api-2.5.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\com.fasterxml.jackson.dataformat\jackson-dataformat-yaml\2.5.0\d3a9039dd90f56880ada44525ef8b1079931e571\jackson-dataformat-yaml-2.5.0.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.seleniumhq.selenium\selenium-java\2.53.0\645a24b52c9e5704e900ac755b5800352e981af7\selenium-java-2.53.0.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\com.github.igor-suhorukov\chromedriver\2.21.1\842b16424d4f69a6e4600c7d8e85b4e88994814d\chromedriver-2.21.1.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.seleniumhq.selenium\selenium-htmlunit-driver\2.52.0\b9945a26ec2518b9d094e43e7d47b8e9146d5d39\selenium-htmlunit-driver-2.52.0.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\net.sourceforge.htmlunit\htmlunit\2.20\5100a7a7c356d571471c739fbf52cc1a2e9ccf17\htmlunit-2.20.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.jsoup\jsoup\1.7.2\d7e275ba05aa380ca254f72d0c0ffebaedc3adcf\jsoup-1.7.2.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.joda\joda-money\0.11\9a3d8b733cb130c05376acd78e6c724e72f39d35\joda-money-0.11.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.imgscalr\imgscalr-lib\4.2\e2838f7119361511ef7d54fe0d502bf07f3325eb\imgscalr-lib-4.2.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.seleniumhq.selenium\selenium-chrome-driver\2.53.0\7541a3948c3fb31122ee9633731e8103f3292b0a\selenium-chrome-driver-2.53.0.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.seleniumhq.selenium\selenium-edge-driver\2.53.0\9f3da0dfa620ae8cd71afacfdd6ddae5028ce1ea\selenium-edge-driver-2.53.0.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.seleniumhq.selenium\selenium-firefox-driver\2.53.0\5dc655b1999898d2fc3791c649b7355d98629a69\selenium-firefox-driver-2.53.0.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.seleniumhq.selenium\selenium-ie-driver\2.53.0\27b703e9fba9c0f636109cf4bd697c1f208ac825\selenium-ie-driver-2.53.0.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.seleniumhq.selenium\selenium-safari-driver\2.53.0\c80224258a8b702c99f2d7b3847ce89dc1bcd2bd\selenium-safari-driver-2.53.0.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.seleniumhq.selenium\selenium-leg-rc\2.53.0\bdabcf672449c588913ae32973dd8f7d54f510de\selenium-leg-rc-2.53.0.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.seleniumhq.selenium\selenium-support\2.53.0\3706aa8a696d1b7938365a41ee7ca46309a0b2d\selenium-support-2.53.0.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.seleniumhq.selenium\selenium-remote-driver\2.53.0\eb76ed037ba5a7c11cce11effd0e2175056905ac\selenium-remote-driver-2.53.0.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.seleniumhq.selenium\selenium-api\2.53.0\a6105ad5c43dcc02c1cf87250111b0a7f1a7c2e6\selenium-api-2.53.0.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\com.google.code.gson\gson\2.8.2\3edcfe49d2c6053a70a2a47e4e1c2f94998a49cf\gson-2.8.2.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\de.slackspace\openkeepass\0.6.1\67cab94bf8b13025c9429b9687dc51d1dbfad9cd\openkeepass-0.6.1.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\com.fasterxml.jackson.core\jackson-databind\2.5.0\489b7552dd3322b63f694122d16ce62c21c303b5\jackson-databind-2.5.0.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\com.fasterxml.jackson.core\jackson-core\2.5.0\b2ece1bd57ac7b4c315b7505b65ac79cb1da4270\jackson-core-2.5.0.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.yaml\snakeyaml\1.12\ebe66a6b88caab31d7a19571ad23656377523545\snakeyaml-1.12.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\commons-collections\commons-collections\3.2.2\8ad72fe39fa8c91eaaf12aadb21e0c3661fe26d5\commons-collections-3.2.2.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.apache.httpcomponents\httpmime\4.5.2\22b4c53dd9b6761024258de8f9240c3dce6ea368\httpmime-4.5.2.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.apache.httpcomponents\httpclient\4.5.2\733db77aa8d9b2d68015189df76ab06304406e50\httpclient-4.5.2.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\com.madgag.spongycastle\core\1.54.0.0\de79c5f8c67234f0d0073e00ed1f3bab0e5d1e67\core-1.54.0.0.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.simpleframework\simple-xml\2.7.1\dd91fb744c2ff921407475cb29a1e3fee397d411\simple-xml-2.7.1.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\com.fasterxml.jackson.core\jackson-annotations\2.5.0\a2a55a3375bc1cef830ca426d68d2ea22961190e\jackson-annotations-2.5.0.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\commons-io\commons-io\2.4\b1b6ea3b7e4aa4f492509a4952029cd8e48019ad\commons-io-2.4.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.apache.commons\commons-exec\1.3\8dfb9facd0830a27b1b5f29f84593f0aeee7773b\commons-exec-1.3.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\net.java.dev.jna\jna-platform\4.1.0\23457ad1cf75c2c16763330de5565a0e67b4bc0a\jna-platform-4.1.0.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\net.java.dev.jna\jna\4.1.0\1c12d070e602efd8021891cdd7fd18bc129372d4\jna-4.1.0.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\io.netty\netty\3.5.7.Final\811465e6dfc89d7c78d21de6a9747b6046cb5403\netty-3.5.7.Final.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.apache.httpcomponents\httpcore\4.4.4\b31526a230871fbe285fbcbe2813f9c0839ae9b0\httpcore-4.4.4.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\commons-logging\commons-logging\1.2\4bfc12adfe4842bf07b657f0369c4cb522955686\commons-logging-1.2.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\commons-codec\commons-codec\1.10\4b95f4897fa13f2cd904aee711aeafc0c5295cd8\commons-codec-1.10.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\stax\stax\1.2.0\c434800de5e4bbe1822805be5fb1c32d6834f830\stax-1.2.0.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\stax\stax-api\1.0.1\49c100caf72d658aca8e58bd74a4ba90fa2b0d70\stax-api-1.0.1.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\xpp3\xpp3\1.1.3.3\64f9d2bb88f58ad2a15a4301487d977ee9b4294\xpp3-1.1.3.3.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\cglib\cglib-nodep\2.1_3\58d3be5953547c0019e5704d6ed4ffda3b0c7c66\cglib-nodep-2.1_3.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\com.google.guava\guava\19.0\6ce200f6b23222af3d8abb6b6459e6c44f4bb0e9\guava-19.0.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\xalan\xalan\2.7.2\d55d3f02a56ec4c25695fe67e1334ff8c2ecea23\xalan-2.7.2.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.apache.commons\commons-lang3\3.4\5fe28b9518e58819180a43a850fbc0dd24b7c050\commons-lang3-3.4.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\net.sourceforge.htmlunit\htmlunit-core-js\2.17\4316d68f449d42f69faf4ee255aa31b03e4f7dd5\htmlunit-core-js-2.17.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\xerces\xercesImpl\2.11.0\9bb329db1cfc4e22462c9d6b43a8432f5850e92c\xercesImpl-2.11.0.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\net.sourceforge.nekohtml\nekohtml\1.9.22\4f54af68ecb345f2453fb6884672ad08414154e3\nekohtml-1.9.22.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\net.sourceforge.cssparser\cssparser\0.9.18\61c015378d27b5e245a5deb7a324c7e716b4706a\cssparser-0.9.18.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.eclipse.jetty.websocket\websocket-client\9.2.15.v20160210\ca9769107f3b8111102c5d4f482122dd116fb711\websocket-client-9.2.15.v20160210.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\xalan\serializer\2.7.2\24247f3bb052ee068971393bdb83e04512bb1c3c\serializer-2.7.2.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\xml-apis\xml-apis\1.4.01\3789d9fada2d3d458c4ba2de349d48780f381ee3\xml-apis-1.4.01.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.w3c.css\sac\1.3\cdb2dcb4e22b83d6b32b93095f644c3462739e82\sac-1.3.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.eclipse.jetty.websocket\websocket-common\9.2.15.v20160210\ee5616ec65d6c8f05fe16ee4dbb6723b2ebff470\websocket-common-9.2.15.v20160210.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.eclipse.jetty\jetty-io\9.2.15.v20160210\5a3af41803c12b0f3628ed8927a8cedb42972169\jetty-io-9.2.15.v20160210.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.eclipse.jetty\jetty-util\9.2.15.v20160210\ccd245541cc63311bdcfe551525bd7d82ea5e92c\jetty-util-9.2.15.v20160210.jar;C:\Users\guest2\.gradle\caches\modules-2\files-2.1\org.eclipse.jetty.websocket\websocket-api\9.2.15.v20160210\f0340017129a65097824dd62a04b3c887f397dd9\websocket-api-9.2.15.v20160210.jar gui.GUI
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by de.slackspace.openkeepass.crypto.Aes (file:/C:/Users/guest2/.gradle/caches/modules-2/files-2.1/de.slackspace/openkeepass/0.6.1/67cab94bf8b13025c9429b9687dc51d1dbfad9cd/openkeepass-0.6.1.jar) to field javax.crypto.JceSecurity.isRestricted
WARNING: Please consider reporting this to the maintainers of de.slackspace.openkeepass.crypto.Aes
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

As a side note it would be great if the probably safer keepass encryption method ChaCha and Argon2 key derivation function would be supported as well.

Thank you.

Custom entry attributes are ignored

How to reproduce:

  • add additional attribute to an entry
  • read with API
  • check that the attribute is filtered in KeePassDatabaseXmlParser.processProtectedValues

p.s. this was working fine in 0.5.0

BouncyCastle <1.50 binary compatibility

BC 1.50 introduced a non backwards compatible change in StreamCipher#processBytes, which now returns an int instead of void.
This change is source compatible (meaning openkeepass can recompile without a change to BC 1.49 and less), but not binary compatible, one gets the following error :

java.lang.NoSuchMethodError: org.bouncycastle.crypto.engines.Salsa20Engine.processBytes([BII[BI)I

While not an issue with this implementation in and of itself, this forces applications to upgrade BouncyCastle to 1.50+, which can be expansive.

Would you consider a change (or a pull request) to refactor Salsa20, using a reflection based appraoch to mitigate the issue ?

I'm thinking something along the lines of :

import org.bouncycastle.crypto.engines.Salsa20Engine;

public class Salsa20EngineAdapter extends Salsa20Engine {

private static final String PROCESS_BYTES_METHOD = "processBytes";
private static final Class<?>[] PROCESS_BYTES_ARG = new Class[] { byte[].class, Integer.TYPE, Integer.TYPE, byte[].class, Integer.TYPE };

public void adaptProcessBytes(byte[] in, int inOff, int len, byte[] out, int outOff) {
  try {
    // BouncyCastle 1.50 and above
    super.processBytes(in, inOff, len, out, outOff);
  } catch (NoSuchMethodError e) {
    // In BC 1.49 and below, #processBytes() returns void instead of int, this breaks binary compatibilty
    // http://stackoverflow.com/a/3589948/2131074
    try {
      Method processBytesMethod = Salsa20Engine.class.getMethod(PROCESS_BYTES_METHOD, PROCESS_BYTES_ARG);
      processBytesMethod.invoke(this, in, inOff, len, out, outOff);
    } catch (Exception e1) {
      // Ignore e1, rethrow e
      throw e;
    }
  }
}

}

Reproducing the behaviour needs a separate project importing openkeepass, but excluding the BC dependency with an older version.

Possible ETA on 0.5.0 release?

Hello, I was wondering if you had a rough idea of the release timing around 0.5.0? I'm currently using the SNAPSHOT version and leveraging the write aspect. Thanks for writing this BTW

EntryBuilder.iconData() strange behavior for me

Hello again)). Sorry, that I disturb you, but i can't understand, how does Entry iconData(ByteArray) works. I tried to save byte array of my own image, but after saving, i am getting another image (standard entry image "key"). It is some strange for me. What should i do, if I want to save my own image?

ChaCha20 encryption with Argon2 key derivation support

Dear @cternes,

According to https://keepass.info/help/base/security.html the "ChaCha20 is the successor of the Salsa20 algorithm" and KeePass 2.x supports it.

According to the source, openkeepass supports KeePass 2.x and Salsa20 algorithm.

Proposal of the User Story description:
As an openkeepass user, I would like to use KeePass 2.x database files with the following configurations:

  • ChaCha20 encryption with Argon2 key derivation key
  • AES/Rijandael encryption with Argon2 key derivation key

Proposal unit tests for the User Story:
ha3ext@4e199d1

I am looking forward to your reply.

Thank you!

Sincerely,
Attila Horvath
Software Engineer
Digitalization

When writing a KeePassFile instance in the file, the password of the entry is made in illegal state.

This behavior There are two problems.

  • Writing twice the instance to the file, the password is incorrect state
  • Can not be compared to the input instance and the output instance for verification

Test Code

@Test
public void testKeePass() throws IOException{

    //Create DB
    {
        final KeePassFile keePassFile = new KeePassFileBuilder("testDB").build();
        final FileOutputStream out = new FileOutputStream("xxx.kdbx");
        try {
            KeePassDatabase.write(keePassFile, "test", out);
        } finally {
            IOUtils.closeQuietly(out);
        }
    }
    //Open / Add Entry / Write / Assert
    {
        File fileX = new File("xxx.kdbx");
        File fileY = new File("yyy.kdbx");
        final FileInputStream in = new FileInputStream(fileX);
        final FileOutputStream out = new FileOutputStream(fileY);
        try {
            final KeePassFile keePassFile = KeePassDatabase.getInstance(fileX).openDatabase("test");
            UUID uuid = UUID.randomUUID();
            Entry enrty = new EntryBuilder(uuid).title("title").password("password").build();
            new GroupBuilder(keePassFile.getRoot().getGroups().get(0)).addEntry(enrty).build();

            KeePassDatabase.write(keePassFile, "test", out);

            // Password Wrong?
            Assert.assertEquals("password", enrty.getPassword());
            Assert.assertEquals("password", keePassFile.getRoot().getGroups().get(0).getEntries().get(0).getPassword());

        } finally {
            IOUtils.closeQuietly(in);
            IOUtils.closeQuietly(out);
        }
    }
}

Compatibility of Java6 Do you need?

Compatibility of Java6 Do you need?

  • Java 6 is already support has been ended.
  • Java7 By the above, can be used StandardCharsets that was introduced in Java7,
    UnsupportedEncodingException check is not required in String.getBytes ( "UTF-8").

Java6

         try {
             return hash (text.getBytes ( "UTF-8"));
         } Catch (UnsupportedEncodingException e) {
             throw new UnsupportedOperationException ( "The encoding 'UTF-8' is not supported", e);
         }

Java7+

         return hash (text.getBytes (StandardCharsets.UTF_8));`

Memory leak

Hi,

I started using this library in one of my use case where my team members access password for different servers stored in the keepass file frequently. In my testing after calling the below code probably after 10times my micro services crashes. I have 1GB allocated to my service.

KeePassDatabase.getInstance(keepassFileinputStream).openDatabase("password",keyfileInputStream);

Is this something you could check and fix this memory leak?

Thanks,
Murthy

getUsername does not resolve referenced field's value

We are using KeePass for tracking service account credentials per application in our environment. It is possible, we will have the same user/pass for multiple applications--but our system requires a unique KeePass entry. As a result, we've tried out adding new entries that have the Username and Password fields referenence an existing/master entry. In the KeePass UI, you'll see the value for the stored Username (or Password or whatever field you're adding reference too) is something in the form of {REF:U@I:36F89B594FD7664E89E62E8CE220129A}. Once we started doing that, our automation broke (that is using the OpenKeePass library) because the usernames & passwords returned weren't the linked values but the links themselves, ie: {REF:U@I:36F89B594FD7664E89E62E8CE220129A}.

I've done my best to dig into the code and don't see any support for this concept. Is it possible to add the ability to resolve the value if an Entry has a linked reference?

Example:
Title: Item 1, Username: masterUser, UUID 36F89B594FD7664E89E62E8CE220129A
Title: Item 2, Username: {REF:U@I:36F89B594FD7664E89E62E8CE220129A}

When doing database.getEntryByTitle("Item 2").getUsername(); it would be nice to have masterUser returned.

Thanks!

Consider moving away from JAXB, to be Android friendly

I'm writing an Android app that uses openkeepass. When I read/write a DB, I see:

java.lang.ClassNotFoundException: Didn't find class "javax.xml.bind.JAXB" on path: DexPathList[[zip file "/data/app/tf.tom.myapplication-1/base.apk"],nativeLibraryDirectories=[/data/app/tf.tom.myapplication-1/lib/x86, /vendor/lib, /system/lib]]
    at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:56)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:511)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:469)
    at de.slackspace.openkeepass.xml.KeePassDatabaseXmlParser.toXml(KeePassDatabaseXmlParser.java:18) 
    at de.slackspace.openkeepass.KeePassDatabase.write(KeePassDatabase.java:426) 

The reason is that Android's javax.xml does provide the entirety of JAXB, nor does Android allow apps to include JARs that are in javax.*

This was news to me, and pretty disappointing.

Consequently, if libraries wish to be Android friendly, they should choose XML libraries other than JAXB, such as Jackson.

Would you be willing to do this? I can provide a PR, when I'm not busy.

Write database with keyfile

It seems so that there is no implementation of using a keyfile for writing the database?

KeePassDatabase.write works only with a password.

Can you verify this?

Why does it use spongycastle instead of bouncycastle?

Spongycastle is already deprecated, is there a reasson why you are still using it? Android, at least since API21 doen't need this anymore, and the lib hasn't seen an update in the last 3 years while bouncy gets continually updates.

openkeepass in Oracle 12c JVM Wrong IV length: must be 16 bytes long

Hi,

I am getting following exception:

Exception in thread "Root Thread" de.slackspace.openkeepass.exception.KeePassDatabaseUnreadableException: Could not decrypt keepass file. Master key wrong?
at de.slackspace.openkeepass.crypto.Aes.createCryptoException(Aes.java:115)
at de.slackspace.openkeepass.crypto.Aes.transformData(Aes.java:72)
at de.slackspace.openkeepass.crypto.Aes.decrypt(Aes.java:41)
at de.slackspace.openkeepass.crypto.Decrypter.processDatabaseEncryption(Decrypter.java:35)
at de.slackspace.openkeepass.crypto.Decrypter.decryptDatabase(Decrypter.java:16)
at de.slackspace.openkeepass.api.KeePassDatabaseReader.decryptStream(KeePassDatabaseReader.java:115)
at de.slackspace.openkeepass.api.KeePassDatabaseReader.decryptAndParseDatabase(KeePassDatabaseReader.java:38)
at de.slackspace.openkeepass.KeePassDatabase.openDatabase(KeePassDatabase.java:165)
at KeepassClobUtil.getPwd(KEEPASSCLOBUTIL:299)
Caused by: java.security.InvalidAlgorithmParameterException: Wrong IV length: must be 16 bytes long
at com.sun.crypto.provider.CipherCore.init(CipherCore.java:525)
at com.sun.crypto.provider.AESCipher.engineInit(AESCipher.java:346)
at javax.crypto.Cipher.implInit(Cipher.java:806)
at javax.crypto.Cipher.chooseProvider(Cipher.java:864)
at javax.crypto.Cipher.init(Cipher.java:1396)
at javax.crypto.Cipher.init(Cipher.java:1327)
at de.slackspace.openkeepass.crypto.Aes.transformData(Aes.java:63)
... 7 more

JVM version inside of Oracle DB version 12.2 is 1.8.0_141. I have copied latest JCE files to following directories:

/oracle/product/12.2.0.1/db_1/javavm/lib/security
/oracle/product/12.2.0.1/db_1/javavm/jdk/jdk8/lib/security
/oracle/product/12.2.0.1/db_1/jdk/jre/lib/security

I have also restarted the DB but still getting the error.
Everything works in local PC (jar built with JDK 1.8.0_141).

I have also tried to load policy files locally to server.

--load jars
declare
r varchar2(500);
begin
R := SYS.DBMS_JAVA.SET_OUTPUT_TO_SQL('1','insert into joutput values (:1)','TEXT');
SYS.DBMS_JAVA.LOADJAVA('-v -r -f /dataio/genio/projects/openkeepass/xpp3-1.1.3.3.jar');
SYS.DBMS_JAVA.LOADJAVA('-v -r -f /dataio/genio/projects/openkeepass/core-1.54.0.0.jar');
SYS.DBMS_JAVA.LOADJAVA('-v -r -f /dataio/genio/projects/openkeepass/simple-xml-2.7.1.jar');
SYS.DBMS_JAVA.LOADJAVA('-v -r -f /dataio/genio/projects/openkeepass/local_policy.jar');
SYS.DBMS_JAVA.LOADJAVA('-v -r -f /dataio/genio/projects/openkeepass/US_export_policy.jar');

end;
/

Can you give a hint ?

Input Stream Resource Leak

  • KeePassDatabase.openDatabase(String password, File keyFile)
  • IconEnricher.getStockIconData(int iconId)

The EntryBuilder.buildWithHistory () method there is a potential problem of circular reference.

The EntryBuilder.buildWithHistory () method there is a potential problem of circular reference.
Stack overflow exception is thrown at the timing to write the KeePassFile to file.

Circulation call occurs in Entry.hashCode () method.

    @Test
    public void testBuildWithHistory() {
        UUID uuid = UUID.randomUUID();
        Entry entry = new EntryBuilder("v1").uuid(uuid).build();
        Entry entry2 = new EntryBuilder(entry).title("v2").buildWithHistory();
        Entry entry3 = new EntryBuilder(entry2).title("v3").buildWithHistory();
        try{
            entry.hashCode();
            entry2.hashCode();
            entry3.hashCode();
        }catch(StackOverflowError e){
            e.printStackTrace();
            Assert.fail();
        }
    }

Android: Editing existing entry with history and saving causes invalid chars in protected fields

I'm currently playing around with your lib.
Opening, listing etc. works great.
But when i try to edit an entry and it contains protected fields (e.g. password), upon save, the password is "unreadable" for other keepass viewers. The testapp itself is capable of decoding the protected fields.

File extStore = Environment.getExternalStorageDirectory();
        mKeePassDatabaseFile = extStore.getAbsolutePath() + "/test_simple.kdbx";
        mDatabase = KeePassDatabase.getInstance(mKeePassDatabaseFile).openDatabase("test");
        Group parentGroup = mDatabase.getRoot().getGroups().get(0);
        Entry entry = mDatabase.getEntries().get(0);
        Entry newEntry = new EntryBuilder(entry).username("hello1").buildWithHistory();
        new GroupBuilder(parentGroup).removeEntry(entry).addEntry(newEntry).build();

        KeePassDatabase.write(mDatabase, "test", mKeePassDatabaseFile);

Before editing the entry was: username: "user1222", password: "pwd1"
The result entry is: username "hello1", but password is "c:w�"

These results i get when:

  • Entry has no history but i use buildWithHistory()
  • Entry has history and I use buildWithHistory()
  • Entry has history and I user build()

in short: only no history and with "build()" works...

Can you please tell me what I'm doing wrong? 😢

Thanks in advance

Add a LICENSE

Add a LICENSE file to the GItHub repo. This helps people who want to use openkeepass or contribute to it, they can directly see what they are allowed to do.

I see you are using Apache 2.0 on Maven. You should add either the full text of the license (up to "END OF TERMS AND CONDITIONS") or the short version (under "APPENDIX", replace the placeholders) to the repo.
For bonus points add a link to tldrlegal at the top.

I plan to use openkeepass myself and have one or two things I'd like to contribute. :)

Edit a loaded Database and save destroy passwords.

Hey whats the best way to add a entry to a database without changing the existing structure?

I try:

KeePassFile keeFile = KeePassDatabase.getInstance( file ).openDatabase( password );
keeFile.getEntries().add(new EntryBuilder(name)
                .username(user)
                .password(pass)
                .build());
KeePassDatabase.write(keeFile, pass, new FileOutputStream( file ) );

It dosn't add the new key and destroy all existing passwords. On next load of the database they are all wrong.

KDBX 4 Support

I cannot open my database file. I get the following error below. I am using KeePassXC v2.5.3 with a MasterPassword to open the file, and the database format is KDBX 4.0. It doesn't seem like the API supports KDBX 4.0.

java.lang.NullPointerException at de.slackspace.openkeepass.domain.CrsAlgorithm.getIntValue(CrsAlgorithm.java:21) at de.slackspace.openkeepass.domain.KeePassHeader.getInnerRandomStreamId(KeePassHeader.java:304) at de.slackspace.openkeepass.domain.KeePassHeader.getValue(KeePassHeader.java:291) at de.slackspace.openkeepass.domain.KeePassHeader.getHeaderSize(KeePassHeader.java:413) at de.slackspace.openkeepass.api.KeePassDatabaseReader.decryptStream(KeePassDatabaseReader.java:114) at de.slackspace.openkeepass.api.KeePassDatabaseReader.decryptAndParseDatabase(KeePassDatabaseReader.java:38) at de.slackspace.openkeepass.KeePassDatabase.openDatabase(KeePassDatabase.java:165)

incorrect data check of implementation in HashedBlockInputStream.java

It is incorrect data check of implementation

if (storedHash == null || storedHash.length != HASH_SIZE) {

  • Meaningless null check
  • Array size is unchanged

HashedBlockInputStream.java

    private byte[] readStoredHashFromStream() throws IOException {
        byte[] storedHash = new byte[32];
        StreamUtils.read(baseStream, storedHash);

        if (storedHash == null || storedHash.length != HASH_SIZE) {
            throw new IOException(MSG_INVALID_DATA_FORMAT);
        }

        return storedHash;
    }

    private void fillBufferFromStream(int bufferSize) throws IOException {
        buffer = new byte[bufferSize];
        StreamUtils.read(baseStream, buffer);
        if (buffer == null || buffer.length != bufferSize) {
            throw new IOException(MSG_INVALID_DATA_FORMAT);
        }
    }

Incorrect termination condition of the read method

KeePassHeader.java

  • Incorrect termination condition of the read method
            if (fieldId == 0) {
                break;
            }
    public void read(byte[] keepassFile) throws IOException {
        SafeInputStream inputStream = new SafeInputStream(new BufferedInputStream(new ByteArrayInputStream(keepassFile)));
        inputStream.skipSafe(VERSION_SIGNATURE_LENGTH); // skip version

        while (true) {
            try {
                int fieldId = inputStream.read();
                byte[] fieldLength = new byte[2];
                inputStream.readSafe(fieldLength);

                ByteBuffer fieldLengthBuffer = ByteBuffer.wrap(fieldLength);
                fieldLengthBuffer.order(ByteOrder.LITTLE_ENDIAN);
                int fieldLengthInt = ByteUtils.toUnsignedInt(fieldLengthBuffer.getShort());

                if (fieldLengthInt > 0) {
                    byte[] data = new byte[fieldLengthInt];
                    inputStream.readSafe(data);
                    setValue(fieldId, data);
                }

                if (fieldId == 0) {
                    break;
                }
            } catch (IOException e) {
                throw new KeePassHeaderUnreadableException("Could not read header input", e);
            }
        }
    }

NPE loading keepass archive created using KeepassXC 2.7.1

I get the following error with my archive:

nullpointerexception: Cannot invoke "de.slackspace.openkeepass.domain.CrsAlgorithm.ordinal()" because "algorithm" is null
     at de.slackspace.openkeepass.domain.CrsAlgorithm.getIntValue(CrsAlgorithm:21)
     at de.slackspace.openkeepass.domain.KeePassHeader.getInnerRandomStreamId(KeePassHeader:304)
     at de.slackspace.openkeepass.domain.KeePassHeader.getValue(KeePassHeader:291)
     at de.slackspace.openkeepass.domain.KeePassHeader.getHeaderSize(KeePassHeader:413)
     at de.slackspace.openkeepass.api.KeePassDatabaseReader.decryptStream(KeePassDatabaseReader:114)
     at de.slackspace.openkeepass.api.KeePassDatabaseReader.decryptAndParseDatabase(KeePassDatabaseReader:38)
     at de.slackspace.openkeepass.KeePassDatabase.openDatabase(KeePassDatabase:165)

History maximum number of management responsibilities

The maximum number of items of management of history is Is the responsibility of the outside of the database library?

    @Test
    public void testBuildWithHistory2() throws FileNotFoundException {


        //Create DB
        {
            final KeePassFile keePassFile = new KeePassFileBuilder("testDB").build();
            FileOutputStream out = null;
            //Default 10
            Assert.assertEquals(10, keePassFile.getMeta().getHistoryMaxItems());
            try {
                out = new FileOutputStream("test/xxx.kdbx");
                KeePassDatabase.write(keePassFile, "test", out);
            } finally {
                IOUtils.closeQuietly(out);
            }
        }
        //Open / Add Entry / Write 
        {
            File fileX = new File("test/xxx.kdbx");
            File fileY = new File("test/yyy.kdbx");
            FileInputStream in = null;
            FileOutputStream out = null;
            try {
                in = new FileInputStream(fileX);
                out = new FileOutputStream(fileY);
                final KeePassFile keePassFile = KeePassDatabase.getInstance(fileX).openDatabase("test");

                UUID uuid = UUID.randomUUID();
                Entry entry1 = new EntryBuilder("v1").uuid(uuid).build();
                Entry entry2 = new EntryBuilder(entry1).title("v2").buildWithHistory();
                Entry entry3 = new EntryBuilder(entry2).title("v3").buildWithHistory();
                Entry entry4 = new EntryBuilder(entry3).title("v4").buildWithHistory();
                Entry entry5 = new EntryBuilder(entry4).title("v5").buildWithHistory();
                Entry entry6 = new EntryBuilder(entry5).title("v6").buildWithHistory();
                Entry entry7 = new EntryBuilder(entry6).title("v7").buildWithHistory();
                Entry entry8 = new EntryBuilder(entry7).title("v8").buildWithHistory();
                Entry entry9 = new EntryBuilder(entry8).title("v9").buildWithHistory();
                Entry entry10 = new EntryBuilder(entry9).title("v10").buildWithHistory();
                Entry entry11 = new EntryBuilder(entry10).title("v11").buildWithHistory();
                Entry entry12 = new EntryBuilder(entry11).title("v12").buildWithHistory();

                new GroupBuilder(keePassFile.getRoot().getGroups().get(0)).addEntry(entry12).build();
                KeePassDatabase.write(keePassFile, "test", out);

            } finally {
                IOUtils.closeQuietly(in);
                IOUtils.closeQuietly(out);
            }
        }
        //History Size Check
        {
            File fileY = new File("test/yyy.kdbx");
            FileInputStream in = null;
            try {
                in = new FileInputStream(fileY);
                final KeePassFile keePassFile = KeePassDatabase.getInstance(fileY).openDatabase("test");
                Entry entry = keePassFile.getRoot().getGroups().get(0).getEntries().get(0);
                Assert.assertEquals(10, entry.getHistory().getHistoricEntries().size());

            } finally {
                IOUtils.closeQuietly(in);
            }
        }
    }

Reading / Writing Entry attachments

I'd like to be able to read / write entry attachments from a KeePass database (I often attach long keys and certificates this way), but am unable to find any calls that point at this functionality.

  1. Is there anything I'm missing? Perhaps a different name for the feature?
  2. If not, would you accept patches / PRs adding this functionality?
  3. If you'll accept patches, any pointers on where to start? I haven't done a deep dive on the KeePass database format (is there a spec somewhere?), but can start familiarizing myself with the openkeepass code base in the meantime.

Thanks!

RuntimeException "Could not move right because the last node at this level has already been reached"

Hello

I'm trying to use Openkeepass to manage my KeePass 2.x databases but I'm getting a

java.lang.RuntimeException with message "Could not move right because the last node at this level has already been reached".

Here is the stack trace: Exception stacktrace.txt

My Main class contains only the main method with the instruction
KeePassFile db = KeePassDatabase.getInstance(path+"Database.kdbx").openDatabase("MasterPassword");

I've obfuscated username/passwords/notes/..., changed master password to "MasterPassword" and enclose here the zipped db: Database.zip
It's protected only with master password

I've tried to make a quick debug.
I may be wrong but it looks like the issue occurs at the root node level (I get a null parent and a current node name "Database" which is only the root).

Thanks and regards,
Mika

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.