All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.elastos.did.DIDStore Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2019 Elastos Foundation
 *
 * 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 org.elastos.did;

import static com.google.common.base.Preconditions.checkArgument;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import org.elastos.did.DIDDocument.PublicKey;
import org.elastos.did.crypto.Aes256cbc;
import org.elastos.did.crypto.Base64;
import org.elastos.did.crypto.EcdsaSigner;
import org.elastos.did.crypto.HDKey;
import org.elastos.did.exception.DIDResolveException;
import org.elastos.did.exception.DIDStorageException;
import org.elastos.did.exception.DIDStoreCryptoException;
import org.elastos.did.exception.DIDStoreException;
import org.elastos.did.exception.DIDSyntaxException;
import org.elastos.did.exception.MalformedExportDataException;
import org.elastos.did.exception.WrongPasswordException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.CryptoException;
import org.spongycastle.crypto.digests.MD5Digest;
import org.spongycastle.crypto.digests.SHA256Digest;
import org.spongycastle.util.encoders.Hex;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

/**
 * This class represents a storage facility for DID objects and private keys.
 *
 * The DIDStore manages different types of entries:
 * - RootIdentity
 * - DIDDocument
 * - VerifiableCredential
 * - PrivateKey
 */
public final class DIDStore {
	/**
	 * The type string for DIDStore.
	 */
	protected static final String DID_STORE_TYPE = "did:elastos:store";
	/**
	 * Current DIDStore version.
	 */
	protected static final int DID_STORE_VERSION = 3;

	private static final int CACHE_INITIAL_CAPACITY = 16;
	private static final int CACHE_MAX_CAPACITY = 128;

	private static final Object NULL = new Object();

	private static final String DID_EXPORT = "did.elastos.export/2.0";
	private static final String DID_LAZY_PRIVATEKEY = "lazy-private-key";

	private Cache cache;

	private DIDStorage storage;
	private Metadata metadata;

	/**
	 * the default conflict handle implementation.
	 */
	protected static final ConflictHandle defaultConflictHandle = (c, l) -> {
		return l;
	};

	private static final Logger log = LoggerFactory.getLogger(DIDStore.class);

	static class Key {
		private static final int TYPE_ROOT_IDENTITY = 0x00;
		private static final int TYPE_ROOT_IDENTITY_PRIVATEKEY = 0x01;
		private static final int TYPE_DID_DOCUMENT = 0x10;
		private static final int TYPE_DID_METADATA = 0x11;
		private static final int TYPE_DID_PRIVATEKEY = 0x12;
		private static final int TYPE_CREDENTIAL = 0x20;
		private static final int TYPE_CREDENTIAL_METADATA = 0x21;

		private int type;
		private Object id;

		private Key(int type, Object id) {
			this.type = type;
			this.id = id;
		}

		@Override
		public int hashCode() {
			return type + id.hashCode();
		}

		@Override
		public boolean equals(Object obj) {
			if (obj == this)
				return true;

			if (obj instanceof Key) {
				Key key = (Key)obj;
				return type == key.type ? id.equals(key.id) : false;
			}

			return false;
		}

		public static Key forRootIdentity(String id) {
			return new Key(TYPE_ROOT_IDENTITY, id);
		}

		public static Key forRootIdentityPrivateKey(String id) {
			return new Key(TYPE_ROOT_IDENTITY_PRIVATEKEY, id);
		}

		public static Key forDidDocument(DID did) {
			return new Key(TYPE_DID_DOCUMENT, did);
		}

		public static Key forDidMetadata(DID did) {
			return new Key(TYPE_DID_METADATA, did);
		}

		private static Key forDidPrivateKey(DIDURL id) {
			return new Key(TYPE_DID_PRIVATEKEY, id);
		}

		private static Key forCredential(DIDURL id) {
			return new Key(TYPE_CREDENTIAL, id);
		}

		private static Key forCredentialMetadata(DIDURL id) {
			return new Key(TYPE_CREDENTIAL_METADATA, id);
		}
	}

	static class Metadata extends AbstractMetadata {
		private static final String TYPE = "type";
		private static final String VERSION = "version";
		private static final String FINGERPRINT = "fingerprint";
		private static final String DEFAULT_ROOT_IDENTITY = "defaultRootIdentity";

		protected Metadata(DIDStore store) {
			super(store);
			put(TYPE, DID_STORE_TYPE);
			put(VERSION, DID_STORE_VERSION);
		}

		/**
		 *  The default constructor for JSON deserialize creator.
		 */
		protected Metadata() {
			this(null);
		}

		protected String getType() {
			return get(TYPE);
		}

		public int getVersion() {
			return getInteger(VERSION, -1);
		}

		private void setFingerprint(String fingerprint) {
			checkArgument(fingerprint != null && !fingerprint.isEmpty(), "Invalid fingerprint");

			put(FINGERPRINT, fingerprint);
		}

		public String getFingerprint() {
			return get(FINGERPRINT);
		}

		protected void setDefaultRootIdentity(String id) {
			put(DEFAULT_ROOT_IDENTITY, id);
		}

		public String getDefaultRootIdentity() {
			return get(DEFAULT_ROOT_IDENTITY);
		}

		@Override
		protected void save() {
			if (attachedStore()) {
				try {
					getStore().storage.storeMetadata(this);
				} catch (DIDStoreException ignore) {
					log.error("INTERNAL - error store metadata for DIDStore");
				}
			}
		}
	}

	/**
	 * ConflictHandle is a interface for solving the conflict,
	 * if the local document is different with the one resolved from chain.
	 */
	@FunctionalInterface
	public interface ConflictHandle {
		/**
		 * The method to merge two did document.
		 *
		 * @param chainCopy the document from chain
		 * @param localCopy the document from local device
		 * @return the merged DIDDocument object
		 */
		DIDDocument merge(DIDDocument chainCopy, DIDDocument localCopy);
	}

