Here is a simple entry-level key manager for use with InfinityDB Encrypted databases. It is important to be able to store encryption keys separately from the encrypted databases rather than in the code for example or with the databases. This key manager holds a set of keys, one per encrypted database in a given client. The client authenticates itself to the key manager server using a username and password, and the key manager, being an InfinityDB Server instance, authenticates itself with an SSL/TLS certificate. There are many other possible organizations, with more or less granularity of control. The key manager server’s back-end database browser can be used to manage keys – for example, a rogue client or disclosed client password or file encryption key can be revoked manually.
This Key Manager Server is not as capable as a commercial KMS, which can also generate key pairs and perform all necessary cryptographic primitives.
This code also demonstrates the use of the InfinityDB Client/Server ‘RemoteItemSpace’ access.
/* Copyright (c) 2019 Boiler Bay Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package com.infinitydb.remote; import java.io.File; import java.io.IOException; import java.security.GeneralSecurityException; import java.security.SecureRandom; import java.util.Arrays; import java.util.Base64; import java.util.HashMap; import java.util.Map; import com.infinitydb.Attribute; import com.infinitydb.Cu; import com.infinitydb.EntityClass; import com.infinitydb.InfinityDB; import com.infinitydb.security.EncryptionLevel; /** * We use for this key manager server demo an imagined software development * environment, which is, say, a company called 'demoLicensee' that has the * rights to use some software developed by company called 'demoLicensor'. * DemoLicensee has multiple servers called 'demoServers', each with the * permission to run the licensed software. Each demoServer needs access to * encryption keys for local databases it owns, such as its own local InfinityDB * Encrypted files it needs to create or open. * * The demoServers need to be able to read the encryption keys at runtime or * boot-time but not to write them. Writing is done by another user or part of * the system called an 'editor', such as demoLicensor, or someone or some * software which acts on the part of demoLicensee. * * This key manager is an InfinityDB Server with a particular setup of users, * passwords, roles, permissions, and databases that it manages via its own * 'admin' user. The admin user is like a 'root' in that it has total control * via a web-based console. The InfinityDB server also has a web-based database * browser for secure database viewing and editing. The database browser or * other tool can easily copy keys into a backup database on the server or even * a secondary server. * * The features of this key manager are not as complete as for some commercial * key managers, which provide a full range of cryptographic primitives, while * here we only provide secure access to the encryption keys and other sensitive * shared data. * * The key manager server runs in its own host, such as an AWS EC2 instance, * with manually-defined IP filters to further limit access to legitimate * demoServers. The key manager server can be operated and hosted by * demoLicensor or demoLicensee. For this demo, we use the infinitydb.com server * as a key manager server, although it can do other things concurrently. The * database for demoLicensee on the infinitydb.com key manager server is * https://infinitydb.com:37412/keys/demo. Each licensee other than * demoLicensee, if there are multiple, can have a separate * 'https://infinitydb.com:37412/keys/other-licensee' database on the same key * manager server. For tighter control and finer granularity, there could be a * separate key manager server database for each demoServer like this: * https://infinitydb.com:37412/keys/demo-server-2. * * A demoServer needs read-only access sometimes to get its encryption keys. At * other times the keys must be altered, so there are two key manager users on * infinitydb.com. The names are arbitrary. One has read-only, the other * read/write permission. * * In this simple scenario, all demoServers can access any of the encryption * keys. For authenticating itself, a particular demoServer stores its username * and password in a home directory owner-only file in an owner-only directory. * Each demoServer runs as a distinct OS user. */ public class KeyManagerDemo { // This is not the web back-end port, but the RemoteItemSpace port one higher. static final String KEY_MANAGER_URL = "https://infinitydb.com:37412/keys/demo"; // static final String KEY_MANAGER_URL = // "http://localhost:37412/keys/demo"; // How to access the key manager server as a demoServer. static final Authority KEY_MANAGER_AUTHORITY = new Authority("demo-key-manager", "secret1".toCharArray()); // How to access the key manager server in order to change the data like // keys static final Authority KEY_MANAGER_EDITOR_AUTHORITY = new Authority("demo-key-manager-editor", "secret2".toCharArray()); // This is located by default in $JAVA_HOME/lib/security. static final String CACERTS_NAME = "cacerts"; static final char[] CACERTS_PASS_WORD = "changeit".toCharArray(); // This is PEM so it has no password. It is a root CA // to handle custom key pairs we generate. Not used in general. static final String INFDB_ROOT_CA = "trust-store/infdbroot.pem"; // The maximum size of the cache for each InfinityDB instance // opened locally. Does not apply to connected remote ItemSpaces. static final int DB_CACHE_SIZE = 100 * 1000 * 1000; /* * The simple schema. These constants are components in the Items that store * the keys. Much more could be added and this is entirely flexible. An * encryption key, i.e. password is identified by server name and file name. * The Items look like: * * LicensedServer "demoServer" file "demoFile.infdb" key Chars("demoKey") */ // Below this are the demoServer names, as strings static final EntityClass LICENSED_SERVER = new EntityClass("LicensedServer"); // Below this are file names as strings local to each demoServer static final EntityClass LICENSED_SERVER__FILE = new EntityClass("File"); // Below this are encryption keys, as short char arrays. static final Attribute LICENSED_SERVER_FILE__KEY = new Attribute("key"); /* * The demoServers read encryption keys from the key manager server via * this. All demoServers can see others' encryption keys in this simple * model. Or, each demoServer could have a unique database in the key * manager server. */ RemoteClientItemSpace keyManagerRemoteClientItemSpace; /* * Licensee or Licensor can modify the server names, file names, and * encryption keys via this remotely connected ItemSpace as an 'editor'. * demoLicensee does not necessarily have access to this. */ RemoteClientItemSpace keyManagerEditorRemoteClientItemSpace; public static void main(String... args) { try { new KeyManagerDemo().demo(); } catch (Exception e) { e.printStackTrace(); } } public KeyManagerDemo() throws IOException, GeneralSecurityException { /* * We are a client, so we don't pass in a private key or password. Also, * there is no custom SSLCertificateNameVerifier. This is just for * accessing the trust store. */ KeyStoreAccessor keyStoreAccessor = new KeyStoreAccessor(null, null, null); // The standard trust store in java home. keyStoreAccessor.addTrustManager(CACERTS_NAME, CACERTS_PASS_WORD); /* * The special trust store that is a self-signed ca root provided by * infinitydb as a default. Optional, in case w need to create our own * end-entity certs. */ // File infdbRootCaPem = new File(workerHomeDirectory, // INFDB_ROOT_CA); // keyStoreAccessor.addTrustManager(infdbRootCaPem, // new char[0]); // If we want to trust every possible license server, use this // keyStoreAccessor.addTrustAnyoneTrustManager(); // System.out.println("keyStoreAccessor=" + keyStoreAccessor); // For any future server params. Default to empty. Map<String, String> formParams = new HashMap<>(); /* * Re-usable connector, for creating connections to the keys/demo * database on the key manager server. The permission is read-only. This * provides encryption keys to the demoServers. We use only one * 'Authority' i.e. user/password for all demoServers here. */ SocketConnector keyManagerSocketConnector = new SocketConnector(KEY_MANAGER_URL, KEY_MANAGER_AUTHORITY, keyStoreAccessor, formParams, ItemSpaceAccessPermissions.READ); keyManagerRemoteClientItemSpace = new RemoteClientItemSpace(keyManagerSocketConnector); /* * The licensee or licensor alters encryption keys or any other * additional optional data through these 'editor' connections. The * permission is read/write. */ SocketConnector keyManagerEditorSocketConnector = new SocketConnector(KEY_MANAGER_URL, KEY_MANAGER_EDITOR_AUTHORITY, keyStoreAccessor, formParams, ItemSpaceAccessPermissions.READ_WRITE); keyManagerEditorRemoteClientItemSpace = new RemoteClientItemSpace( keyManagerEditorSocketConnector); } void demo() throws IOException { // Where we actually would store data locally File dbFile = File.createTempFile("InfinityDB_Encrypted_file_for_demo", ".infdb"); try (Cu cu = Cu.alloc()) { // We could use any other kind of db id. String demoFileName = dbFile.toString(); String demoServer = "demoServer"; // char[] encryptionKey = "demoEncryptionKey".toCharArray(); char[] encryptionKey = generateRandomKey(); // This could be restricted to the editor // You can use random passwords too setEncryptionKey(demoServer, demoFileName, encryptionKey); // default level is AES-128. You can also use 256-bit // STRONG_ENCRYPTION. InfinityDB db = InfinityDB.create(dbFile.toString(), true, DB_CACHE_SIZE, encryptionKey, EncryptionLevel.DEFAULT_ENCRYPTION); // EncryptionLevel.STRONG_ENCRYPTION); // Put data in db.. db.insert("Hello world"); db.close(); encryptionKey = getEncryptionKey(demoServer, demoFileName); db = InfinityDB.open(dbFile.toString(), true, DB_CACHE_SIZE, encryptionKey); // Use data in db.. cu.setItem("Hello world"); System.out.println("found=" + db.exists(cu)); db.close(); } catch (Exception e) { e.printStackTrace(); } finally { dbFile.delete(); } } /** * A base-64 char[] is generated. Intermediate arrays are zeroed. The * returned char[] should be zeroed after use. */ char[] generateRandomKey() { // Any length. 32 is good. byte[] passWordBytes = new byte[32]; new SecureRandom().nextBytes(passWordBytes); Base64.Encoder base64Encoder = Base64.getEncoder(); byte[] passWordEncoded = base64Encoder.encode(passWordBytes); char[] passWordChars = new char[passWordEncoded.length]; for (int i = 0; i < passWordEncoded.length; i++) { passWordChars[i] = (char)(passWordEncoded[i] & 0xff); } // Try to keep pw from floating in memory Arrays.fill(passWordBytes, (byte)0); Arrays.fill(passWordEncoded, (byte)0); // System.out.println("generated key=" + // Arrays.toString(passWordChars)); return passWordChars; } /** * Get a file's password from the license server. * * @param serverName * for example, demoServer * @param fileName * is the local encrypted file on that demo server that we need * to work with. * @return the char[] encryption key, i.e. password */ char[] getEncryptionKey(String serverName, String fileName) throws IOException { try (Cu cu = Cu.alloc()) { cu.append(LICENSED_SERVER).append(serverName) .append(LICENSED_SERVER__FILE) .append(fileName) .append(LICENSED_SERVER_FILE__KEY); int off = cu.length(); if (keyManagerRemoteClientItemSpace.next(cu, off)) { char[] passWord = cu.charArrayAt(off); return passWord; } return null; } } /** * Set an encryption key i.e. password for a given file in a given * demoServer. The editor connection is used, so the editor user name and * password are required (not the InfinityDB key manager server admin user, * who is like 'root' and has total control). The demoServers themselves * would actually not use this. The editor user could login and do this * manually too. */ void setEncryptionKey(String serverName, String fileName, char[] encryptionKey) throws IOException { try (Cu cu = Cu.alloc()) { cu.append(LICENSED_SERVER).append(serverName) .append(LICENSED_SERVER__FILE) .append(fileName) .append(LICENSED_SERVER_FILE__KEY); int prefixLength = cu.length(); // Making it a string leaves it in memory. cu.append(encryptionKey); // Replace previous key. This deletes all suffixes after // prefixLength, // then inserts the entire Item in cu. keyManagerEditorRemoteClientItemSpace.update(cu, prefixLength); } } }