diff --git a/README.md b/README.md
index 623c4529..1cb0076c 100644
--- a/README.md
+++ b/README.md
@@ -70,7 +70,7 @@ java -cp "target/at_client-1.0-SNAPSHOT.jar:target/lib/*" org.atsign.client.cli.
3) Get
4) Delete
5) Register
-6) Onboard
+6) Activate
#### Note: Each of these classes requires a different set of arguments, make sure to read the help text and provide necessary arguments
** Text about the remaining functionalities coming soon **
diff --git a/at_client/pom.xml b/at_client/pom.xml
index 818dc167..fedf19bd 100644
--- a/at_client/pom.xml
+++ b/at_client/pom.xml
@@ -287,12 +287,19 @@
test
-
- org.hamcrest
- hamcrest-all
- 1.3
- test
-
+
+ org.hamcrest
+ hamcrest
+ 2.2
+ test
+
+
+
+ org.awaitility
+ awaitility
+ 4.2.0
+ test
+
diff --git a/at_client/src/main/java/org/atsign/client/api/AtClient.java b/at_client/src/main/java/org/atsign/client/api/AtClient.java
index 205b32cf..db629836 100644
--- a/at_client/src/main/java/org/atsign/client/api/AtClient.java
+++ b/at_client/src/main/java/org/atsign/client/api/AtClient.java
@@ -5,17 +5,15 @@
import org.atsign.client.api.impl.connections.DefaultAtConnectionFactory;
import org.atsign.client.api.impl.events.SimpleAtEventBus;
import org.atsign.client.api.impl.secondaries.RemoteSecondary;
-import org.atsign.client.util.KeysUtil;
import org.atsign.common.AtException;
import org.atsign.common.AtSign;
-import org.atsign.common.exceptions.AtClientConfigException;
import org.atsign.common.exceptions.AtSecondaryConnectException;
import org.atsign.common.exceptions.AtSecondaryNotFoundException;
import org.atsign.common.options.GetRequestOptions;
+import java.io.Closeable;
import java.io.IOException;
import java.util.List;
-import java.util.Map;
import java.util.concurrent.CompletableFuture;
import static org.atsign.common.Keys.*;
@@ -24,57 +22,70 @@
* The primary interface of the AtSign client library.
*/
@SuppressWarnings("unused")
-public interface AtClient extends Secondary, AtEvents.AtEventBus {
+public interface AtClient extends Secondary, AtEvents.AtEventBus, Closeable {
/**
* Standard AtClient factory - uses production @ root to look up the cloud secondary address for this atSign
- * @param atSign the atsign of this client
+ * @param atSign the {@link AtSign} of this client - e.g. @alice
+ * @param keys the {@link AtKeys} for this client
* @return An {@link AtClient}
* @throws AtException if something goes wrong with looking up or connecting to the remote secondary
*/
- static AtClient withRemoteSecondary(AtSign atSign) throws AtException {
- return withRemoteSecondary("root.atsign.org:64", atSign);
+ static AtClient withRemoteSecondary(AtSign atSign, AtKeys keys) throws AtException {
+ return withRemoteSecondary("root.atsign.org:64", atSign, keys);
}
/**
* Standard AtClient factory - uses production @ root to look up the cloud secondary address for this atSign
- * @param atSign the atsign of this client
+ * @param atSign the {@link AtSign} of this client - e.g. @alice
+ * @param keys the {@link AtKeys} for this client
* @param verbose set to true for chatty logs
* @return An {@link AtClient}
* @throws AtException if something goes wrong with looking up or connecting to the remote secondary
*/
- static AtClient withRemoteSecondary(AtSign atSign, boolean verbose) throws AtException {
- return withRemoteSecondary("root.atsign.org:64", atSign, verbose);
+ static AtClient withRemoteSecondary(AtSign atSign, AtKeys keys, boolean verbose) throws AtException {
+ return withRemoteSecondary("root.atsign.org:64", atSign, keys, verbose);
}
/**
* Factory to use when you wish to use a custom Secondary.AddressFinder
- * @param atSign the atSign of this client
+ * @param atSign the {@link AtSign} of this client - e.g. @alice
+ * @param keys the {@link AtKeys} for this client
* @param secondaryAddressFinder will be used to find the Secondary.Address of the atSign
* @return An {@link AtClient}
* @throws AtException if any other exception occurs while connecting to the remote (cloud) secondary
*/
- static AtClient withRemoteSecondary(AtSign atSign, Secondary.AddressFinder secondaryAddressFinder) throws AtException {
+ static AtClient withRemoteSecondary(AtSign atSign, AtKeys keys, Secondary.AddressFinder secondaryAddressFinder) throws AtException {
Secondary.Address remoteSecondaryAddress;
try {
remoteSecondaryAddress = secondaryAddressFinder.findSecondary(atSign);
} catch (IOException e) {
throw new AtSecondaryConnectException("Failed to find secondary, with IOException", e);
}
- return withRemoteSecondary(atSign, remoteSecondaryAddress, false);
+ return withRemoteSecondary(atSign, keys, remoteSecondaryAddress, false);
}
/**
* Factory - returns default AtClientImpl with a RemoteSecondary and a DefaultConnectionFactory
* @param rootUrl the address of the root server to use - e.g. root.atsign.org:64 for production at-signs
- * @param atSign the atSign of the client - e.g. @alice
+ * @param atSign the {@link AtSign} of this client - e.g. @alice
+ * @param keys the {@link AtKeys} for this client
* @return An {@link AtClient}
* @throws AtException if anything goes wrong during construction
*/
- static AtClient withRemoteSecondary(String rootUrl, AtSign atSign) throws AtException {
- return withRemoteSecondary(rootUrl, atSign, false);
+ static AtClient withRemoteSecondary(String rootUrl, AtSign atSign, AtKeys keys) throws AtException {
+ return withRemoteSecondary(rootUrl, atSign, keys, false);
}
- static AtClient withRemoteSecondary(String rootUrl, AtSign atSign, boolean verbose) throws AtException {
+ /**
+ * Factory - returns default AtClientImpl with a RemoteSecondary and a DefaultConnectionFactory
+ * @param rootUrl the address of the root server to use - e.g. root.atsign.org:64 for production at-signs
+ * @param atSign the {@link AtSign} of this client - e.g. @alice
+ * @param keys the {@link AtKeys} for this client
+ * @param verbose set to true for chatty logs
+ * @return An {@link AtClient}
+ * @throws AtException if anything goes wrong during construction
+ */
+ static AtClient withRemoteSecondary(String rootUrl, AtSign atSign, AtKeys keys, boolean verbose) throws AtException {
DefaultAtConnectionFactory connectionFactory = new DefaultAtConnectionFactory();
Secondary.Address secondaryAddress;
@@ -88,41 +99,36 @@ static AtClient withRemoteSecondary(String rootUrl, AtSign atSign, boolean verbo
throw new AtSecondaryNotFoundException("Failed to lookup remote secondary", e);
}
- return withRemoteSecondary(atSign, secondaryAddress, verbose);
+ return withRemoteSecondary(atSign, keys, secondaryAddress, verbose);
}
/**
* Factory to use when you wish to use a custom Secondary.AddressFinder
- * @param atSign the atSign of this client
+ * @param atSign the {@link AtSign} of this client - e.g. @alice
+ * @param keys the {@link AtKeys} for this client
* @param verbose set to true for chatty logs
* @return An {@link AtClient}
* @throws IOException if thrown by the address finder
* @throws AtException if any other exception occurs while connecting to the remote (cloud) secondary
*/
- static AtClient withRemoteSecondary(AtSign atSign, Secondary.AddressFinder secondaryAddressFinder, boolean verbose) throws IOException, AtException {
+ static AtClient withRemoteSecondary(AtSign atSign, AtKeys keys, Secondary.AddressFinder secondaryAddressFinder, boolean verbose) throws IOException, AtException {
Secondary.Address remoteSecondaryAddress = secondaryAddressFinder.findSecondary(atSign);
- return withRemoteSecondary(atSign, remoteSecondaryAddress, verbose);
+ return withRemoteSecondary(atSign, keys, remoteSecondaryAddress, verbose);
}
/**
* Factory to use when you already know the address of the remote (cloud) secondary
- * @param atSign the atSign of this client
+ * @param atSign the {@link AtSign} of this client - e.g. @alice
+ * @param keys the {@link AtKeys} for this client
* @param remoteSecondaryAddress the address of the remote secondary server
* @param verbose set to true for chatty logs
* @return An {@link AtClient}
* @throws AtException if any other exception occurs while connecting to the remote (cloud) secondary
*/
- static AtClient withRemoteSecondary(AtSign atSign, Secondary.Address remoteSecondaryAddress, boolean verbose) throws AtException {
+ static AtClient withRemoteSecondary(AtSign atSign, AtKeys keys, Secondary.Address remoteSecondaryAddress, boolean verbose) throws AtException {
DefaultAtConnectionFactory connectionFactory = new DefaultAtConnectionFactory();
AtEvents.AtEventBus eventBus = new SimpleAtEventBus();
- Map keys;
- try {
- keys = KeysUtil.loadKeys(atSign);
- } catch (Exception e) {
- throw new AtClientConfigException("Failed to load keys", e);
- }
-
RemoteSecondary secondary;
try {
secondary = new RemoteSecondary(eventBus, atSign, remoteSecondaryAddress, keys, connectionFactory, verbose);
@@ -135,20 +141,21 @@ static AtClient withRemoteSecondary(AtSign atSign, Secondary.Address remoteSecon
/**
* Factory to use when you already know the address of the remote (cloud) secondary
- * @param atSign the atSign of this client
* @param remoteSecondaryAddress the address of the remote secondary server
+ * @param atSign the {@link AtSign} of this client - e.g. @alice
+ * @param keys the {@link AtKeys} for this client
* @return An {@link AtClient}
* @throws AtException if any other exception occurs while connecting to the remote (cloud) secondary
*/
- static AtClient withRemoteSecondary(Secondary.Address remoteSecondaryAddress, AtSign atSign) throws AtException {
- return withRemoteSecondary(atSign, remoteSecondaryAddress, false);
+ static AtClient withRemoteSecondary(Secondary.Address remoteSecondaryAddress, AtSign atSign, AtKeys keys) throws AtException {
+ return withRemoteSecondary(atSign, keys, remoteSecondaryAddress, false);
}
AtSign getAtSign();
Secondary getSecondary();
- Map getEncryptionKeys();
+ AtKeys getEncryptionKeys();
CompletableFuture get(SharedKey sharedKey);
CompletableFuture getBinary(SharedKey sharedKey);
diff --git a/at_client/src/main/java/org/atsign/client/api/AtKeys.java b/at_client/src/main/java/org/atsign/client/api/AtKeys.java
new file mode 100644
index 00000000..1f982c84
--- /dev/null
+++ b/at_client/src/main/java/org/atsign/client/api/AtKeys.java
@@ -0,0 +1,189 @@
+package org.atsign.client.api;
+
+import org.atsign.client.util.EnrollmentId;
+
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Data class used to hold an {@link org.atsign.client.api.AtClient}s keys
+ */
+public class AtKeys {
+
+ /**
+ * unique id which is assigned by the at_server at enrollment time, this is associated with a specific
+ * application and device and therefore a specific the apkam key pair
+ */
+ private EnrollmentId enrollmentId;
+
+ /**
+ * Public Key used for Authentication Management, once this is stored in the at_server then an
+ * {@link org.atsign.client.api.AtClient} is able to authenticate using the corresponding private key.
+ */
+ private String apkamPublicKey;
+
+ /**
+ * Private Key used for Authentication Management, this is used to sign the challenge during
+ * authentication with the at_server.
+ */
+ private String apkamPrivateKey;
+
+ /**
+ * Encryption Key used during enrollment. This key is sent as part of the enrollment request (encrypted
+ * with the atsigns public encryption key). The process that approves the enrollment request uses this
+ * key to encrypt the {@link #selfEncryptKey} and {@link #encryptPrivateKey} in the response that it sends.
+ * The process which requested the enrollment can then decrypt and store those keys.
+ * This ensures that all {@link AtKeys} got and {@link org.atsign.common.AtSign} share the same
+ * {@link #selfEncryptKey} and {@link #encryptPrivateKey}
+ */
+ private String apkamSymmetricKey;
+
+ /**
+ * Encryption Key used to encrypt {@link org.atsign.common.Keys.SelfKey}s and the pkam and encryption key pairs
+ * when they are externalised as JSON
+ */
+ private String selfEncryptKey;
+
+ /**
+ * This is used to encrypt the symmetric keys that are used to encrypt {@link org.atsign.common.Keys.SharedKey}s
+ * where the shared with {@link org.atsign.common.AtSign} is this {@link org.atsign.common.AtSign}
+ */
+
+ private String encryptPublicKey;
+
+ /**
+ * This is used to decrypt the symmetric keys that are used to encrypt {@link org.atsign.common.Keys.SharedKey}s
+ * where the shared with {@link org.atsign.common.AtSign} is this {@link org.atsign.common.AtSign}
+ */
+ private String encryptPrivateKey;
+
+ /**
+ * Transient cache of other keys
+ */
+ private Map cache = new ConcurrentHashMap<>();
+
+ public boolean hasEnrollmentId() {
+ return enrollmentId != null;
+ }
+
+ public EnrollmentId getEnrollmentId() {
+ return enrollmentId;
+ }
+
+ public AtKeys setEnrollmentId(EnrollmentId enrollmentId) {
+ this.enrollmentId = enrollmentId;
+ return this;
+ }
+
+ public String getSelfEncryptKey() {
+ return selfEncryptKey;
+ }
+
+ public AtKeys setSelfEncryptKey(String key) {
+ this.selfEncryptKey = key;
+ return this;
+ }
+
+ public String getApkamPublicKey() {
+ return apkamPublicKey;
+ }
+
+ public AtKeys setApkamPublicKey(String key) {
+ this.apkamPublicKey = key;
+ return this;
+ }
+
+ public AtKeys setApkamPublicKey(PublicKey key) {
+ return setApkamPublicKey(createStringBase64(key));
+ }
+
+ public String getApkamPrivateKey() {
+ return apkamPrivateKey;
+ }
+
+ public AtKeys setApkamPrivateKey(String key) {
+ this.apkamPrivateKey = key;
+ return this;
+ }
+
+ public AtKeys setApkamPrivateKey(PrivateKey key) {
+ return setApkamPrivateKey(createStringBase64(key));
+ }
+
+ public AtKeys setApkamKeyPair(KeyPair keyPair) {
+ setApkamPublicKey(keyPair.getPublic());
+ setApkamPrivateKey(keyPair.getPrivate());
+ return this;
+ }
+
+ public boolean hasPkamKeys() {
+ return this.apkamPublicKey != null && this.apkamPrivateKey != null;
+ }
+
+ public AtKeys setEncryptKeyPair(KeyPair keyPair) {
+ setEncryptPublicKey(keyPair.getPublic());
+ setEncryptPrivateKey(keyPair.getPrivate());
+ return this;
+ }
+
+ public String getEncryptPublicKey() {
+ return encryptPublicKey;
+ }
+
+ public AtKeys setEncryptPublicKey(String key) {
+ this.encryptPublicKey = key;
+ return this;
+ }
+
+ public AtKeys setEncryptPublicKey(PublicKey key) {
+ return setEncryptPublicKey(createStringBase64(key));
+ }
+
+ public String getEncryptPrivateKey() {
+ return encryptPrivateKey;
+ }
+
+ public AtKeys setEncryptPrivateKey(String key) {
+ this.encryptPrivateKey = key;
+ return this;
+ }
+
+ public AtKeys setEncryptPrivateKey(PrivateKey key) {
+ return setEncryptPrivateKey(createStringBase64(key));
+ }
+
+ public String getApkamSymmetricKey() {
+ return apkamSymmetricKey;
+ }
+
+ public AtKeys setApkamSymmetricKey(String key) {
+ this.apkamSymmetricKey = key;
+ return this;
+ }
+
+ public String get(String key) {
+ return cache.get(key);
+ }
+
+ public void put(String key, String value) {
+ cache.put(key, value);
+ }
+
+ public Map getCache() {
+ return Collections.unmodifiableMap(cache);
+ }
+
+ private static String createStringBase64(PublicKey key) {
+ return Base64.getEncoder().encodeToString(key.getEncoded());
+ }
+
+ private static String createStringBase64(PrivateKey key) {
+ return Base64.getEncoder().encodeToString(key.getEncoded());
+ }
+
+}
diff --git a/at_client/src/main/java/org/atsign/client/api/Secondary.java b/at_client/src/main/java/org/atsign/client/api/Secondary.java
index 29198a79..94e48a94 100644
--- a/at_client/src/main/java/org/atsign/client/api/Secondary.java
+++ b/at_client/src/main/java/org/atsign/client/api/Secondary.java
@@ -4,6 +4,7 @@
import org.atsign.common.exceptions.*;
import org.atsign.common.AtSign;
+import java.io.Closeable;
import java.io.IOException;
/**
@@ -21,7 +22,7 @@
* interface is effectively the same as when interacting with a cloud secondary via openssl
* from command line.
*/
-public interface Secondary extends AtEvents.AtEventListener {
+public interface Secondary extends AtEvents.AtEventListener, Closeable {
/**
* @param command in @ protocol format
* @param throwExceptionOnErrorResponse sometimes we want to inspect an error response,
diff --git a/at_client/src/main/java/org/atsign/client/api/impl/clients/AtClientImpl.java b/at_client/src/main/java/org/atsign/client/api/impl/clients/AtClientImpl.java
index f30b643c..169daeab 100644
--- a/at_client/src/main/java/org/atsign/client/api/impl/clients/AtClientImpl.java
+++ b/at_client/src/main/java/org/atsign/client/api/impl/clients/AtClientImpl.java
@@ -7,8 +7,8 @@
import org.atsign.client.api.AtEvents.AtEventListener;
import org.atsign.client.api.AtEvents.AtEventType;
import org.atsign.client.api.Secondary;
+import org.atsign.client.api.AtKeys;
import org.atsign.client.util.EncryptionUtil;
-import org.atsign.client.util.KeysUtil;
import org.atsign.common.*;
import org.atsign.common.Keys.AtKey;
import org.atsign.common.Keys.PublicKey;
@@ -33,6 +33,7 @@
import java.util.concurrent.CompletionException;
import static org.atsign.client.api.AtEvents.AtEventType.decryptedUpdateNotification;
+import static org.atsign.client.util.Preconditions.checkNotNull;
/**
* @see org.atsign.client.api.AtClient
@@ -46,18 +47,18 @@ public class AtClientImpl implements AtClient {
private final AtSign atSign;
@Override public AtSign getAtSign() {return atSign;}
- private final Map keys;
- @Override public Map getEncryptionKeys() {return keys;}
+ private final AtKeys keys;
+ @Override public AtKeys getEncryptionKeys() {return keys;}
private final Secondary secondary;
@Override public Secondary getSecondary() {return secondary;}
private final AtEventBus eventBus;
- public AtClientImpl(AtEventBus eventBus, AtSign atSign, Map keys, Secondary secondary) {
+ public AtClientImpl(AtEventBus eventBus, AtSign atSign, AtKeys keys, Secondary secondary) {
this.eventBus = eventBus;
this.atSign = atSign;
this.keys = keys;
this.secondary = secondary;
-
+ checkNotNull(keys.getEncryptPrivateKey(), "AtKeys have not been fully enrolled");
eventBus.addEventListener(this, EnumSet.allOf(AtEventType.class));
}
@@ -92,7 +93,7 @@ public synchronized void handleEvent(AtEventType eventType, Map
String sharedSharedKeyEncryptedValue = (String) eventData.get("value");
// decrypt it with our encryption private key
try {
- String sharedKeyDecryptedValue = EncryptionUtil.rsaDecryptFromBase64(sharedSharedKeyEncryptedValue, keys.get(KeysUtil.encryptionPrivateKeyName));
+ String sharedKeyDecryptedValue = EncryptionUtil.rsaDecryptFromBase64(sharedSharedKeyEncryptedValue, keys.getEncryptPrivateKey());
keys.put(sharedSharedKeyName, sharedKeyDecryptedValue);
} catch (Exception e) {
System.err.println(OffsetDateTime.now() + ": caught exception " + e + " while decrypting received shared key " + sharedSharedKeyName);
@@ -344,7 +345,13 @@ public Response executeCommand(String command, boolean throwExceptionOnErrorResp
return secondary.executeCommand(command, throwExceptionOnErrorResponse);
}
-// ============================================================================================================================================
+ @Override
+ public void close() throws IOException {
+ stopMonitor();
+ secondary.close();
+ }
+
+ // ============================================================================================================================================
// ============================================================================================================================================
// ============================================================================================================================================
@@ -449,7 +456,7 @@ private String _get(SelfKey key) throws AtException {
// 3. decrypt the value
String decryptedValue;
String encryptedValue = fetched.data;
- String selfEncryptionKey = keys.get(KeysUtil.selfEncryptionKeyName);
+ String selfEncryptionKey = keys.getSelfEncryptKey();
try {
decryptedValue = EncryptionUtil.aesDecryptFromBase64(encryptedValue, selfEncryptionKey);
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | NoSuchProviderException e) {
@@ -469,7 +476,7 @@ private String _put(SelfKey selfKey, String value) throws AtException {
// 2. encrypt data with self encryption key
String cipherText;
try {
- cipherText = EncryptionUtil.aesEncryptToBase64(value, keys.get(KeysUtil.selfEncryptionKeyName));
+ cipherText = EncryptionUtil.aesEncryptToBase64(value, keys.getSelfEncryptKey());
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | NoSuchProviderException e) {
throw new AtEncryptionException("Failed to encrypt value with self encryption key", e);
}
@@ -583,8 +590,10 @@ private List _getAtKeys(String regex, boolean fetchMetadata) throws AtExc
} catch (IOException e) {
throw new AtSecondaryConnectException("Failed to execute " + scanCommand, e);
}
- ResponseTransformers.ScanResponseTransformer scanResponseTransformer = new ResponseTransformers.ScanResponseTransformer();
+ ResponseTransformers.ScanResponseTransformer scanResponseTransformer
+ = new ResponseTransformers.ScanResponseTransformer(AtClientImpl::isNotManagementKey);
List rawArray = scanResponseTransformer.transform(scanRawResponse);
+
List atKeys = new ArrayList<>();
for(String atKeyRaw : rawArray) { // eg atKeyRaw == @bob:phone@alice
AtKey atKey = Keys.fromString(atKeyRaw);
@@ -656,7 +665,7 @@ private String getEncryptionKeySharedByMe(SharedKey key) throws AtException {
// When we stored it, we encrypted it with our encryption public key; so we need to decrypt it now with our encryption private key
try {
- return EncryptionUtil.rsaDecryptFromBase64(rawResponse.getRawDataResponse(), keys.get(KeysUtil.encryptionPrivateKeyName));
+ return EncryptionUtil.rsaDecryptFromBase64(rawResponse.getRawDataResponse(), keys.getEncryptPrivateKey());
} catch (Exception e) {
throw new AtDecryptionException("Failed to decrypt " + toLookup, e);
}
@@ -683,7 +692,7 @@ private String getEncryptionKeySharedByOther(SharedKey sharedKey) throws AtExcep
String sharedSharedKeyDecryptedValue;
try {
- sharedSharedKeyDecryptedValue = EncryptionUtil.rsaDecryptFromBase64(rawResponse.getRawDataResponse(), keys.get(KeysUtil.encryptionPrivateKeyName));
+ sharedSharedKeyDecryptedValue = EncryptionUtil.rsaDecryptFromBase64(rawResponse.getRawDataResponse(), keys.getEncryptPrivateKey());
} catch (Exception e) {
throw new AtDecryptionException("Failed to decrypt the shared_key with our encryption private key", e);
}
@@ -715,7 +724,7 @@ private String createSharedEncryptionKey(SharedKey sharedKey) throws AtException
what = "encrypt new shared key with our public key";
// Encrypt key with our publickey and save it shared_key.bob@alice
- String encryptedForUs = EncryptionUtil.rsaEncryptToBase64(aesKey, keys.get(KeysUtil.encryptionPublicKeyName));
+ String encryptedForUs = EncryptionUtil.rsaEncryptToBase64(aesKey, keys.getEncryptPublicKey());
what = "save encrypted shared key for us";
secondary.executeCommand("update:" + "shared_key." + sharedKey.sharedWith.withoutPrefix() + sharedKey.sharedBy
@@ -757,10 +766,14 @@ private String getPublicEncryptionKey(AtSign sharedWith) throws AtException {
private String generateSignature(String value) throws AtException {
String signature;
try {
- signature = EncryptionUtil.signSHA256RSA(value, keys.get(KeysUtil.encryptionPrivateKeyName));
+ signature = EncryptionUtil.signSHA256RSA(value, keys.getEncryptPrivateKey());
} catch (Exception e) {
throw new AtEncryptionException("Failed to sign value: " + value, e);
}
return signature;
}
+
+ private static boolean isNotManagementKey(String s) {
+ return !s.matches(".+\\.__manage@.+");
+ }
}
diff --git a/at_client/src/main/java/org/atsign/client/api/impl/connections/AtSecondaryConnection.java b/at_client/src/main/java/org/atsign/client/api/impl/connections/AtSecondaryConnection.java
index bef7be2d..a649cb8c 100644
--- a/at_client/src/main/java/org/atsign/client/api/impl/connections/AtSecondaryConnection.java
+++ b/at_client/src/main/java/org/atsign/client/api/impl/connections/AtSecondaryConnection.java
@@ -5,13 +5,14 @@
import org.atsign.client.api.Secondary;
import org.atsign.common.AtSign;
+import java.io.Closeable;
import java.io.IOException;
/**
* A connection which understands how to talk with the secondary server.
* @see org.atsign.client.api.AtConnection
*/
-public class AtSecondaryConnection extends AtConnectionBase {
+public class AtSecondaryConnection extends AtConnectionBase implements Closeable {
private final AtSign atSign;
public AtSign getAtSign() {return atSign;}
@@ -51,4 +52,9 @@ protected String parseRawResponse(String rawResponse) throws IOException {
throw new IOException("Invalid response from server: " + rawResponse);
}
}
+
+ @Override
+ public void close() throws IOException {
+ disconnect();
+ }
}
diff --git a/at_client/src/main/java/org/atsign/client/api/impl/secondaries/RemoteSecondary.java b/at_client/src/main/java/org/atsign/client/api/impl/secondaries/RemoteSecondary.java
index 53bfbd51..e6af9ff9 100644
--- a/at_client/src/main/java/org/atsign/client/api/impl/secondaries/RemoteSecondary.java
+++ b/at_client/src/main/java/org/atsign/client/api/impl/secondaries/RemoteSecondary.java
@@ -4,6 +4,7 @@
import org.atsign.client.api.Secondary;
import org.atsign.client.api.impl.connections.AtMonitorConnection;
import org.atsign.client.api.impl.connections.AtSecondaryConnection;
+import org.atsign.client.api.AtKeys;
import org.atsign.client.util.AuthUtil;
import org.atsign.common.AtException;
import org.atsign.common.AtSign;
@@ -33,7 +34,7 @@ public class RemoteSecondary implements Secondary {
@SuppressWarnings("unused")
public AtSecondaryConnection getConnection() {return connection;}
- private AtMonitorConnection monitorConnection;
+ private volatile AtMonitorConnection monitorConnection;
@SuppressWarnings("unused")
public AtMonitorConnection getMonitorConnection() {return monitorConnection;}
@@ -58,11 +59,11 @@ public void setVerbose(boolean b) {
@SuppressWarnings("unused")
public RemoteSecondary(AtEventBus eventBus, AtSign atSign, Secondary.Address secondaryAddress,
- Map keys, AtConnectionFactory connectionFactory) throws IOException, AtException {
+ AtKeys keys, AtConnectionFactory connectionFactory) throws IOException, AtException {
this(eventBus, atSign, secondaryAddress, keys, connectionFactory, false);
}
public RemoteSecondary(AtEventBus eventBus, AtSign atSign, Secondary.Address secondaryAddress,
- Map keys, AtConnectionFactory connectionFactory,
+ AtKeys keys, AtConnectionFactory connectionFactory,
boolean verbose) throws IOException, AtException {
this.eventBus = eventBus;
this.atSign = atSign;
@@ -116,6 +117,12 @@ public synchronized void handleEvent(AtEventType eventType, Map
// if (eventType == )
}
+ @Override
+ public void close() throws IOException {
+ ensureMonitorNotRunning();
+ connection.close();
+ }
+
private void ensureMonitorRunning() {
String what = "";
try {
diff --git a/at_client/src/main/java/org/atsign/client/cli/AbstractCli.java b/at_client/src/main/java/org/atsign/client/cli/AbstractCli.java
new file mode 100644
index 00000000..15af3176
--- /dev/null
+++ b/at_client/src/main/java/org/atsign/client/cli/AbstractCli.java
@@ -0,0 +1,267 @@
+package org.atsign.client.cli;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.atsign.client.api.impl.connections.AtRootConnection;
+import org.atsign.client.api.impl.connections.AtSecondaryConnection;
+import org.atsign.client.api.impl.events.SimpleAtEventBus;
+import org.atsign.client.api.AtKeys;
+import org.atsign.client.util.AuthUtil;
+import org.atsign.client.util.KeysUtil;
+import org.atsign.client.util.TypedString;
+import org.atsign.common.AtException;
+import org.atsign.common.AtSign;
+import org.atsign.common.exceptions.AtSecondaryNotFoundException;
+import picocli.CommandLine;
+import picocli.CommandLine.ITypeConverter;
+import picocli.CommandLine.Option;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static org.atsign.client.util.Preconditions.checkNotNull;
+
+public abstract class AbstractCli> {
+
+ protected static final Pattern DATA_JSON_NON_EMPTY_MAP = Pattern.compile("data:(\\{.+})");
+ protected static final Pattern DATA_JSON_MAP = Pattern.compile("data:(\\{.*})");
+ protected static final Pattern DATA_JSON_NO_EMPTY_LIST = Pattern.compile("data:(\\[.+])");
+ protected static final Pattern DATA_INT = Pattern.compile("data:\\d+");
+ public static final Pattern DATA_NON_WHITESPACE = Pattern.compile("data:(\\S+)");
+
+ protected String rootUrl = "root.atsign.org";
+ protected AtSign atSign;
+ protected File keysFile;
+ protected int connectionRetries = 1;
+ private boolean verbose = false;
+
+ protected abstract T self();
+
+ public T setVerbose(boolean isVerbose) {
+ this.verbose = isVerbose;
+ return self();
+ }
+
+ public T setVerbose() {
+ return setVerbose(true);
+ }
+
+ @Option(names = {"-r", "--root"}, paramLabel = "HOST:PORT", description = "atDirectory (aka root) server domain (e.g., root.atsign.org)")
+ public T setRootUrl(String rootUrl) {
+ this.rootUrl = rootUrl;
+ return self();
+ }
+
+ @Option(names = {"-a", "--atsign"}, description = "the atsign e.g. @colin", paramLabel = "ATSIGN", converter = AtSignConverter.class)
+ public T setAtSign(AtSign atSign) {
+ this.atSign = atSign;
+ return self();
+ }
+
+ @Option(names = {"-k", "--keys"}, paramLabel = "PATH", description = "path to atKeys file to use / create")
+ public T setKeysFile(String path) {
+ this.keysFile = new File(path);
+ return self();
+ }
+
+ protected static File checkNotExists(File f) {
+ if (f.exists()) {
+ throw new IllegalArgumentException(f.getPath() + " would be overwritten");
+ }
+ return f;
+ }
+
+ protected static File checkExists(File f) {
+ if (!f.exists()) {
+ throw new IllegalArgumentException(f.getPath() + " not found");
+ }
+ return f;
+ }
+
+ protected static File getAtKeysFile(File keysFile, AtSign atSign) {
+ return keysFile != null ? keysFile : KeysUtil.getKeysFile(atSign);
+ }
+
+ protected static void checkAtServerMatchesAtSign(AtSecondaryConnection connection, AtSign atSign) throws IOException {
+ if (!matchDataJsonList(connection.executeCommand("scan")).contains("signing_publickey" + atSign)) {
+ // TODO: understand precisely what this means (observed in Dart SDK)
+ throw new IllegalStateException("TBC");
+ }
+ }
+
+ protected static void deleteKey(AtSecondaryConnection connection, String key) {
+ try {
+ match(connection.executeCommand("delete:" + key), DATA_INT);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ protected static void authenticateWithApkam(AtSecondaryConnection connection, AtSign atSign, AtKeys keys) throws AtException, IOException {
+ new AuthUtil().authenticateWithPkam(connection, atSign, keys);
+ }
+
+ protected AtSecondaryConnection createAtSecondaryConnection(AtSign atSign,
+ String rootUrl,
+ int retries) throws Exception {
+ checkNotNull(atSign, "atsign not set");
+ checkNotNull(rootUrl, "root server endpoint not set");
+
+ String secondaryUrl = resolveSecondaryUrl(atSign, rootUrl, retries);
+ AtSecondaryConnection conn = new AtSecondaryConnection(new SimpleAtEventBus(), atSign, secondaryUrl, null, false, verbose);
+ int retriesRemaining = retries;
+ Exception ex;
+ do {
+ try {
+ conn.connect();
+ return conn;
+ } catch (Exception e) {
+ ex = e;
+ Thread.sleep(2000);
+ }
+ } while (retriesRemaining-- > 0);
+ throw ex;
+ }
+
+ protected static String resolveSecondaryUrl(AtSign atSign, String rootUrl, int retries) throws Exception {
+ int retriesRemaining = retries;
+ Exception ex;
+ do {
+ try {
+ return new AtRootConnection(rootUrl).lookupAtSign(atSign);
+ } catch (AtSecondaryNotFoundException e) {
+ ex = e;
+ Thread.sleep(1000);
+ }
+ } while (retriesRemaining-- > 0);
+ throw ex;
+ }
+
+ protected static String encodeKeyValuesAsJson(Object... nameValuePairs) throws Exception {
+ return encodeAsJson(toObjectMap(nameValuePairs));
+ }
+
+ protected static Map toObjectMap(Object... nameValuePairs) {
+ if ((nameValuePairs.length % 2) != 0) {
+ throw new IllegalArgumentException("odd number of parameters");
+ }
+ Map map = new HashMap<>();
+ for (int i = 0; i < nameValuePairs.length; i++) {
+ String key = nameValuePairs[i].toString();
+ Object value = nameValuePairs[++i];
+ if (value instanceof TypedString) {
+ map.put(key, value.toString());
+ } else {
+ map.put(key, value);
+ }
+ }
+ return map;
+ }
+
+ protected static String encodeAsJson(Map map) throws JsonProcessingException {
+ ObjectMapper objectMapper = new ObjectMapper();
+ return objectMapper.writeValueAsString(map);
+ }
+
+ protected static Map decodeJsonMapOfStrings(String json) {
+ try {
+ return new ObjectMapper().readValue(json, new TypeReference