	/**
	 * A filter for DIDs.
	 *
	 * 

* Instances of this interface may be passed to the listDids(DIDFilter) * method of the DIDStore class. *

*/ @FunctionalInterface public interface DIDFilter { /** * Tests whether or not the specified DID should be included in a * DIDs list. * * @param did the DID to be tested * @return true if and only if DID should be included */ public boolean accept(DID did); } /** * A filter for DIDURLs. * *

* Instances of this interface may be passed to the * listCredentials(CredentialFilter) method of the DIDStore class. *

*/ @FunctionalInterface public interface CredentialFilter { /** * Tests whether or not the specified id should be included in a * id list. * * @param id the DIDURL to be tested * @return true if and only if DIDURL should be included */ public boolean accept(DIDURL id); } private DIDStore(int initialCacheCapacity, int maxCacheCapacity, DIDStorage storage) throws DIDStoreException { if (initialCacheCapacity < 0) initialCacheCapacity = 0; if (maxCacheCapacity < 0) maxCacheCapacity = 0; // The RemovalListener used for debug purpose. // TODO: comment the RemovalListener /* RemovalListener listener; listener = new RemovalListener() { @Override public void onRemoval(RemovalNotification n) { if (n.wasEvicted()) { String cause = n.getCause().name(); log.trace("Cache removed {} cause {}", n.getKey(), cause); } } }; */ cache = CacheBuilder.newBuilder() .initialCapacity(initialCacheCapacity) .maximumSize(maxCacheCapacity) .softValues() // .removalListener(listener) // .recordStats() .build(); this.storage = storage; this.metadata = storage.loadMetadata(); this.metadata.attachStore(this); log.info("DID store opened: {}, cache(init:{}, max:{})", storage.getLocation(), initialCacheCapacity, maxCacheCapacity); } /** * Open a DIDStore instance with given storage location. * * @param location the storage location for the DIDStore * @param initialCacheCapacity the initial cache capacity * @param maxCacheCapacity the maximum cache capacity * @return the DIDStore object * @throws DIDStoreException if an error occurred when opening the store */ public static DIDStore open(File location, int initialCacheCapacity, int maxCacheCapacity) throws DIDStoreException { checkArgument(location != null, "Invalid store location"); checkArgument(maxCacheCapacity >= initialCacheCapacity, "Invalid cache capacity spec"); try { location = location.getCanonicalFile(); } catch (IOException e) { throw new IllegalArgumentException("Invalid store location", e); } DIDStorage storage = new FileSystemStorage(location); return new DIDStore(initialCacheCapacity, maxCacheCapacity, storage); } /** * Open a DIDStore instance with given storage location. * * @param location the storage location for the DIDStore * @param initialCacheCapacity the initial cache capacity * @param maxCacheCapacity the maximum cache capacity * @return the DIDStore object * @throws DIDStoreException if an error occurred when opening the store */ public static DIDStore open(String location, int initialCacheCapacity, int maxCacheCapacity) throws DIDStoreException { checkArgument(location != null && !location.isEmpty(), "Invalid store location"); return open(new File(location), initialCacheCapacity, maxCacheCapacity); } /** * Open a DIDStore instance with given storage location. * * @param location the storage location for the DIDStore * @return the DIDStore object * @throws DIDStoreException if an error occurred when opening the store */ public static DIDStore open(File location) throws DIDStoreException { return open(location, CACHE_INITIAL_CAPACITY, CACHE_MAX_CAPACITY); } /** * Open a DIDStore instance with given storage location. * * @param location the storage location for the DIDStore * @return the DIDStore object * @throws DIDStoreException if an error occurred when opening the store */ public static DIDStore open(String location) throws DIDStoreException { return open(location, CACHE_INITIAL_CAPACITY, CACHE_MAX_CAPACITY); } /** * Close this DIDStore object. */ public void close() { // log.verbose("Cache statistics: {}", cache.stats().toString()); cache.invalidateAll(); cache = null; metadata = null; storage = null; } private static String calcFingerprint(String password) throws DIDStoreException { // Here should use Argon2, better to avoid the password attack. // But spongycastle library not include the Argon2 implementation, // so here we use one-time AES encryption to secure the password hash. MD5Digest md5 = new MD5Digest(); byte[] digest = new byte[md5.getDigestSize()]; byte[] passwd = password.getBytes(); md5.update(passwd, 0, passwd.length); md5.doFinal(digest, 0); md5.reset(); try { byte[] cipher = Aes256cbc.encrypt(digest, password); md5.update(cipher, 0, cipher.length); md5.doFinal(digest, 0); return Hex.toHexString(digest); } catch (CryptoException e) { throw new DIDStoreCryptoException("Calculate fingerprint error.", e); } } private static String encryptToBase64(byte[] input, String passwd) throws DIDStoreException { try { byte[] cipher = Aes256cbc.encrypt(input, passwd); return Base64.encodeToString(cipher, Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP); } catch (CryptoException e) { throw new DIDStoreCryptoException("Encrypt data error.", e); } } private static byte[] decryptFromBase64(String input, String passwd) throws DIDStoreException { try { byte[] cipher = Base64.decode(input, Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP); return Aes256cbc.decrypt(cipher, passwd); } catch (CryptoException e) { throw new WrongPasswordException("Decrypt private key error.", e); } } private static String reEncrypt(String secret, String oldpass, String newpass) throws DIDStoreException { byte[] plain = decryptFromBase64(secret, oldpass); String newSecret = encryptToBase64(plain, newpass); Arrays.fill(plain, (byte)0); return newSecret; } private String encrypt(byte[] input, String passwd) throws DIDStoreException { String fingerprint = metadata.getFingerprint(); String currentFingerprint = calcFingerprint(passwd); if (fingerprint != null && !currentFingerprint.equals(fingerprint)) throw new WrongPasswordException("Password mismatched with previous password."); String result = encryptToBase64(input, passwd); if (fingerprint == null || fingerprint.isEmpty()) metadata.setFingerprint(currentFingerprint); return result; } private byte[] decrypt(String input, String passwd) throws DIDStoreException { String fingerprint = metadata.getFingerprint(); String currentFingerprint = calcFingerprint(passwd); byte[] result = decryptFromBase64(input, passwd); if (fingerprint == null || fingerprint.isEmpty()) metadata.setFingerprint(currentFingerprint); return result; } /** * Save the RootIdentity object with private keys to this DID store. * * @param identity an RootIdentity object * @param storepass the password for this DID store * @throws DIDStoreException if an error occurred when accessing the store */ protected void storeRootIdentity(RootIdentity identity, String storepass) throws DIDStoreException { checkArgument(identity != null, "Invalid identity"); checkArgument(storepass != null && !storepass.isEmpty(), "Invalid storepass"); String encryptedMnemonic = null; if (identity.getMnemonic() != null) encryptedMnemonic = encrypt(identity.getMnemonic().getBytes(), storepass); String encryptedPrivateKey = encrypt(identity.getRootPrivateKey().serialize(), storepass); String publicKey = identity.getPreDerivedPublicKey().serializePublicKeyBase58(); storage.storeRootIdentity(identity.getId(), encryptedMnemonic, encryptedPrivateKey, publicKey, identity.getIndex()); if (metadata.getDefaultRootIdentity() == null) metadata.setDefaultRootIdentity(identity.getId()); cache.invalidate(Key.forRootIdentity(identity.getId())); cache.invalidate(Key.forRootIdentityPrivateKey(identity.getId())); } /** * Save the RootIdentity object to this DID store(Update the derive index * only). * * @param identity an RootIdentity object * @throws DIDStoreException if an error occurred when accessing the store */ protected void storeRootIdentity(RootIdentity identity) throws DIDStoreException { checkArgument(identity != null, "Invalid identity"); storage.updateRootIdentityIndex(identity.getId(), identity.getIndex()); } /** * Set the identity as the default RootIdentity of the DIDStore. * * @param identity a RootIdentity object * @throws DIDStoreException if an error occurred when accessing the store */ protected void setDefaultRootIdentity(RootIdentity identity) throws DIDStoreException { checkArgument(identity != null, "Invalid identity"); if (!containsRootIdentity(identity.getId())) throw new IllegalArgumentException("Invalid identity, not exists in the store"); metadata.setDefaultRootIdentity(identity.getId()); } /** * Load a RootIdentity object from this DIDStore. * * @param id the id of the RootIdentity * @return the RootIdentity object, null if the identity not exists * @throws DIDStoreException if an error occurred when accessing the store */ public RootIdentity loadRootIdentity(String id) throws DIDStoreException { checkArgument(id != null && !id.isEmpty(), "Invalid id"); try { Object value = cache.get(Key.forRootIdentity(id), new Callable() { @Override public Object call() throws DIDStoreException { RootIdentity identity = storage.loadRootIdentity(id); if (identity != null) { identity.setMetadata(loadRootIdentityMetadata(id)); return identity; } else { return NULL; } } }); return value == NULL ? null : (RootIdentity)value; } catch (ExecutionException e) { throw new DIDStoreException("Load root identity failed: " + id, e); } } /** * Load the default RootIdentity object from this DIDStore. * * @return the default RootIdentity object, null if the identity exists * @throws DIDStoreException if an error occurred when accessing the store */ public RootIdentity loadRootIdentity() throws DIDStoreException { String id = metadata.getDefaultRootIdentity(); if (id == null || id.isEmpty()) { List ids = storage.listRootIdentities(); if (ids.size() != 1) { return null; } else { RootIdentity identity = ids.get(0); identity.setMetadata(loadRootIdentityMetadata(identity.getId())); metadata.setDefaultRootIdentity(identity.getId()); return identity; } } return loadRootIdentity(id); } /** * Check whether the RootIdentity exists in this DIDStore. * * @param id the id of the RootIdentity to be check * @return true if exists else false * @throws DIDStoreException if an error occurred when accessing the store */ public boolean containsRootIdentity(String id) throws DIDStoreException { return storage.loadRootIdentity(id) != null; } /** * Export the mnemonic of the specific RootIdentity from this DIDStore. * * @param id the id of the RootIdentity * @param storepass the password for DIDStore * @return the mnemonic string, null if the identity not exists or does * not have mnemonic * @throws DIDStoreException if an error occurred when accessing the store */ protected String exportRootIdentityMnemonic(String id, String storepass) throws DIDStoreException { checkArgument(id != null && !id.isEmpty(), "Invalid id"); checkArgument(storepass != null && !storepass.isEmpty(), "Invalid storepass"); String encryptedMnemonic = storage.loadRootIdentityMnemonic(id); if (encryptedMnemonic != null) return new String(decrypt(encryptedMnemonic, storepass)); else return null; } /** * Check whether the RootIdentity has mnemonic. * * @param id the id of the RootIdentity * @return true if exists else false * @throws DIDStoreException if an error occurred when accessing the store */ protected boolean containsRootIdentityMnemonic(String id) throws DIDStoreException { checkArgument(id != null && !id.isEmpty(), "Invalid id"); String encryptedMnemonic = storage.loadRootIdentityMnemonic(id); return encryptedMnemonic != null; } private HDKey loadRootIdentityPrivateKey(String id, String storepass) throws DIDStoreException { try { Object value = cache.get(Key.forRootIdentityPrivateKey(id), new Callable() { @Override public Object call() throws DIDStorageException { String encryptedKey = storage.loadRootIdentityPrivateKey(id); return encryptedKey != null ? encryptedKey : NULL; } }); if (value != NULL) { byte[] keyData = decrypt((String)value, storepass); return HDKey.deserialize(keyData); } else { return null; } } catch (ExecutionException e) { throw new DIDStoreException("Load root identity private key failed: " + id, e); } } HDKey derive(String id, String path, String storepass) throws DIDStoreException { checkArgument(id != null && !id.isEmpty(), "Invalid identity"); checkArgument(path != null && !path.isEmpty(), "Invalid path"); checkArgument(storepass != null && !storepass.isEmpty(), "Invalid storepass"); HDKey rootPrivateKey = loadRootIdentityPrivateKey(id, storepass); HDKey key = rootPrivateKey.derive(path); rootPrivateKey.wipe(); return key; } /** * Delete the specific RootIdentity object from this store. * * @param id the id of RootIdentity object * @return true if the identity exists and delete successful; false otherwise * @throws DIDStoreException if an error occurred when accessing the store */ public boolean deleteRootIdentity(String id) throws DIDStoreException { checkArgument(id != null && !id.isEmpty(), "Invalid id"); boolean success = storage.deleteRootIdentity(id); if (success) { if (metadata.getDefaultRootIdentity() != null && metadata.getDefaultRootIdentity().equals(id)) metadata.setDefaultRootIdentity(null); cache.invalidate(Key.forRootIdentity(id)); cache.invalidate(Key.forRootIdentityPrivateKey(id)); } return success; } /** * List all RootIdentity object from this store. * * @return an array of RootIdentity objects * @throws DIDStoreException if an error occurred when accessing the store */ public List listRootIdentities() throws DIDStoreException { List ids = storage.listRootIdentities(); for (RootIdentity id : ids) { RootIdentity.Metadata metadata = storage.loadRootIdentityMetadata(id.getId()); if (metadata == null) metadata = new RootIdentity.Metadata(); metadata.setId(id.getId()); metadata.attachStore(this); id.setMetadata(metadata); } return Collections.unmodifiableList(ids); } /** * Check whether the this store has RootIdentity objects. * * @return true if the store has RootIdentity objects else false * @throws DIDStoreException if an error occurred when accessing the store */ public boolean containsRootIdentities() throws DIDStoreException { return storage.containsRootIdenities(); } /** * Save the RootIdentity metadata to this store. * * @param id the id of the RootIdentity object * @param metadata a RootIdentity.Metadata object * @throws DIDStoreException if an error occurred when accessing the store */ protected void storeRootIdentityMetadata(String id, RootIdentity.Metadata metadata) throws DIDStoreException { checkArgument(id != null && !id.isEmpty(), "Invalid id"); checkArgument(metadata != null, "Invalid metadata"); storage.storeRootIdentityMetadata(id, metadata); } /** * Read the RootIdentity metadata from this store. * * @param id the id of the RootIdentity object * @return the RootIdentity.Metadata object * @throws DIDStoreException if an error occurred when accessing the store */ protected RootIdentity.Metadata loadRootIdentityMetadata(String id) throws DIDStoreException { checkArgument(id != null && !id.isEmpty(), "Invalid id"); RootIdentity.Metadata metadata = storage.loadRootIdentityMetadata(id); if (metadata != null) { metadata.setId(id); metadata.attachStore(this); } else { metadata = new RootIdentity.Metadata(id, this); } return metadata; } /** * Save the DID document to this store. * * @param doc the DIDDocument object * @throws DIDStoreException if an error occurred when accessing the store */ public void storeDid(DIDDocument doc) throws DIDStoreException { checkArgument(doc != null, "Invalid doc"); storage.storeDid(doc); if (doc.getStore() != this) { DIDMetadata metadata = loadDidMetadata(doc.getSubject()); doc.getMetadata().merge(metadata); doc.getMetadata().attachStore(this); } storeDidMetadata(doc.getSubject(), doc.getMetadata()); for (VerifiableCredential vc : doc.getCredentials()) storeCredential(vc); cache.put(Key.forDidDocument(doc.getSubject()), doc); } /** * Read the specific DID document from this store. * * @param did the DID to be load * @return the DIDDocument object * @throws DIDStoreException if an error occurred when accessing the store */ public DIDDocument loadDid(DID did) throws DIDStoreException { checkArgument(did != null, "Invalid did"); try { Object value = cache.get(Key.forDidDocument(did), new Callable() { @Override public Object call() throws DIDStoreException { DIDDocument doc = storage.loadDid(did); if (doc != null) { doc.setMetadata(loadDidMetadata(did)); return doc; } else { return NULL; } } }); return value == NULL ? null : (DIDDocument)value; } catch (ExecutionException e) { throw new DIDStoreException("Load did document failed: " + did, e); } } /** * Read the specific DID document from this store. * * @param did the DID to be load * @return the DIDDocument object * @throws DIDStoreException if an error occurred when accessing the store */ public DIDDocument loadDid(String did) throws DIDStoreException { return loadDid(DID.valueOf(did)); } /** * Check if this store contains the specific DID. * * @param did the specified DID * @return true if the store contains this DID, false otherwise * @throws DIDStoreException if an error occurred when accessing the store */ public boolean containsDid(DID did) throws DIDStoreException { checkArgument(did != null, "Invalid did"); return loadDid(did) != null; } /** * Check if this store contains the specific DID. * * @param did the specified DID * @return true if the store contains this DID, false otherwise * @throws DIDStoreException if an error occurred when accessing the store */ public boolean containsDid(String did) throws DIDStoreException { return containsDid(DID.valueOf(did)); } /** * Check if this store contains DIDs. * * @return true if the store contains DIDs, false otherwise * @throws DIDStoreException if an error occurred when accessing the store */ public boolean containsDids() throws DIDStoreException { return storage.containsDids(); } /** * Save the DID Metadata to this store. * * @param did the owner of the metadata object * @param metadata the DID metadata object * @throws DIDStoreException if an error occurred when accessing the store */ protected void storeDidMetadata(DID did, DIDMetadata metadata) throws DIDStoreException { checkArgument(did != null, "Invalid did"); checkArgument(metadata != null, "Invalid metadata"); storage.storeDidMetadata(did, metadata); metadata.attachStore(this); cache.put(Key.forDidMetadata(did), metadata); } /** * Read the specific DID metadata object for this store. * * @param did a DID to be load * @return the DID metadata object * @throws DIDStoreException if an error occurred when accessing the store */ protected DIDMetadata loadDidMetadata(DID did) throws DIDStoreException { checkArgument(did != null, "Invalid did"); try { Object value = cache.get(Key.forDidMetadata(did) , new Callable() { @Override public Object call() throws DIDStorageException { DIDMetadata metadata = storage.loadDidMetadata(did); if (metadata != null) { metadata.setDid(did); metadata.attachStore(DIDStore.this); } else { metadata = new DIDMetadata(did, DIDStore.this); } return metadata; } }); return value == NULL ? null : (DIDMetadata)value; } catch (ExecutionException e) { throw new DIDStoreException("Load did metadata failed: " + did, e); } } /** * Delete the specific DID from this store. * *

* When delete the DID, all private keys, credentials that owned by this * DID will also be deleted. *

* * @param did the DID to be delete * @return true if the DID exist and deleted successful, false otherwise * @throws DIDStoreException if an error occurred when accessing the store */ public boolean deleteDid(DID did) throws DIDStoreException { checkArgument(did != null, "Invalid did"); boolean success = storage.deleteDid(did); if (success) { cache.invalidate(Key.forDidDocument(did)); cache.invalidate(Key.forDidMetadata(did)); // invalidate every thing belongs to this did for (Key key : cache.asMap().keySet()) { if (key.id instanceof DIDURL) { DIDURL id = (DIDURL)key.id; if (id.getDid().equals(did)) cache.invalidate(key); } } } return success; } /** * Delete the specific DID from this store. * *

* When delete the DID, all private keys, credentials that owned by this * DID will also be deleted. *

* * @param did the DID to be delete * @return true if the DID exist and deleted successful, false otherwise * @throws DIDStoreException if an error occurred when accessing the store */ public boolean deleteDid(String did) throws DIDStoreException { return deleteDid(DID.valueOf(did)); } /** * List all DIDs from this store. * * @return an array of DIDs * @throws DIDStoreException if an error occurred when accessing the store */ public List listDids() throws DIDStoreException { List dids = storage.listDids(); for (DID did : dids) { DIDMetadata metadata = storage.loadDidMetadata(did); if (metadata == null) metadata = new DIDMetadata(); metadata.setDid(did); metadata.attachStore(this); did.setMetadata(metadata); } return Collections.unmodifiableList(dids); } /** * List all DIDs that satisfy the specified filter from this store. * * @param filter a DID filter * @return an array of DIDs * @throws DIDStoreException if an error occurred when accessing the store */ public List listDids(DIDFilter filter) throws DIDStoreException { List dids = listDids(); if (filter != null) { List dest = new ArrayList(); for (DID did : dids) { if (filter.accept(did)) dest.add(did); } dids = dest; } return Collections.unmodifiableList(dids); } /** * Save the credential object to this store. * * @param credential a VerifiableCredential object * @throws DIDStoreException if an error occurred when accessing the store */ public void storeCredential(VerifiableCredential credential) throws DIDStoreException { checkArgument(credential != null, "Invalid credential"); storage.storeCredential(credential); if (credential.getMetadata().getStore() != this) { CredentialMetadata metadata = loadCredentialMetadata(credential.getId()); credential.getMetadata().merge(metadata); credential.getMetadata().attachStore(this); } storeCredentialMetadata(credential.getId(), credential.getMetadata()); cache.put(Key.forCredential(credential.getId()), credential); } /** * Read the specific credential object from this store. * * @param id the credential id * @return the VerifiableCredential object * @throws DIDStoreException if an error occurred when accessing the store */ public VerifiableCredential loadCredential(DIDURL id) throws DIDStoreException { checkArgument(id != null, "Invalid credential id"); try { Object value = cache.get(Key.forCredential(id), new Callable() { @Override public Object call() throws DIDStoreException { VerifiableCredential vc = storage.loadCredential(id); if (vc != null) { vc.setMetadata(loadCredentialMetadata(id)); return vc; } else { return NULL; } } }); return value == NULL ? null : (VerifiableCredential)value; } catch (ExecutionException e) { throw new DIDStoreException("Load credential failed: " + id, e); } } /** * Read the specific credential object from this store. * * @param id the credential id * @return the VerifiableCredential object * @throws DIDStoreException if an error occurred when accessing the store */ public VerifiableCredential loadCredential(String id) throws DIDStoreException { return loadCredential(DIDURL.valueOf(id)); } /** * Check whether this store contains the specific credential. * * @param id the credential id * @return true if the store contains this credential, false otherwise * @throws DIDStoreException if an error occurred when accessing the store */ public boolean containsCredential(DIDURL id) throws DIDStoreException { checkArgument(id != null, "Invalid credential id"); return loadCredential(id) != null; } /** * Check whether this store contains the specific credential. * * @param id the credential id * @return true if the store contains this credential, false otherwise * @throws DIDStoreException if an error occurred when accessing the store */ public boolean containsCredential(String id) throws DIDStoreException { return containsCredential(DIDURL.valueOf(id)); } /** * Check whether this store contains the credentials that owned by the * specific DID. * * @param did the credential owner's DID * @return true if the store contains this credential, false otherwise * @throws DIDStoreException if an error occurred when accessing the store */ public boolean containsCredentials(DID did) throws DIDStoreException { checkArgument(did != null, "Invalid did"); return storage.containsCredentials(did); } /** * Check whether this store contains the credentials that owned by the * specific DID. * * @param did the credential owner's DID * @return true if the store contains this credential, false otherwise * @throws DIDStoreException if an error occurred when accessing the store */ public boolean containsCredentials(String did) throws DIDStoreException { return containsCredentials(DID.valueOf(did)); } /** * Save the credential's metadata to this store. * * @param id the credential id * @param metadata the credential metadata object * @throws DIDStoreException if an error occurred when accessing the store */ protected void storeCredentialMetadata(DIDURL id, CredentialMetadata metadata) throws DIDStoreException { checkArgument(id != null, "Invalid credential id"); checkArgument(metadata != null, "Invalid credential metadata"); storage.storeCredentialMetadata(id, metadata); metadata.attachStore(this); cache.put(Key.forCredentialMetadata(id), metadata); } /** * Read the credential's metadata from this store. * * @param id the credential id * @return the credential metadata object * @throws DIDStoreException if an error occurred when accessing the store */ protected CredentialMetadata loadCredentialMetadata(DIDURL id) throws DIDStoreException { checkArgument(id != null, "Invalid credential id"); try { Object value = cache.get(Key.forCredentialMetadata(id), new Callable() { @Override public Object call() throws DIDStorageException { CredentialMetadata metadata = storage.loadCredentialMetadata(id); if (metadata != null) { metadata.setId(id); metadata.attachStore(DIDStore.this); } else { metadata = new CredentialMetadata(id, DIDStore.this); } return metadata; } }); return value == NULL ? null : (CredentialMetadata)value; } catch (ExecutionException e) { throw new DIDStoreException("Load Credential metadata failed: " + id, e); } } /** * Delete the specific credential from this store. * * @param id the credential id to be delete * @return true if the credential exist and deleted successful, false otherwise * @throws DIDStoreException if an error occurred when accessing the store */ public boolean deleteCredential(DIDURL id) throws DIDStoreException { checkArgument(id != null, "Invalid credential id"); boolean success = storage.deleteCredential(id); if (success) { cache.invalidate(Key.forCredential(id)); cache.invalidate(Key.forCredentialMetadata(id)); } return success; } /** * Delete the specific credential from this store. * * @param id the credential id to be delete * @return true if the credential exist and deleted successful, false otherwise * @throws DIDStoreException if an error occurred when accessing the store */ public boolean deleteCredential(String id) throws DIDStoreException { return deleteCredential(DIDURL.valueOf(id)); } /** * List all credentials that owned the specific DID. * * @param did the credential owner's DID * @return an array of DIDURL denoting the credentials * @throws DIDStoreException if an error occurred when accessing the store */ public List listCredentials(DID did) throws DIDStoreException { checkArgument(did != null, "Invalid did"); List ids = storage.listCredentials(did); for (DIDURL id : ids) { CredentialMetadata metadata = storage.loadCredentialMetadata(id); if (metadata == null) metadata = new CredentialMetadata(); metadata.setId(id); metadata.attachStore(this); id.setMetadata(metadata); } return Collections.unmodifiableList(ids); } /** * List all credentials that owned the specific DID. * * @param did the credential owner's DID * @return an array of DIDURL denoting the credentials * @throws DIDStoreException if an error occurred when accessing the store */ public List listCredentials(String did) throws DIDStoreException { return listCredentials(DID.valueOf(did)); } /** * List all credentials that owned the specific DID and satisfy the * specified filter from this store. * * @param did the credential owner's DID * @param filter a credential filter * @return an array of DIDURL denoting the credentials * @throws DIDStoreException if an error occurred when accessing the store */ public List listCredentials(DID did, CredentialFilter filter) throws DIDStoreException { checkArgument(did != null, "Invalid did"); List vcs = listCredentials(did); if (filter != null) { List dest = new ArrayList(); for (DIDURL id : vcs) { if (filter.accept(id)) dest.add(id); } vcs = dest; } return Collections.unmodifiableList(vcs); } /** * List all credentials that owned the specific DID and satisfy the * specified filter from this store. * * @param did the credential owner's DID * @param filter a credential filter * @return an array of DIDURL denoting the credentials * @throws DIDStoreException if an error occurred when accessing the store */ public List listCredentials(String did, CredentialFilter filter) throws DIDStoreException { return listCredentials(DID.valueOf(did), filter); } /** * Save the DID's lazy private key string to the store. * * @param id the private key id * @throws DIDStoreException if an error occurred when accessing the store */ private void storeLazyPrivateKey(DIDURL id) throws DIDStoreException { checkArgument(id != null, "Invalid private key id"); storage.storePrivateKey(id, DID_LAZY_PRIVATEKEY); cache.invalidate(Key.forDidPrivateKey(id)); } /** * Save the DID's private key to the store, the private key will be encrypt * using the store password. * * @param id the private key id * @param privateKey the binary extended private key * @param storepass the password for this store * @throws DIDStoreException if an error occurred when accessing the store */ public void storePrivateKey(DIDURL id, byte[] privateKey, String storepass) throws DIDStoreException { checkArgument(id != null, "Invalid private key id"); checkArgument(privateKey != null && privateKey.length != 0, "Invalid private key"); checkArgument(storepass != null && !storepass.isEmpty(), "Invalid storepass"); String encryptedKey = encrypt(privateKey, storepass); storage.storePrivateKey(id, encryptedKey); cache.put(Key.forDidPrivateKey(id), encryptedKey); } /** * Save the DID's private key to the store, the private key will be encrypt * using the store password. * * @param id the private key id * @param privateKey the binary extended private key * @param storepass the password for this store * @throws DIDStoreException if an error occurred when accessing the store */ public void storePrivateKey(String id, byte[] privateKey, String storepass) throws DIDStoreException { storePrivateKey(DIDURL.valueOf(id), privateKey, storepass); } private String loadPrivateKey(DIDURL id) throws DIDStoreException { try { Object value = cache.get(Key.forDidPrivateKey(id), new Callable() { @Override public Object call() throws DIDStoreException { String encryptedKey = storage.loadPrivateKey(id); return encryptedKey != null ? encryptedKey : NULL; } }); return value == NULL ? null : (String)value; } catch (ExecutionException e) { throw new DIDStoreException("Load did private key failed: " + id, e); } } byte[] loadPrivateKey(DIDURL id, String storepass) throws DIDStoreException { checkArgument(id != null, "Invalid private key id"); checkArgument(storepass != null && !storepass.isEmpty(), "Invalid storepass"); String encryptedKey = loadPrivateKey(id); if (encryptedKey == null || encryptedKey.isEmpty()) { return null; } else if (encryptedKey.equals(DID_LAZY_PRIVATEKEY)) { // fail-back to lazy private key generation return RootIdentity.lazyCreateDidPrivateKey(id, this, storepass); } else { return decrypt(encryptedKey, storepass); } } /** * Check if this store contains the specific private key. * * @param id the key id * @return true if this store contains the specific key, false otherwise * @throws DIDStoreException if an error occurred when accessing the store */ public boolean containsPrivateKey(DIDURL id) throws DIDStoreException { checkArgument(id != null, "Invalid private key id"); String privatekey = loadPrivateKey(id); return privatekey != null && !privatekey.isEmpty(); } /** * Check if this store contains the specific private key. * * @param id the key id * @return true if this store contains the specific key, false otherwise * @throws DIDStoreException if an error occurred when accessing the store */ public boolean containsPrivateKey(String id) throws DIDStoreException { return containsPrivateKey(DIDURL.valueOf(id)); } /** * Check if this store contains the private keys that owned by the * specific DID. * * @param did the owner's DID * @return true if this store contains the private keys owned by the the * DID, false otherwise * @throws DIDStoreException if an error occurred when accessing the store */ public boolean containsPrivateKeys(DID did) throws DIDStoreException { checkArgument(did != null, "Invalid did"); return storage.containsPrivateKeys(did); } /** * Check if this store contains the private keys that owned by the * specific DID. * * @param did the owner's DID * @return true if this store contains the private keys owned by the the * DID, false otherwise * @throws DIDStoreException if an error occurred when accessing the store */ public boolean containsPrivateKeys(String did) throws DIDStoreException { return containsPrivateKeys(DID.valueOf(did)); } /** * Delete the specific private key from this store. * * @param id the key id * @return true if the private key exist and deleted successful, false otherwise * @throws DIDStoreException if an error occurred when accessing the store */ public boolean deletePrivateKey(DIDURL id) throws DIDStoreException { checkArgument(id != null, "Invalid private key id"); boolean success = storage.deletePrivateKey(id); if (success) cache.invalidate(Key.forDidPrivateKey(id)); return success; } /** * Delete the specific private key from this store. * * @param id the key id * @return true if the private key exist and deleted successful, false otherwise * @throws DIDStoreException if an error occurred when accessing the store */ public boolean deletePrivateKey(String id) throws DIDStoreException { return deletePrivateKey(DIDURL.valueOf(id)); } /** * Sign the digest using the specified key. * * @param id the key id * @param storepass the password for this store * @param digest the binary digest in bytes array * @return the base64(URL safe) encoded signature string * @throws DIDStoreException if an error occurred when accessing the store */ protected String sign(DIDURL id, String storepass, byte[] digest) throws DIDStoreException { checkArgument(id != null, "Invalid private key id"); checkArgument(storepass != null && !storepass.isEmpty(), "Invalid storepass"); checkArgument(digest != null && digest.length > 0, "Invalid digest"); byte[] binKey = loadPrivateKey(id, storepass); if (binKey == null) throw new DIDStoreException("Key not exists: " + id.toString()); HDKey key = HDKey.deserialize(binKey); byte[] sig = EcdsaSigner.sign(key.getPrivateKeyBytes(), digest); Arrays.fill(binKey, (byte)0); key.wipe(); return Base64.encodeToString(sig, Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP); } /** * Change the password for this store. * * @param oldPassword the old password * @param newPassword the new password * @throws DIDStoreException if an error occurred when accessing the store */ public void changePassword(String oldPassword, String newPassword) throws DIDStoreException { checkArgument(oldPassword != null && !oldPassword.isEmpty(), "Invalid old password"); checkArgument(newPassword != null && !newPassword.isEmpty(), "Invalid new password"); storage.changePassword((data) -> { return DIDStore.reEncrypt(data, oldPassword, newPassword); }); metadata.setFingerprint(calcFingerprint(newPassword)); cache.invalidateAll(); } /** * Internal DID synchronize implementation. * * @param did the DID to be synchronize * @param handle an application defined handle to process the conflict * between the chain copy and the local copy * @param rootIdentity the related root identity or null if customized DID * @param index the derive index * @return true if synchronized success, false if not synchronized * @throws DIDResolveException if an error occurred when resolving DIDs * @throws DIDStoreException if an error occurred when accessing the store */ protected synchronized boolean synchronize(DID did, ConflictHandle handle, String rootIdentity, int index) throws DIDResolveException, DIDStoreException { log.info("Synchronize {}/{}...", did.toString(), (index >= 0 ? Integer.toString(index) : "n/a")); DIDDocument resolvedDoc = did.resolve(); if (resolvedDoc == null) { log.info("Synchronize {}/{}...not exists", did.toString(), (index >= 0 ? Integer.toString(index) : "n/a")); return false; } boolean isCustomizedDid = resolvedDoc.isCustomizedDid(); log.debug("Synchronize {}/{}..exists, got the on-chain copy.", did.toString(), (index >= 0 ? Integer.toString(index) : "n/a")); DIDDocument finalDoc = resolvedDoc; DIDDocument localDoc = storage.loadDid(did); if (localDoc != null) { // Update metadata off-store, then store back localDoc.setMetadata(storage.loadDidMetadata(did)); localDoc.getMetadata().detachStore(); // localdoc == resolveddoc || localdoc not modified since last publish if (localDoc.getSignature().equals(resolvedDoc.getSignature()) || (localDoc.getMetadata().getSignature() != null && localDoc.getProof().getSignature().equals( localDoc.getMetadata().getSignature()))) { finalDoc.getMetadata().merge(localDoc.getMetadata()); } else { log.debug("{} on-chain copy conflict with local copy.", did.toString()); if (handle == null) handle = defaultConflictHandle; // Local copy was modified finalDoc = handle.merge(resolvedDoc, localDoc); if (finalDoc == null || !finalDoc.getSubject().equals(did)) { localDoc.getMetadata().attachStore(this); log.error("Conflict handle merge the DIDDocument error."); throw new DIDStoreException("deal with local modification error."); } else { log.debug("Conflict handle return the final copy."); } } } DIDMetadata metadata = finalDoc.getMetadata(); metadata.setPublishTime(resolvedDoc.getMetadata().getPublishTime()); metadata.setSignature(resolvedDoc.getProof().getSignature()); if (resolvedDoc.getMetadata().isDeactivated()) metadata.setDeactivated(true); if (!isCustomizedDid && rootIdentity != null) { metadata.setRootIdentityId(rootIdentity); metadata.setIndex(index); } metadata.attachStore(this); finalDoc.setMetadata(metadata); if (localDoc != null) localDoc.getMetadata().attachStore(this); // Invalidate current cached items cache.invalidate(Key.forDidDocument(did)); cache.invalidate(Key.forDidMetadata(did)); storage.storeDid(finalDoc); storage.storeDidMetadata(did, metadata); for (VerifiableCredential vc : finalDoc.getCredentials()) storage.storeCredential(vc); if (!isCustomizedDid && rootIdentity != null) storeLazyPrivateKey(finalDoc.getDefaultPublicKeyId()); List vcIds = storage.listCredentials(did); for (DIDURL vcId : vcIds) { VerifiableCredential localVc = storage.loadCredential(vcId); VerifiableCredential resolvedVc = VerifiableCredential.resolve(vcId, localVc.getIssuer()); if (resolvedVc == null) continue; resolvedVc.getMetadata().merge(localVc.getMetadata()); cache.invalidate(Key.forCredential(vcId)); cache.invalidate(Key.forCredentialMetadata(vcId)); storage.storeCredential(resolvedVc); storage.storeCredentialMetadata(vcId, resolvedVc.getMetadata()); } return true; } /** * Synchronize all RootIdentities, DIDs and credentials in this store. * *

* If the ConflictHandle is not set by the developers, this method will * use the default ConflictHandle implementation: if conflict between * the chain copy and the local copy, it will keep the local copy, but * update the local metadata with the chain copy. *

* * @param handle an application defined handle to process the conflict * between the chain copy and the local copy * @throws DIDResolveException if an error occurred when resolving DIDs * @throws DIDStoreException if an error occurred when accessing the store */ public void synchronize(ConflictHandle handle) throws DIDResolveException, DIDStoreException { if (handle == null) handle = defaultConflictHandle; List identities = listRootIdentities(); for (RootIdentity identity : identities) { identity.synchronize(handle); } List dids = storage.listDids(); for (DID did : dids) { DIDDocument doc = storage.loadDid(did); if (doc.isCustomizedDid()) synchronize(did, handle, null, -1); } } /** * Synchronize all RootIdentities, DIDs and credentials in this store. * * @throws DIDResolveException if an error occurred when resolving DIDs * @throws DIDStoreException if an error occurred when accessing the store */ public void synchronize() throws DIDResolveException, DIDStoreException { synchronize((ConflictHandle)null); } /** * Synchronize all RootIdentities, DIDs and credentials in * asynchronous mode. * *

* If the ConflictHandle is not set by the developers, this method will * use the default ConflictHandle implementation: if conflict between * the chain copy and the local copy, it will keep the local copy, but * update the local metadata with the chain copy. *

* * @param handle an application defined handle to process the conflict * between the chain copy and the local copy * @return a new CompletableStage */ public CompletableFuture synchronizeAsync(ConflictHandle handle) { CompletableFuture future = CompletableFuture.runAsync(() -> { try { synchronize(handle); } catch (DIDResolveException | DIDStoreException e) { throw new CompletionException(e); } }); return future; } /** * Synchronize all RootIdentities, DIDs and credentials in * asynchronous mode. * * @return a new CompletableStage */ public CompletableFuture synchronizeAsync() { return synchronizeAsync((ConflictHandle)null); } /** * Synchronize specific DID in this store. * *

* If the ConflictHandle is not set by the developers, this method will * use the default ConflictHandle implementation: if conflict between * the chain copy and the local copy, it will keep the local copy, but * update the local metadata with the chain copy. *

* * @param did the DID to be synchronize * @param handle an application defined handle to process the conflict * between the chain copy and the local copy * @return true if synchronized success, false if not synchronized * * @throws DIDResolveException if an error occurred when resolving DIDs * @throws DIDStoreException if an error occurred when accessing the store */ public boolean synchronize(DID did, ConflictHandle handle) throws DIDResolveException, DIDStoreException { checkArgument(did != null, "Invalid DID"); DIDDocument doc = loadDid(did); if (doc == null) return false; String rootIdentity = null; int index = -1; if (!doc.isCustomizedDid()) { rootIdentity = doc.getMetadata().getRootIdentityId(); index = doc.getMetadata().getIndex(); } return synchronize(did, handle, rootIdentity, index); } /** * Synchronize specific DID in this store. * * @param did the DID to be synchronize * @return true if synchronized success, false if not synchronized * * @throws DIDResolveException if an error occurred when resolving DIDs * @throws DIDStoreException if an error occurred when accessing the store */ public boolean synchronize(DID did) throws DIDResolveException, DIDStoreException { return synchronize(did, null); } /** * Synchronize specific DID in asynchronous mode. * *

* If the ConflictHandle is not set by the developers, this method will * use the default ConflictHandle implementation: if conflict between * the chain copy and the local copy, it will keep the local copy, but * update the local metadata with the chain copy. *

* * @param did the DID to be synchronize * @param handle an application defined handle to process the conflict * between the chain copy and the local copy * @return a new CompletableStage */ public CompletableFuture synchronizeAsync(DID did, ConflictHandle handle) { CompletableFuture future = CompletableFuture.runAsync(() -> { try { synchronize(did, handle); } catch (DIDResolveException | DIDStoreException e) { throw new CompletionException(e); } }); return future; } /** * Synchronize specific DID in asynchronous mode. * * @param did the DID to be synchronize * @return a new CompletableStage */ public CompletableFuture synchronizeAsync(DID did) { return synchronizeAsync(did, null); } @JsonPropertyOrder({ "type", "id", "document", "credential", "privateKey", "created", "fingerprint" }) @JsonInclude(Include.NON_NULL) static class DIDExport extends DIDEntity { @JsonProperty("type") private String type; @JsonProperty("id") private DID id; @JsonProperty("document") private Document document; @JsonProperty("credential") private List credentials; @JsonProperty("privateKey") private List privatekeys; @JsonProperty("created") private Date created; @JsonProperty("fingerprint") private String fingerprint; @JsonPropertyOrder({ "content", "metadata" }) @JsonInclude(Include.NON_NULL) static class Document { @JsonProperty("content") private DIDDocument content; @JsonProperty("metadata") private DIDMetadata metadata; @JsonCreator protected Document(@JsonProperty(value = "content", required = true) DIDDocument content, @JsonProperty(value = "metadata") DIDMetadata metadata) { this.content = content; this.metadata = metadata; } } @JsonPropertyOrder({ "content", "metadata" }) @JsonInclude(Include.NON_NULL) static class Credential { @JsonProperty("content") private VerifiableCredential content; @JsonProperty("metadata") private CredentialMetadata metadata; @JsonCreator protected Credential(@JsonProperty(value = "content", required = true) VerifiableCredential content, @JsonProperty(value = "metadata") CredentialMetadata metadata) { this.content = content; this.metadata = metadata; } } @JsonPropertyOrder({ "id", "key" }) static class PrivateKey { @JsonProperty("id") private DIDURL id; @JsonProperty("key") private String key; @JsonCreator protected PrivateKey(@JsonProperty(value = "id", required = true) DIDURL id) { this.id = id; } public DIDURL getId() { return id; } public void setId(DIDURL id) { this.id = id; } public String getKey(String exportpass, String storepass) throws DIDStoreException { return reEncrypt(key, exportpass, storepass); } public void setKey(String key, String storepass, String exportpass) throws DIDStoreException { this.key = reEncrypt(key, storepass, exportpass); } } @JsonCreator protected DIDExport(@JsonProperty(value = "type", required = true) String type, @JsonProperty(value = "id", required = true) DID id) { if (type == null) throw new IllegalArgumentException("Invalid export type"); this.type = type; this.id = id; } public DID getId() { return id; } public DIDDocument getDocument() { return document.content; } public void setDocument(DIDDocument doc) { this.document = new Document(doc, doc.getMetadata().isEmpty() ? null : doc.getMetadata()); } public List getCredentials() { if (credentials == null) return Collections.emptyList(); List vcs = new ArrayList(); for (Credential cred : credentials) vcs.add(cred.content); return vcs; } public void addCredential(VerifiableCredential credential) { if (this.credentials == null) this.credentials = new ArrayList(); this.credentials.add(new Credential(credential, credential.getMetadata().isEmpty() ? null : credential.getMetadata())); } public List getPrivateKeys() { return privatekeys != null ? privatekeys : Collections.emptyList(); } public void addPrivatekey(DIDURL id, String privatekey, String storepass, String exportpass) throws DIDStoreException { if (this.privatekeys == null) this.privatekeys = new ArrayList(); PrivateKey sk = new PrivateKey(id); sk.setKey(privatekey, storepass, exportpass); this.privatekeys.add(sk); } private String calculateFingerprint(String exportpass) { SHA256Digest sha256 = new SHA256Digest(); byte[] bytes = exportpass.getBytes(); sha256.update(bytes, 0, bytes.length); bytes = type.getBytes(); sha256.update(bytes, 0, bytes.length); bytes = id.toString().getBytes(); sha256.update(bytes, 0, bytes.length); bytes = document.content.toString(true).getBytes(); sha256.update(bytes, 0, bytes.length); if (document.metadata != null) { bytes = document.metadata.toString(true).getBytes(); sha256.update(bytes, 0, bytes.length); } if (credentials != null && credentials.size() > 0) { for (Credential cred : credentials) { bytes = cred.content.toString(true).getBytes(); sha256.update(bytes, 0, bytes.length); if (cred.metadata != null) { bytes = cred.metadata.toString(true).getBytes(); sha256.update(bytes, 0, bytes.length); } } } if (privatekeys != null && privatekeys.size() > 0) { for (PrivateKey sk : privatekeys) { bytes = sk.id.toString().getBytes(); sha256.update(bytes, 0, bytes.length); bytes = sk.key.getBytes(); sha256.update(bytes, 0, bytes.length); } } bytes = dateFormat.format(created).getBytes(); sha256.update(bytes, 0, bytes.length); byte digest[] = new byte[32]; sha256.doFinal(digest, 0); return Base64.encodeToString(digest, Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP); } public DIDExport seal(String exportpass) { Calendar now = Calendar.getInstance(); now.set(Calendar.MILLISECOND, 0); this.created = now.getTime(); fingerprint = calculateFingerprint(exportpass); return this; } public void verify(String exportpass) throws MalformedExportDataException { if (!fingerprint.equals(calculateFingerprint(exportpass))) throw new MalformedExportDataException( "Invalid export data, fingerprint mismatch."); } @Override protected void sanitize() throws MalformedExportDataException { if (type == null || !type.equals(DID_EXPORT)) throw new MalformedExportDataException( "Invalid export data, unknown type."); if (created == null) throw new MalformedExportDataException( "Invalid export data, missing created time."); if (id == null) throw new MalformedExportDataException( "Invalid export data, missing id."); if (document == null || document.content == null) throw new MalformedExportDataException( "Invalid export data, missing document."); document.content.setMetadata(document.metadata); if (credentials != null) { for (Credential cred : credentials) { if (cred == null || cred.content == null) throw new MalformedExportDataException( "Invalid export data, invalid credential."); cred.content.setMetadata(cred.metadata); } } if (privatekeys != null) { for (PrivateKey sk : privatekeys) { if (sk == null || sk.id == null || sk.key == null || sk.key.isEmpty()) throw new MalformedExportDataException( "Invalid export data, invalid privatekey."); } } if (fingerprint == null || fingerprint.isEmpty()) throw new MalformedExportDataException( "Invalid export data, missing fingerprint."); } } private DIDExport exportDid(DID did, String password, String storepass) throws DIDStoreException, IOException { // All objects should load directly from storage, // avoid affects the cached objects. DIDDocument doc = storage.loadDid(did); if (doc == null) throw new DIDStoreException("Export DID " + did + " failed, not exist."); doc.setMetadata(storage.loadDidMetadata(did)); log.debug("Exporting {}...", did.toString()); DIDExport de = new DIDExport(DID_EXPORT, did); de.setDocument(doc); if (storage.containsCredentials(did)) { List ids = new ArrayList(listCredentials(did)); Collections.sort(ids); for (DIDURL id : ids) { log.debug("Exporting credential {}...", id.toString()); VerifiableCredential vc = storage.loadCredential(id); vc.setMetadata(storage.loadCredentialMetadata(id)); de.addCredential(vc); } } if (storage.containsPrivateKeys(did)) { List pks = doc.getPublicKeys(); for (PublicKey pk : pks) { if (!pk.getController().equals(did)) continue; DIDURL id = pk.getId(); String key = storage.loadPrivateKey(id); if (key != null) { log.debug("Exporting private key {}...", id.toString()); de.addPrivatekey(id, key, storepass, password); } } } return de.seal(password); } /** * Export the specific DID with all DID objects that related with this DID, * include: document, credentials, private keys and their metadata. * * @param did the DID to be export * @param out the output stream that the data export to * @param password the password to encrypt the private keys in the exported data * @param storepass the password for this store * @throws DIDStoreException if an error occurred when accessing the store * @throws IOException if an IO error occurred when writing the exported data */ public void exportDid(DID did, OutputStream out, String password, String storepass) throws DIDStoreException, IOException { checkArgument(did != null, "Invalid did"); checkArgument(out != null, "Invalid output stream"); checkArgument(password != null && !password.isEmpty(), "Invalid password"); checkArgument(storepass != null && !storepass.isEmpty(), "Invaid store password"); exportDid(did, password, storepass).serialize(out, true); } /** * Export the specific DID with all DID objects that related with this DID, * include: document, credentials, private keys and their metadata. * * @param did the DID to be export * @param out the output stream that the data export to * @param password the password to encrypt the private keys in the exported data * @param storepass the password for this store * @throws DIDStoreException if an error occurred when accessing the store * @throws IOException if an IO error occurred when writing the exported data */ public void exportDid(String did, OutputStream out, String password, String storepass) throws DIDStoreException, IOException { exportDid(DID.valueOf(did), out, password, storepass); } /** * Export the specific DID with all DID objects that related with this DID, * include: document, credentials, private keys and their metadata. * * @param did the DID to be export * @param out the writer object that the data export to * @param password the password to encrypt the private keys in the exported data * @param storepass the password for this store * @throws DIDStoreException if an error occurred when accessing the store * @throws IOException if an IO error occurred when writing the exported data */ public void exportDid(DID did, Writer out, String password, String storepass) throws DIDStoreException, IOException { checkArgument(did != null, "Invalid did"); checkArgument(out != null, "Invalid output writer"); checkArgument(password != null && !password.isEmpty(), "Invalid password"); checkArgument(storepass != null && !storepass.isEmpty(), "Invaid store password"); exportDid(did, password, storepass).serialize(out, true); } /** * Export the specific DID with all DID objects that related with this DID, * include: document, credentials, private keys and their metadata. * * @param did the DID to be export * @param out the writer object that the data export to * @param password the password to encrypt the private keys in the exported data * @param storepass the password for this store * @throws DIDStoreException if an error occurred when accessing the store * @throws IOException if an IO error occurred when writing the exported data */ public void exportDid(String did, Writer out, String password, String storepass) throws DIDStoreException, IOException { exportDid(DID.valueOf(did), out, password, storepass); } /** * Export the specific DID with all DID objects that related with this DID, * include: document, credentials, private keys and their metadata. * * @param did the DID to be export * @param file the File object that the data export to * @param password the password to encrypt the private keys in the exported data * @param storepass the password for this store * @throws DIDStoreException if an error occurred when accessing the store * @throws IOException if an IO error occurred when writing the exported data */ public void exportDid(DID did, File file, String password, String storepass) throws DIDStoreException, IOException { checkArgument(did != null, "Invalid did"); checkArgument(file != null, "Invalid output file"); checkArgument(password != null && !password.isEmpty(), "Invalid password"); checkArgument(storepass != null && !storepass.isEmpty(), "Invaid store password"); exportDid(did, password, storepass).serialize(file, true); } /** * Export the specific DID with all DID objects that related with this DID, * include: document, credentials, private keys and their metadata. * * @param did the DID to be export * @param file the File object that the data export to * @param password the password to encrypt the private keys in the exported data * @param storepass the password for this store * @throws DIDStoreException if an error occurred when accessing the store * @throws IOException if an IO error occurred when writing the exported data */ public void exportDid(String did, File file, String password, String storepass) throws DIDStoreException, IOException { exportDid(DID.valueOf(did), file, password, storepass); } /** * Export the specific DID with all DID objects that related with this DID, * include: document, credentials, private keys and their metadata. * * @param did the DID to be export * @param file the file name that the data export to * @param password the password to encrypt the private keys in the exported data * @param storepass the password for this store * @throws DIDStoreException if an error occurred when accessing the store * @throws IOException if an IO error occurred when writing the exported data */ public void exportDid(DID did, String file, String password, String storepass) throws DIDStoreException, IOException { checkArgument(did != null, "Invalid did"); checkArgument(file != null && !file.isEmpty(), "Invalid output file name"); checkArgument(password != null && !password.isEmpty(), "Invalid password"); checkArgument(storepass != null && !storepass.isEmpty(), "Invaid store password"); exportDid(did, new File(file), password, storepass); } /** * Export the specific DID with all DID objects that related with this DID, * include: document, credentials, private keys and their metadata. * * @param did the DID to be export * @param file the file name that the data export to * @param password the password to encrypt the private keys in the exported data * @param storepass the password for this store * @throws DIDStoreException if an error occurred when accessing the store * @throws IOException if an IO error occurred when writing the exported data */ public void exportDid(String did, String file, String password, String storepass) throws DIDStoreException, IOException { exportDid(DID.valueOf(did), file, password, storepass); } private void importDid(DIDExport de, String password, String storepass) throws MalformedExportDataException, DIDStoreException, IOException { de.verify(password); // Save log.debug("Importing document..."); DIDDocument doc = de.document.content; storage.storeDid(doc); storage.storeDidMetadata(doc.getSubject(), doc.getMetadata()); List vcs = de.getCredentials(); for (VerifiableCredential vc : vcs) { log.debug("Importing credential {}...", vc.getId().toString()); storage.storeCredential(vc); storage.storeCredentialMetadata(vc.getId(), vc.getMetadata()); } List sks = de.getPrivateKeys(); for (DIDExport.PrivateKey sk : sks) { log.debug("Importing private key {}...", sk.getId().toString()); storage.storePrivateKey(sk.getId(), sk.getKey(password, storepass)); } } /** * Import a DID and all related DID object from the exported data to * this store. * * @param in the input stream for the exported data * @param password the password for the exported data * @param storepass the password for this store * @throws MalformedExportDataException if the exported data is invalid * @throws DIDStoreException if an error occurred when accessing the store * @throws IOException if an IO error occurred when reading the exported data */ public void importDid(InputStream in, String password, String storepass) throws MalformedExportDataException, DIDStoreException, IOException { checkArgument(in != null, "Invalid input stream"); checkArgument(password != null && !password.isEmpty(), "Invalid password"); checkArgument(storepass != null && !storepass.isEmpty(), "Invaid store password"); DIDExport de; try { de = DIDExport.parse(in, DIDExport.class); } catch (DIDSyntaxException e) { throw (MalformedExportDataException)e; } importDid(de, password, storepass); } /** * Import a DID and all related DID object from the exported data to * this store. * * @param in the reader object for the exported data * @param password the password for the exported data * @param storepass the password for this store * @throws MalformedExportDataException if the exported data is invalid * @throws DIDStoreException if an error occurred when accessing the store * @throws IOException if an IO error occurred when reading the exported data */ public void importDid(Reader in, String password, String storepass) throws MalformedExportDataException, DIDStoreException, IOException { checkArgument(in != null, "Invalid input reader"); checkArgument(password != null && !password.isEmpty(), "Invalid password"); checkArgument(storepass != null && !storepass.isEmpty(), "Invaid store password"); DIDExport de; try { de = DIDExport.parse(in, DIDExport.class); } catch (DIDSyntaxException e) { throw (MalformedExportDataException)e; } importDid(de, password, storepass); } /** * Import a DID and all related DID object from the exported data to * this store. * * @param file the file object for the exported data * @param password the password for the exported data * @param storepass the password for this store * @throws MalformedExportDataException if the exported data is invalid * @throws DIDStoreException if an error occurred when accessing the store * @throws IOException if an IO error occurred when reading the exported data */ public void importDid(File file, String password, String storepass) throws MalformedExportDataException, DIDStoreException, IOException { checkArgument(file != null, "Invalid input file"); checkArgument(password != null && !password.isEmpty(), "Invalid password"); checkArgument(storepass != null && !storepass.isEmpty(), "Invaid store password"); DIDExport de; try { de = DIDExport.parse(file, DIDExport.class); } catch (DIDSyntaxException e) { throw (MalformedExportDataException)e; } importDid(de, password, storepass); } /** * Import a DID and all related DID object from the exported data to * this store. * * @param file the file name for the exported data * @param password the password for the exported data * @param storepass the password for this store * @throws MalformedExportDataException if the exported data is invalid * @throws DIDStoreException if an error occurred when accessing the store * @throws IOException if an IO error occurred when reading the exported data */ public void importDid(String file, String password, String storepass) throws MalformedExportDataException, DIDStoreException, IOException { checkArgument(file != null, "Invalid input file name"); checkArgument(password != null && !password.isEmpty(), "Invalid password"); checkArgument(storepass != null && !storepass.isEmpty(), "Invaid store password"); importDid(new File(file), password, storepass); } @JsonPropertyOrder({ "type", "mnemonic", "privateKey", "publicKey", "index", "default", "created", "fingerprint" }) @JsonInclude(Include.NON_NULL) static class RootIdentityExport extends DIDEntity { @JsonProperty("type") private String type; @JsonProperty("mnemonic") private String mnemonic; @JsonProperty("privateKey") private String privateKey; @JsonProperty("publicKey") private String publicKey; @JsonProperty("index") @JsonSerialize(using = ToStringSerializer.class) private int index; @JsonProperty("default") @JsonSerialize(using = ToStringSerializer.class) @JsonInclude(Include.NON_NULL) private Boolean isDefault; @JsonProperty("created") private Date created; @JsonProperty("fingerprint") private String fingerprint; @JsonCreator protected RootIdentityExport(@JsonProperty(value = "type", required = true) String type) { if (type == null) throw new IllegalArgumentException("Invalid export type"); this.type = type; } public String getMnemonic(String exportpass, String storepass) throws DIDStoreException { return mnemonic == null ? null : reEncrypt(mnemonic, exportpass, storepass); } public void setMnemonic(String mnemonic, String storepass, String exportpass) throws DIDStoreException { this.mnemonic = reEncrypt(mnemonic, storepass, exportpass); } public String getPrivateKey(String exportpass, String storepass) throws DIDStoreException { return reEncrypt(privateKey, exportpass, storepass); } public void setPrivateKey(String privateKey, String storepass, String exportpass) throws DIDStoreException { this.privateKey = reEncrypt(privateKey, storepass, exportpass); } public String getPublicKey() { return publicKey; } public void setPubkey(String publicKey) { this.publicKey = publicKey; } public int getIndex() { return index; } public void setIndex(int index) { this.index = index; } public boolean isDefault() { return isDefault == null ? false : isDefault; } public void setDefault() { isDefault = Boolean.valueOf(true); } private String calculateFingerprint(String exportpass) { SHA256Digest sha256 = new SHA256Digest(); byte[] bytes = exportpass.getBytes(); sha256.update(bytes, 0, bytes.length); bytes = type.getBytes(); sha256.update(bytes, 0, bytes.length); if (mnemonic != null) { bytes = mnemonic.getBytes(); sha256.update(bytes, 0, bytes.length); } bytes = privateKey.getBytes(); sha256.update(bytes, 0, bytes.length); bytes = publicKey.getBytes(); sha256.update(bytes, 0, bytes.length); bytes = Integer.toString(index).getBytes(); sha256.update(bytes, 0, bytes.length); bytes = Boolean.toString(isDefault()).getBytes(); sha256.update(bytes, 0, bytes.length); bytes = dateFormat.format(created).getBytes(); sha256.update(bytes, 0, bytes.length); byte digest[] = new byte[32]; sha256.doFinal(digest, 0); return Base64.encodeToString(digest, Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP); } public RootIdentityExport seal(String exportpass) { Calendar now = Calendar.getInstance(); now.set(Calendar.MILLISECOND, 0); this.created = now.getTime(); this.fingerprint = calculateFingerprint(exportpass); return this; } public void verify(String exportpass) throws MalformedExportDataException { if (!fingerprint.equals(calculateFingerprint(exportpass))) throw new MalformedExportDataException( "Invalid export data, fingerprint mismatch."); } @Override protected void sanitize() throws MalformedExportDataException { if (type == null || !type.equals(DID_EXPORT)) throw new MalformedExportDataException( "Invalid export data, unknown type."); if (created == null) throw new MalformedExportDataException( "Invalid export data, missing created time."); if (privateKey == null || privateKey.isEmpty()) throw new MalformedExportDataException( "Invalid export data, missing key."); if (fingerprint == null || fingerprint.isEmpty()) throw new MalformedExportDataException( "Invalid export data, missing fingerprint."); } } private RootIdentityExport exportRootIdentity(String id, String password, String storepass) throws DIDStoreException { RootIdentityExport rie = new RootIdentityExport(DID_EXPORT); // TODO: support multiple named root identities String mnemonic = storage.loadRootIdentityMnemonic(id); if (mnemonic != null) rie.setMnemonic(mnemonic, storepass, password); rie.setPrivateKey(storage.loadRootIdentityPrivateKey(id), storepass, password); RootIdentity identity = storage.loadRootIdentity(id); rie.setPubkey(identity.getPreDerivedPublicKey().serializePublicKeyBase58()); rie.setIndex(identity.getIndex()); if (identity.getId().equals(metadata.getDefaultRootIdentity())) rie.setDefault(); return rie.seal(password); } /** * Export the specific RootIdentity, include: mnemonic, private key, * pre-derived public key, derive index, metadata... * * @param id the id of the RootIdentity to be export * @param out the output stream that the data export to * @param password the password to encrypt the private keys in the exported data * @param storepass the password for this store * @throws DIDStoreException if an error occurred when accessing the store * @throws IOException if an IO error occurred when writing the exported data */ public void exportRootIdentity(String id, OutputStream out, String password, String storepass) throws DIDStoreException, IOException { checkArgument(id != null && !id.isEmpty(), "Invalid identity id"); checkArgument(out != null, "Invalid output stream"); checkArgument(password != null && !password.isEmpty(), "Invalid password"); checkArgument(storepass != null && !storepass.isEmpty(), "Invalid storepass"); exportRootIdentity(id, password, storepass).serialize(out); } /** * Export the specific RootIdentity, include: mnemonic, private key, * pre-derived public key, derive index, metadata... * * @param id the id of the RootIdentity to be export * @param out the writer object that the data export to * @param password the password to encrypt the private keys in the exported data * @param storepass the password for this store * @throws DIDStoreException if an error occurred when accessing the store * @throws IOException if an IO error occurred when writing the exported data */ public void exportRootIdentity(String id, Writer out, String password, String storepass) throws DIDStoreException, IOException { checkArgument(id != null && !id.isEmpty(), "Invalid identity id"); checkArgument(out != null, "Invalid output writer"); checkArgument(password != null && !password.isEmpty(), "Invalid password"); checkArgument(storepass != null && !storepass.isEmpty(), "Invalid storepass"); exportRootIdentity(id, password, storepass).serialize(out); } /** * Export the specific RootIdentity, include: mnemonic, private key, * pre-derived public key, derive index, metadata... * * @param id the id of the RootIdentity to be export * @param file the file object that the data export to * @param password the password to encrypt the private keys in the exported data * @param storepass the password for this store * @throws DIDStoreException if an error occurred when accessing the store * @throws IOException if an IO error occurred when writing the exported data */ public void exportRootIdentity(String id, File file, String password, String storepass) throws DIDStoreException, IOException { checkArgument(id != null && !id.isEmpty(), "Invalid identity id"); checkArgument(file != null, "Invalid output file"); checkArgument(password != null && !password.isEmpty(), "Invalid password"); checkArgument(storepass != null && !storepass.isEmpty(), "Invalid storepass"); exportRootIdentity(id, password, storepass).serialize(file); } /** * Export the specific RootIdentity, include: mnemonic, private key, * pre-derived public key, derive index, metadata... * * @param id the id of the RootIdentity to be export * @param file the file name that the data export to * @param password the password to encrypt the private keys in the exported data * @param storepass the password for this store * @throws DIDStoreException if an error occurred when accessing the store * @throws IOException if an IO error occurred when writing the exported data */ public void exportRootIdentity(String id, String file, String password, String storepass) throws DIDStoreException, IOException { checkArgument(id != null && !id.isEmpty(), "Invalid identity id"); checkArgument(file != null && !file.isEmpty(), "Invalid output file name"); checkArgument(password != null && !password.isEmpty(), "Invalid password"); checkArgument(storepass != null && !storepass.isEmpty(), "Invalid storepass"); exportRootIdentity(id, new File(file), password, storepass); } private void importRootIdentity(RootIdentityExport rie, String password, String storepass) throws MalformedExportDataException, DIDStoreException, IOException { rie.verify(password); // Save String encryptedMnemonic = rie.getMnemonic(password, storepass); String encryptedPrivateKey = (rie.getPrivateKey(password, storepass)); String publicKey = rie.getPublicKey(); HDKey pk = HDKey.deserializeBase58(publicKey); String id = RootIdentity.getId(pk.serializePublicKey()); storage.storeRootIdentity(id, encryptedMnemonic, encryptedPrivateKey, publicKey, rie.getIndex()); if (rie.isDefault() && metadata.getDefaultRootIdentity() == null) metadata.setDefaultRootIdentity(id); } /** * Import a RootIdentity object from the exported data to this store. * * @param in the input stream for the exported data * @param password the password for the exported data * @param storepass the password for this store * @throws MalformedExportDataException if the exported data is invalid * @throws DIDStoreException if an error occurred when accessing the store * @throws IOException if an IO error occurred when reading the exported data */ public void importRootIdentity(InputStream in, String password, String storepass) throws MalformedExportDataException, DIDStoreException, IOException { checkArgument(in != null, "Invalid input stream"); checkArgument(password != null && !password.isEmpty(), "Invalid password"); checkArgument(storepass != null && !storepass.isEmpty(), "Invalid storepass"); try { RootIdentityExport rie = RootIdentityExport.parse(in, RootIdentityExport.class); importRootIdentity(rie, password, storepass); } catch (DIDSyntaxException e) { throw (MalformedExportDataException)e; } } /** * Import a RootIdentity object from the exported data to this store. * * @param in the reader object for the exported data * @param password the password for the exported data * @param storepass the password for this store * @throws MalformedExportDataException if the exported data is invalid * @throws DIDStoreException if an error occurred when accessing the store * @throws IOException if an IO error occurred when reading the exported data */ public void importRootIdentity(Reader in, String password, String storepass) throws MalformedExportDataException, DIDStoreException, IOException { checkArgument(in != null, "Invalid input reader"); checkArgument(password != null && !password.isEmpty(), "Invalid password"); checkArgument(storepass != null && !storepass.isEmpty(), "Invalid storepass"); try { RootIdentityExport rie = RootIdentityExport.parse(in, RootIdentityExport.class); importRootIdentity(rie, password, storepass); } catch (DIDSyntaxException e) { throw (MalformedExportDataException)e; } } /** * Import a RootIdentity object from the exported data to this store. * * @param file the file object for the exported data * @param password the password for the exported data * @param storepass the password for this store * @throws MalformedExportDataException if the exported data is invalid * @throws DIDStoreException if an error occurred when accessing the store * @throws IOException if an IO error occurred when reading the exported data */ public void importRootIdentity(File file, String password, String storepass) throws MalformedExportDataException, DIDStoreException, IOException { checkArgument(file != null, "Invalid input file"); checkArgument(password != null && !password.isEmpty(), "Invalid password"); checkArgument(storepass != null && !storepass.isEmpty(), "Invalid storepass"); try { RootIdentityExport rie = RootIdentityExport.parse(file, RootIdentityExport.class); importRootIdentity(rie, password, storepass); } catch (DIDSyntaxException e) { throw (MalformedExportDataException)e; } } /** * Import a RootIdentity object from the exported data to this store. * * @param file the file name for the exported data * @param password the password for the exported data * @param storepass the password for this store * @throws MalformedExportDataException if the exported data is invalid * @throws DIDStoreException if an error occurred when accessing the store * @throws IOException if an IO error occurred when reading the exported data */ public void importRootIdentity(String file, String password, String storepass) throws MalformedExportDataException, DIDStoreException, IOException { checkArgument(file != null && !file.isEmpty(), "Invalid input file name"); checkArgument(password != null && !password.isEmpty(), "Invalid password"); checkArgument(storepass != null && !storepass.isEmpty(), "Invalid storepass"); importRootIdentity(new File(file), password, storepass); } /** * Export all DID objects from this store. * * @param out the zip output stream that the data export to * @param password the password to encrypt the private keys in the exported data * @param storepass the password for this store * @throws DIDStoreException if an error occurred when accessing the store * @throws IOException if an IO error occurred when writing the exported data */ public void exportStore(ZipOutputStream out, String password, String storepass) throws DIDStoreException, IOException { checkArgument(out != null, "Invalid zip output stream"); checkArgument(password != null && !password.isEmpty(), "Invalid password"); checkArgument(storepass != null && !storepass.isEmpty(), "Invalid storepass"); ZipEntry ze; List ris = listRootIdentities(); for (RootIdentity ri : ris) { ze = new ZipEntry("rootIdentity-" + ri.getId()); out.putNextEntry(ze); exportRootIdentity(ri.getId(), out, password, storepass); out.closeEntry(); } List dids = listDids(); for (DID did : dids) { ze = new ZipEntry("did-" + did.getMethodSpecificId()); out.putNextEntry(ze); exportDid(did, out, password, storepass); out.closeEntry(); } } /** * Export all DID objects from this store. * * @param zipFile the ZipFile object that the data export to * @param password the password to encrypt the private keys in the exported data * @param storepass the password for this store * @throws DIDStoreException if an error occurred when accessing the store * @throws IOException if an IO error occurred when writing the exported data */ public void exportStore(File zipFile, String password, String storepass) throws DIDStoreException, IOException { checkArgument(zipFile != null, "Invalid zip output file"); checkArgument(password != null && !password.isEmpty(), "Invalid password"); checkArgument(storepass != null && !storepass.isEmpty(), "Invalid storepass"); ZipOutputStream out = new ZipOutputStream(new FileOutputStream(zipFile)); exportStore(out, password, storepass); out.close(); } /** * Export all DID objects from this store. * * @param zipFile the zip file name that the data export to * @param password the password to encrypt the private keys in the exported data * @param storepass the password for this store * @throws DIDStoreException if an error occurred when accessing the store * @throws IOException if an IO error occurred when writing the exported data */ public void exportStore(String zipFile, String password, String storepass) throws DIDStoreException, IOException { checkArgument(zipFile != null && !zipFile.isEmpty(), "Invalid zip output file name"); checkArgument(password != null && !password.isEmpty(), "Invalid password"); checkArgument(storepass != null && !storepass.isEmpty(), "Invalid storepass"); exportStore(new File(zipFile), password, storepass); } /** * Import a exported DIDStore from the exported data to this store. * * @param in the zip input stream for the exported data * @param password the password for the exported data * @param storepass the password for this store * @throws MalformedExportDataException if the exported data is invalid * @throws DIDStoreException if an error occurred when accessing the store * @throws IOException if an IO error occurred when reading the exported data */ public void importStore(ZipInputStream in, String password, String storepass) throws MalformedExportDataException, DIDStoreException, IOException { checkArgument(in != null, "Invalid zip input stream"); checkArgument(password != null && !password.isEmpty(), "Invalid password"); checkArgument(storepass != null && !storepass.isEmpty(), "Invalid storepass"); String fingerprint = metadata.getFingerprint(); String currentFingerprint = calcFingerprint(storepass); if (fingerprint != null && !currentFingerprint.equals(fingerprint)) throw new WrongPasswordException("Password mismatched with previous password."); ZipEntry ze; while ((ze = in.getNextEntry()) != null) { if (ze.getName().startsWith("rootIdentity")) importRootIdentity(in, password, storepass); else if (ze.getName().startsWith("did")) importDid(in, password, storepass); else log.warn("Skip unknow export entry: " + ze.getName()); in.closeEntry(); } if (fingerprint == null || fingerprint.isEmpty()) metadata.setFingerprint(currentFingerprint); } /** * Import a exported DIDStore from the exported data to this store. * * @param zipFile the ZipFile object for the exported data * @param password the password for the exported data * @param storepass the password for this store * @throws MalformedExportDataException if the exported data is invalid * @throws DIDStoreException if an error occurred when accessing the store * @throws IOException if an IO error occurred when reading the exported data */ public void importStore(File zipFile, String password, String storepass) throws MalformedExportDataException, DIDStoreException, IOException { checkArgument(zipFile != null, "Invalid zip input file"); checkArgument(password != null && !password.isEmpty(), "Invalid password"); checkArgument(storepass != null && !storepass.isEmpty(), "Invalid storepass"); ZipInputStream in = new ZipInputStream(new FileInputStream(zipFile)); importStore(in, password, storepass); in.close(); } /** * Import a exported DIDStore from the exported data to this store. * * @param zipFile the zip file name for the exported data * @param password the password for the exported data * @param storepass the password for this store * @throws MalformedExportDataException if the exported data is invalid * @throws DIDStoreException if an error occurred when accessing the store * @throws IOException if an IO error occurred when reading the exported data */ public void importStore(String zipFile, String password, String storepass) throws MalformedExportDataException, DIDStoreException, IOException { checkArgument(zipFile != null && !zipFile.isEmpty(), "Invalid zip input file name"); checkArgument(password != null && !password.isEmpty(), "Invalid password"); checkArgument(storepass != null && !storepass.isEmpty(), "Invalid storepass"); importStore(new File(zipFile), password, storepass); } }