diff --git a/at_client/src/main/java/org/atsign/client/api/AtCommandExecutorContext.java b/at_client/src/main/java/org/atsign/client/api/AtCommandExecutorContext.java index 3db7cfa9..987d28d5 100644 --- a/at_client/src/main/java/org/atsign/client/api/AtCommandExecutorContext.java +++ b/at_client/src/main/java/org/atsign/client/api/AtCommandExecutorContext.java @@ -2,42 +2,87 @@ import lombok.*; +import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; +import static java.util.Collections.emptyMap; +import static java.util.Collections.unmodifiableMap; +import static org.atsign.client.impl.common.Preconditions.checkNotNull; + /** * The identity a connection authenticates as (its {@code atSign}, {@code keys} and {@code config}) * together with the single-use challenge from the {@code from:} that is issued as the first command * once the connection is ready. * *

- * The context is created by the builder (see - * {@code AtCommandExecutors#createCommandExecutor}) and closed over by the {@code onReady} - * consumers it wires, so the {@code from:} sender can retain the challenge and the authentication - * that follows on the same connection can reuse it rather than issuing a second {@code from:}. - * The command executor itself is pure transport and knows nothing about this context. + * There is one context per connection. {@code AtClients#createAtClient} creates it and passes it to + * both the executor and the client; {@code AtCommandExecutors#createCommandExecutor} creates one + * itself when no caller supplies one. The {@code onReady} consumers hold a reference to it, so the + * {@code from:} sender can retain the challenge and the authentication that follows on the same + * connection can reuse it instead of issuing a second {@code from:}. The command executor itself + * only sends and receives, and never sees the context. * *

* The identity fields are fixed for the life of the context. The challenge is per-connection state: * {@link #setChallenge(String) retained} when the initial {@code from:} completes and * {@link #consumeChallenge() consumed} at most once (the server's {@code from:} challenge is - * single-use). On reconnect the ready sequence re-runs, so a fresh challenge overwrites any - * previous - * one before it is consumed. + * single-use). On reconnect the ready sequence re-runs, so a fresh challenge replaces any + * previous one before it is consumed. */ @Value public class AtCommandExecutorContext { + /** + * The atSign the connection authenticates as. Never {@code null}: a connection is built either from + * a context or from an atSign, and both establish an identity. + */ AtSign atSign; + /** + * The keys the connection authenticates with, or {@code null} for a connection that is not + * PKAM-authenticated — one that only issues {@code from:}, or one authenticating with CRAM before + * any keys exist. + */ AtKeys keys; + /** + * The client config sent with {@code from:}. Never {@code null} and never modifiable; empty + * when the connection has none. + */ Map config; + /** + * Holds the {@code from:} challenge for this connection. + */ @Getter(AccessLevel.NONE) @EqualsAndHashCode.Exclude @ToString.Exclude - AtomicReference challenge = new AtomicReference<>(); + AtomicReference challenge; + + /** + * A context with no client config, for a connection whose {@code from:} carries no + * {@code clientConfig} segment. + * + * @param atSign the atSign the connection authenticates as + * @param keys the keys the connection authenticates with, or null if it is not PKAM-authenticated + */ + public AtCommandExecutorContext(AtSign atSign, AtKeys keys) { + this(atSign, keys, null); + } + + /** + * @param atSign the atSign the connection authenticates as + * @param keys the keys the connection authenticates with, or null if it is not PKAM-authenticated + * @param config the client config to send with {@code from:}. Copied, so a later change to the + * caller's map cannot alter what this connection sends; null is stored as an empty map + */ + public AtCommandExecutorContext(AtSign atSign, AtKeys keys, Map config) { + this.atSign = checkNotNull(atSign, "atSign not set"); + this.keys = keys; + this.config = config != null ? unmodifiableMap(new LinkedHashMap<>(config)) : emptyMap(); + this.challenge = new AtomicReference<>(); + } /** * Retains the challenge returned by the initial {@code from:}, so the authentication that follows diff --git a/at_client/src/main/java/org/atsign/client/impl/AtClientImpl.java b/at_client/src/main/java/org/atsign/client/impl/AtClientImpl.java index 7968dc88..87beaf29 100644 --- a/at_client/src/main/java/org/atsign/client/impl/AtClientImpl.java +++ b/at_client/src/main/java/org/atsign/client/impl/AtClientImpl.java @@ -35,8 +35,7 @@ * *

  * AtClientImplBuilder builder = AtClientImpl.builder()
- *     .atSign(...)
- *     .keys(...)
+ *     .context(...)
  *     .executor(...)
  *     .eventBus(...);
  *
@@ -50,10 +49,8 @@
 @Slf4j
 public class AtClientImpl implements AtClient {
 
-  private final AtSign atSign;
-  private final AtKeys keys;
-  private MonitorOptions monitorOptions;
-  private final Map config;
+  private final AtCommandExecutorContext context;
+  private final MonitorOptions monitorOptions;
   private final AtCommandExecutor executor;
   private final AtEventBus eventBus;
   private final AtomicBoolean isMonitoring = new AtomicBoolean();
@@ -61,7 +58,7 @@ public class AtClientImpl implements AtClient {
 
   @Override
   public AtSign getAtSign() {
-    return atSign;
+    return context.getAtSign();
   }
 
   @Override
@@ -70,23 +67,20 @@ public AtCommandExecutor getCommandExecutor() {
   }
 
   @Builder
-  public AtClientImpl(AtSign atSign,
-                      AtKeys keys,
+  public AtClientImpl(AtCommandExecutorContext context,
                       boolean withMonitoring,
                       MonitorOptions monitorOptions,
-                      Map config,
                       AtCommandExecutor executor,
                       AtEventBus eventBus) {
-    this.atSign = checkNotNull(atSign, "atSign not set");
-    this.keys = checkNotNull(keys, "keys not set");
+    this.context = checkNotNull(context, "context not set");
+    checkNotNull(context.getKeys(), "keys not set");
+    checkNotNull(context.getKeys().getEncryptPrivateKey(), "keys have not been fully enrolled");
     this.monitorOptions = monitorOptions != null ? monitorOptions : MonitorOptions.builder().build();
-    this.config = config;
     this.executor = checkNotNull(executor, "executor not set");
     this.eventBus = checkNotNull(eventBus, "eventBus not set");
     this.eventBus.addEventListener(this::handleEvent, EnumSet.allOf(AtEventType.class));
     this.isMonitoring.set(withMonitoring);
-    this.eventBusBridge = new Notifications.EventBusBridge(eventBus, atSign, this.monitorOptions);
-    checkNotNull(keys.getEncryptPrivateKey(), "keys have not been fully enrolled");
+    this.eventBusBridge = new Notifications.EventBusBridge(eventBus, context.getAtSign(), this.monitorOptions);
   }
 
   /**
@@ -96,13 +90,16 @@ public AtClientImpl(AtSign atSign,
    * 
    *
    * AtClientImpl.builder()
-   *   .atSign(...)  // the AtSign that this client will authenticate as
-   *   .keys(...)    // the AtKeys that this client will use
+   *   .context(...) // the connection context: atSign, keys and client config
    *   .executor()   // the AtCommandExecutor this client will use
    *   .eventBus()   // the AtEventBus this client will publish to
    *   .build();
    * }
    * 
+ * + * NOTE the context must be the same instance the {@link AtCommandExecutor} was built with + * (see {@link AtClients#createAtClient}), so that the {@code from:} challenge and the client config + * are shared by the connection's authentication and by every command the client issues. */ public static class AtClientImplBuilder { // required for javadoc @@ -116,13 +113,13 @@ public void close() throws Exception { @Override public void startMonitor() { isMonitoring.compareAndSet(false, true); - executor.onReady(Notifications.monitor(atSign, monitorOptions, keys, config, eventBusBridge)); + executor.onReady(Notifications.monitor(context, monitorOptions, eventBusBridge)); } @Override public void stopMonitor() { isMonitoring.compareAndSet(true, false); - executor.onReady(AuthenticationCommands.pkamAuthenticator(atSign, keys, config)); + executor.onReady(AuthenticationCommands.pkamAuthenticator(context)); } @Override @@ -147,17 +144,17 @@ public int publishEvent(AtEventType eventType, Map eventData) { @Override public String get(SharedKey sharedKey) throws AtException { - return SharedKeyCommands.get(executor, atSign, keys, sharedKey); + return SharedKeyCommands.get(executor, context, sharedKey); } @Override public byte[] getBinary(SharedKey sharedKey) throws AtException { - return Base2e15Utils.decode(SharedKeyCommands.get(executor, atSign, keys, sharedKey, true)); + return Base2e15Utils.decode(SharedKeyCommands.get(executor, context, sharedKey, true)); } @Override public void put(SharedKey sharedKey, String value) throws AtException { - SharedKeyCommands.put(executor, atSign, keys, sharedKey, value); + SharedKeyCommands.put(executor, context, sharedKey, value); } @Override @@ -167,17 +164,17 @@ public void delete(SharedKey sharedKey) throws AtException { @Override public String get(SelfKey selfKey) throws AtException { - return SelfKeyCommands.get(executor, atSign, keys, selfKey); + return SelfKeyCommands.get(executor, context, selfKey); } @Override public byte[] getBinary(SelfKey selfKey) throws AtException { - return Base2e15Utils.decode(SelfKeyCommands.get(executor, atSign, keys, selfKey, true)); + return Base2e15Utils.decode(SelfKeyCommands.get(executor, context, selfKey, true)); } @Override public void put(SelfKey selfKey, String value) throws AtException { - SelfKeyCommands.put(executor, atSign, keys, selfKey, value); + SelfKeyCommands.put(executor, context, selfKey, value); } @Override @@ -192,7 +189,7 @@ public String get(PublicKey publicKey) throws AtException { @Override public String get(PublicKey publicKey, GetRequestOptions options) throws AtException { - return PublicKeyCommands.get(executor, atSign, publicKey, options); + return PublicKeyCommands.get(executor, context, publicKey, options); } @Override @@ -202,12 +199,12 @@ public byte[] getBinary(PublicKey publicKey) throws AtException { @Override public byte[] getBinary(PublicKey publicKey, GetRequestOptions options) throws AtException { - return Base2e15Utils.decode(PublicKeyCommands.get(executor, atSign, publicKey, true, options)); + return Base2e15Utils.decode(PublicKeyCommands.get(executor, context, publicKey, true, options)); } @Override public void put(PublicKey publicKey, String value) throws AtException { - PublicKeyCommands.put(executor, atSign, keys, publicKey, value); + PublicKeyCommands.put(executor, context, publicKey, value); } @Override @@ -262,6 +259,7 @@ private void onSharedKeyNotification(Map eventData) throws AtDec // If we also got a value, we can decrypt it and add it to our keys map // Note: a value isn't supplied when the ttr on the shared key was set to 0 if (eventData.get("value") != null) { + AtKeys keys = context.getKeys(); String keyName = (String) eventData.get("key"); String value = (String) eventData.get("value"); String decrypted = rsaDecryptFromBase64(value, keys.getEncryptPrivateKey()); @@ -272,6 +270,7 @@ private void onSharedKeyNotification(Map eventData) throws AtDec private void onUpdateNotification(Map eventData) throws AtException { // Let's see if we can decrypt it on the fly if (eventData.get("value") != null) { + AtKeys keys = context.getKeys(); String encryptedValue = (String) eventData.get("value"); Map metadata = (Map) eventData.get("metadata"); String ivNonce = (String) metadata.get("ivNonce"); @@ -282,7 +281,7 @@ private void onUpdateNotification(Map eventData) throws AtExcept } else { String key = (String) eventData.get("key"); SharedKey sk = org.atsign.client.api.Keys.sharedKeyBuilder().rawKey(key).build(); - encryptKeySharedByOther = SharedKeyCommands.lookupEncryptKeySharedByOther(executor, keys, sk); + encryptKeySharedByOther = SharedKeyCommands.lookupEncryptKeySharedByOther(executor, context, sk); } String decryptedValue = aesDecryptFromBase64(encryptedValue, encryptKeySharedByOther, ivNonce); HashMap newEventData = new HashMap<>(eventData); @@ -292,8 +291,8 @@ private void onUpdateNotification(Map eventData) throws AtExcept } /** - * A runnable command which returns a String value but can throw {@link AtException}s or execution - * exceptions + * A runnable command returning a String; may throw {@link AtException}s or + * execution exceptions. */ public interface AtCommandThatReturnsString { String run() throws AtException, ExecutionException, InterruptedException; @@ -310,9 +309,8 @@ public static CompletableFuture wrapAsync(AtCommandThatReturnsString com } /** - * A runnable command which returns a byte array value but can throw {@link AtException}s or - * execution - * exceptions + * A runnable command returning a byte array; may throw {@link AtException}s or + * execution exceptions. */ public interface AtCommandThatReturnsByteArray { byte[] run() throws AtException, ExecutionException, InterruptedException; @@ -329,8 +327,8 @@ public static CompletableFuture wrapAsync(AtCommandThatReturnsByteArray } /** - * A runnable command which does NOT return a value but can throw {@link AtException}s or execution - * exceptions + * A runnable command returning no value; may throw {@link AtException}s or + * execution exceptions. */ public interface AtCommandThatReturnsVoid { void run() throws AtException, ExecutionException, InterruptedException; diff --git a/at_client/src/main/java/org/atsign/client/impl/AtClients.java b/at_client/src/main/java/org/atsign/client/impl/AtClients.java index 203c2b39..5b61359d 100644 --- a/at_client/src/main/java/org/atsign/client/impl/AtClients.java +++ b/at_client/src/main/java/org/atsign/client/impl/AtClients.java @@ -8,6 +8,7 @@ import org.atsign.client.api.AtClient; import org.atsign.client.api.AtCommandExecutor; +import org.atsign.client.api.AtCommandExecutorContext; import org.atsign.client.api.AtKeys; import org.atsign.client.api.AtSign; import org.atsign.client.impl.commands.MonitorOptions; @@ -63,18 +64,18 @@ public static AtClient createAtClient(String url, monitorOptions = MonitorOptions.builder().build(); } + AtCommandExecutorContext context = createContext(atSign, keys, config); + Consumer onReady = null; if (withMonitoring) { Notifications.EventBusBridge eventBusBridge = new Notifications.EventBusBridge(eventBus, atSign, monitorOptions); - onReady = Notifications.monitor(atSign, monitorOptions, keys, config, eventBusBridge); + onReady = createMonitoringOnReady(context, monitorOptions, eventBusBridge); } AtCommandExecutor executor = AtCommandExecutors.builder() .url(url) - .atSign(atSign) - .keys(keys) + .context(context) .onReady(onReady) - .config(config) .timeoutMillis(timeoutMillis) .awaitReadyMillis(awaitReadyMillis) .reconnect(reconnect) @@ -83,16 +84,41 @@ public static AtClient createAtClient(String url, .build(); return AtClientImpl.builder() - .atSign(atSign) - .keys(keys) + .context(context) .withMonitoring(withMonitoring) .monitorOptions(monitorOptions) - .config(config) .executor(executor) .eventBus(eventBus) .build(); } + /** + * The one context for the whole connection. The executor's {@code onReady} sequence and every + * command the client issues both read from it, so they share one {@code from:} challenge and one + * client config. + * + *

+ * The config is enriched here — {@code clientId} plus the {@code client-config.properties} entries + * — and enriched exactly once, because a second call would mint a second {@code clientId}. + * + *

+ * Package-private so a test can drive it together with {@link #createMonitoringOnReady}. + */ + static AtCommandExecutorContext createContext(AtSign atSign, AtKeys keys, Map config) { + return new AtCommandExecutorContext(atSign, keys, AtCommandExecutors.createClientConfig(config)); + } + + /** + * The {@code onReady} sequence for a client built with monitoring: authenticate with PKAM using the + * connection's context, then send the {@code monitor} command. Package-private so that it can be + * driven with a stubbed {@link AtCommandExecutor} in tests. + */ + static Consumer createMonitoringOnReady(AtCommandExecutorContext context, + MonitorOptions monitorOptions, + Consumer consumer) { + return Notifications.monitor(context, monitorOptions, consumer); + } + private static AtKeys loadKeys(String path, AtSign atSign) throws AtClientConfigException { if (path == null) { return KeysUtils.loadKeys(atSign); diff --git a/at_client/src/main/java/org/atsign/client/impl/AtCommandExecutors.java b/at_client/src/main/java/org/atsign/client/impl/AtCommandExecutors.java index 70773ada..e7dfeb66 100644 --- a/at_client/src/main/java/org/atsign/client/impl/AtCommandExecutors.java +++ b/at_client/src/main/java/org/atsign/client/impl/AtCommandExecutors.java @@ -22,7 +22,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.Consumer; -import static org.atsign.client.impl.common.Preconditions.checkNotNull; +import static org.atsign.client.impl.common.Preconditions.*; /** * Utility methods / builders for instantiating {@link AtCommandExecutor} implementations @@ -42,10 +42,9 @@ * * NOTE: If the url is prefixed with proxy (e.g. proxy:host:port) then the builder * will automatically attempt to connect to an At Server at host:port. - * NOTE: If an atSign is provided then the builder issues {@code from:@atSign} as the first - * command once connected (so proxies / gateways can route the connection); if keys are also - * provided it then authenticates the {@link AtCommandExecutor} with PKAM, reusing that - * {@code from:}'s challenge. + * NOTE: The builder issues {@code from:@atSign} as the first command once connected (so + * proxies / gateways can route the connection); if keys are also provided it then authenticates the + * {@link AtCommandExecutor} with PKAM, reusing that {@code from:}'s challenge. * NOTE: If reconnect is not set then the builder will default to a * {@link SimpleReconnectStrategy} */ @@ -58,6 +57,7 @@ public class AtCommandExecutors { public static AtCommandExecutor createCommandExecutor(String url, AtSign atSign, AtKeys keys, + AtCommandExecutorContext context, Consumer onReady, Map config, Long timeoutMillis, @@ -67,22 +67,16 @@ public static AtCommandExecutor createCommandExecutor(String url, Boolean isVerbose) throws AtException { - if (AtEndpointSuppliers.isProxyUrl(url)) { - checkNotNull(atSign, "atSign not set"); - } - - // the context is closed over by the onReady consumers the builder wires (see createOnReady); the - // command executor itself stays pure transport and knows nothing about it - AtCommandExecutorContext context = new AtCommandExecutorContext(atSign, keys, createClientConfig(config)); + AtCommandExecutorContext executorContext = resolveContext(context, atSign, keys, config); return NettyAtCommandExecutor.builder() - .endpoint(AtEndpointSuppliers.builder().url(url).atSign(atSign).build()) + .endpoint(AtEndpointSuppliers.builder().url(url).atSign(executorContext.getAtSign()).build()) .isVerbose(isVerbose) .timeoutMillis(defaultIfNotSet(timeoutMillis, DEFAULT_TIMEOUT_MILLIS)) .awaitReadyMillis(defaultIfNotSet(awaitReadyMillis, DEFAULT_TIMEOUT_MILLIS)) .reconnect(defaultIfNotSet(reconnect, SimpleReconnectStrategy.builder().build())) .queueLimit(queueLimit) - .onReady(defaultIfNotSet(onReady, createOnReady(context))) + .onReady(defaultIfNotSet(onReady, createOnReady(executorContext))) .build(); } @@ -94,8 +88,9 @@ public static AtCommandExecutor createCommandExecutor(String url, * * AtCommandExecutors.builder() * .url(...) // the url for the root server or proxy (optional) - * .atSign(...) // the AtSign that this client will authenticate as (optional) + * .atSign(...) // the AtSign that this client will authenticate as (unless context is set) * .keys(...) // the AtKeys that this client will use (optional) + * .context(...) // the connection context to share with the caller (instead of the above) * .timeoutMillis() // timeout after which commands will complete exceptionally (optional) * .awaitReadyMillis() // how long to wait for executor to become ready during build() (optional) * .reconnect() // a ReconnectStrategy (optional) @@ -112,6 +107,11 @@ public static AtCommandExecutor createCommandExecutor(String url, * {@link AtCommandExecutors#DEFAULT_TIMEOUT_MILLIS}. * If reconnect is not set then the builder will default to a {@link SimpleReconnectStrategy} * with no limit to the retry attempts. + * If context is not set then the builder creates one from atSign, keys and + * config, and atSign is then required. Set context when the caller needs the + * same context for its own commands, so that the {@code from:} challenge and the client config are + * shared rather than duplicated; it carries the whole identity, so it cannot be combined with + * atSign, keys or config. */ public static class AtCommandExecutorBuilder { // required for javadoc @@ -135,16 +135,30 @@ public static Map createClientConfig(Map config) } /** - * The default protocol for a newly-ready connection. A connection with no atSign (e.g. one talking - * to the atDirectory / root server) sends nothing. Every connection that has an atSign issues - * {@code from:@atSign} first so that proxies / gateways can route it; if keys are also present it - * then authenticates with PKAM, reusing the challenge from that initial {@code from:}. + * The context the {@code onReady} consumers read the connection's identity from (see + * {@link #createOnReady}). The executor itself only sends and receives, and never sees it. + * + *

+ * A caller that needs the same context for its own commands passes one in, so that the connection + * has a single challenge and a single {@code clientId} rather than one of each per holder. */ - private static Consumer createOnReady(AtCommandExecutorContext context) { - if (context.getAtSign() == null) { - return c -> { - }; + private static AtCommandExecutorContext resolveContext(AtCommandExecutorContext context, + AtSign atSign, + AtKeys keys, + Map config) { + if (context != null) { + checkAllNull("both context and one or more of atSign,keys and config set", atSign, keys, config); + return context; } + return new AtCommandExecutorContext(atSign, keys, createClientConfig(config)); + } + + /** + * The default protocol for a newly-ready connection: issue {@code from:@atSign} first so that + * proxies / gateways can route it, then, if keys are present, authenticate with PKAM reusing the + * challenge from that initial {@code from:}. + */ + private static Consumer createOnReady(AtCommandExecutorContext context) { Consumer onReady = AuthenticationCommands.sendFrom(context); if (context.getKeys() != null) { onReady = onReady.andThen(AuthenticationCommands.pkamAuthenticator(context)); diff --git a/at_client/src/main/java/org/atsign/client/impl/AtEndpointSuppliers.java b/at_client/src/main/java/org/atsign/client/impl/AtEndpointSuppliers.java index f6ce24d8..35e105df 100644 --- a/at_client/src/main/java/org/atsign/client/impl/AtEndpointSuppliers.java +++ b/at_client/src/main/java/org/atsign/client/impl/AtEndpointSuppliers.java @@ -91,8 +91,4 @@ public static AtEndpointSupplier createEndpointSupplier(String url, AtSign atSig public static class AtEndpointSuppliersBuilder { // required for javadoc } - - public static boolean isProxyUrl(String s) { - return s != null && PATTERN_PROXY_URL.matcher(s).matches(); - } } diff --git a/at_client/src/main/java/org/atsign/client/impl/cli/AbstractCli.java b/at_client/src/main/java/org/atsign/client/impl/cli/AbstractCli.java index 73a85e85..78f810ee 100644 --- a/at_client/src/main/java/org/atsign/client/impl/cli/AbstractCli.java +++ b/at_client/src/main/java/org/atsign/client/impl/cli/AbstractCli.java @@ -113,7 +113,7 @@ protected AtCommandExecutor createAuthenticatedConnection(String rootUrl, AtSign protected AtCommandExecutor createConnectionSendingFrom(AtCommandExecutorContext context) throws AtException { return AtCommandExecutors.builder() .url(rootUrl) - .atSign(context.getAtSign()) + .context(context) .onReady(AuthenticationCommands.sendFrom(context)) .reconnect(ReconnectStrategy.NONE) .isVerbose(verbose) diff --git a/at_client/src/main/java/org/atsign/client/impl/cli/Activate.java b/at_client/src/main/java/org/atsign/client/impl/cli/Activate.java index e7685bd7..4c3793b6 100644 --- a/at_client/src/main/java/org/atsign/client/impl/cli/Activate.java +++ b/at_client/src/main/java/org/atsign/client/impl/cli/Activate.java @@ -28,8 +28,8 @@ import picocli.CommandLine.Parameters; /** - * Utility (and CommandLineInterface) for onboarding and enrolling atSigns and AtSign application - * devices + * Utility (and CommandLineInterface) for onboarding and enrolling atSigns and AtSign + * application devices. */ @Command( mixinStandardHelpOptions = true) @@ -37,7 +37,7 @@ public class Activate extends AbstractCli implements Callable enum Action { onboard, enroll, otp, list, approve, deny, revoke, unrevoke - }; + } public static final String DEFAULT_FIRST_APP = "firstApp"; public static final String DEFAULT_FIRST_DEVICE = "firstDevice"; @@ -218,7 +218,7 @@ public void approve() throws Exception { public void approve(EnrollmentId enrollmentId) throws Exception { try (AtCommandExecutor executor = createAuthenticatedConnection(rootUrl, atSign, connectionRetries)) { - EnrollCommands.approve(executor, getKeys(), enrollmentId); + EnrollCommands.approve(executor, newConnectionContext(getKeys()), enrollmentId); } } @@ -286,7 +286,7 @@ public EnrollmentId enroll(AtCommandExecutor executor) throws Exception { checkNotExists(file); } AtKeys keys = generateAtKeys(false); - keys = EnrollCommands.enroll(executor, atSign, keys, otp, appName, deviceName, namespaces); + keys = EnrollCommands.enroll(executor, newConnectionContext(keys), otp, appName, deviceName, namespaces); KeysUtils.saveKeys(keys, keysFile); return keys.getEnrollmentId(); } @@ -301,7 +301,7 @@ public void complete() throws Exception { public void complete(AtCommandExecutor executor) throws Exception { AtKeys keys = KeysUtils.loadKeys(keysFile); - keys = EnrollCommands.complete(executor, atSign, keys); + keys = EnrollCommands.complete(executor, newConnectionContext(keys)); KeysUtils.saveKeys(keys, keysFile); } @@ -316,19 +316,24 @@ public void complete(AtCommandExecutor executor, int retries, long sleepDuration throws Exception { Exception exception; int remainingRetries = retries; + AtKeys keys = KeysUtils.loadKeys(keysFile); + AtCommandExecutorContext context = newConnectionContext(keys); do { Thread.sleep(sleepUnit.toMillis(sleepDuration)); try { - complete(executor); + keys = EnrollCommands.complete(executor, context); + saveKeys(keys, keysFile); return; - } catch (AtUnauthenticatedException e) { - exception = e.getMessage().contains("is pending") ? null : e; } catch (Exception e) { exception = e; } - } while (exception == null && remainingRetries-- > 0); + } while (isPendingApproval(exception) && remainingRetries-- > 0); + + throw exception; + } - throw exception != null ? exception : new IllegalArgumentException(); + private static boolean isPendingApproval(Exception e) { + return e instanceof AtUnauthenticatedException && e.getMessage().contains("is pending"); } protected static AtKeys generateAtKeys(boolean generateEncryptionKeyPair) throws AtEncryptionException { diff --git a/at_client/src/main/java/org/atsign/client/impl/commands/AuthenticationCommands.java b/at_client/src/main/java/org/atsign/client/impl/commands/AuthenticationCommands.java index f4ac9b54..6e5d1336 100644 --- a/at_client/src/main/java/org/atsign/client/impl/commands/AuthenticationCommands.java +++ b/at_client/src/main/java/org/atsign/client/impl/commands/AuthenticationCommands.java @@ -9,7 +9,6 @@ import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.function.Consumer; @@ -18,7 +17,6 @@ import org.atsign.client.api.AtCommandExecutorContext; import org.atsign.client.impl.util.EncryptionUtils; import org.atsign.client.impl.exceptions.AtException; -import org.atsign.client.api.AtSign; import org.atsign.client.impl.exceptions.AtEncryptionException; import org.atsign.client.impl.exceptions.AtUnauthenticatedException; @@ -58,70 +56,33 @@ public static Consumer sendFrom(AtCommandExecutorContext cont * @return an onReady consumer that performs PKAM authentication */ public static Consumer pkamAuthenticator(AtCommandExecutorContext context) { - return throwOnReadyException(executor -> authenticateWithPkam(executor, context.getAtSign(), - context.getKeys(), context.getConfig(), - context.consumeChallenge())); - } - - public static Consumer pkamAuthenticator(AtSign atSign, AtKeys keys, Map config) { - return throwOnReadyException(executor -> authenticateWithPkam(executor, atSign, keys, config)); - } - - /** - * Implements the protocol workflow / sequence for PKAM authentication. - * - * @param executor The executor with which to send the commands. - * @param atSign The asign to authenticate. - * @param keys The keys to use to authenticate. - * @throws AtException If authentication fails. - */ - public static void authenticateWithPkam(AtCommandExecutor executor, AtSign atSign, AtKeys keys) - throws AtException { - authenticateWithPkam(executor, atSign, keys, null); + return throwOnReadyException(executor -> authenticateWithPkam(executor, context)); } /** - * Implements the protocol workflow / sequence for PKAM authentication. + * Implements the protocol workflow / sequence for PKAM authentication, using the identity in the + * given {@code context}. Reuses the challenge from an initial {@code from:} (as issued by + * {@link #sendFrom(AtCommandExecutorContext)}) when the context holds one, and otherwise issues its + * own {@code from:}. * * @param executor The executor with which to send the commands. - * @param atSign The asign to authenticate. - * @param keys The keys to use to authenticate. - * @param config The map of configuration values to send in the from command. + * @param context The connection context; supplies the atSign, keys and client config, and holds + * the {@code from:} challenge. * @throws AtException If authentication fails. */ - public static void authenticateWithPkam(AtCommandExecutor executor, - AtSign atSign, - AtKeys keys, - Map config) - throws AtException { - authenticateWithPkam(executor, atSign, keys, config, null); - } - - /** - * Implements the protocol workflow / sequence for PKAM authentication, reusing an already-issued - * {@code from:} challenge when one is supplied. - * - * @param executor The executor with which to send the commands. - * @param atSign The asign to authenticate. - * @param keys The keys to use to authenticate. - * @param config The map of configuration values to send in the from command. - * @param reusableChallenge The challenge from an initial {@code from:} to reuse, or {@code null} to - * issue a fresh {@code from:}. - * @throws AtException If authentication fails. - */ - private static void authenticateWithPkam(AtCommandExecutor executor, - AtSign atSign, - AtKeys keys, - Map config, - String reusableChallenge) + public static void authenticateWithPkam(AtCommandExecutor executor, AtCommandExecutorContext context) throws AtException { + AtKeys keys = context.getKeys(); try { // reuse the challenge from the initial from: if one was issued on this connection, otherwise // send a from command and expect to receive a challenge - String challenge = reusableChallenge; + String challenge = context.consumeChallenge(); if (challenge == null) { - String fromCommand = CommandBuilders.fromCommandBuilder().atSign(atSign).config(config).build(); + String fromCommand = CommandBuilders.fromCommandBuilder() + .atSign(context.getAtSign()) + .config(context.getConfig()) + .build(); String fromResponse = executor.sendSync(fromCommand); challenge = matchDataStringNoWhitespace(throwExceptionIfError(fromResponse)); } diff --git a/at_client/src/main/java/org/atsign/client/impl/commands/EnrollCommands.java b/at_client/src/main/java/org/atsign/client/impl/commands/EnrollCommands.java index 963eaa88..df079330 100644 --- a/at_client/src/main/java/org/atsign/client/impl/commands/EnrollCommands.java +++ b/at_client/src/main/java/org/atsign/client/impl/commands/EnrollCommands.java @@ -84,9 +84,11 @@ public static AtKeys onboard(AtCommandExecutor executor, .build(); // authenticate with PKAM — issues its own from: (the connection's challenge was single-use and - // is spent by CRAM above; PKAM also authenticates with the freshly-enrolled keys, not the - // connection identity) - AuthenticationCommands.authenticateWithPkam(executor, atSign, keys); + // is spent by CRAM above). The keys now carry the enrollment id, so this needs a context built + // over them rather than the connection's; the client config is carried across so the from: it + // sends still identifies the client + AtCommandExecutorContext enrolled = new AtCommandExecutorContext(atSign, keys, context.getConfig()); + AuthenticationCommands.authenticateWithPkam(executor, enrolled); // explicitly store the public encryption key in the atserver String updateCommand = CommandBuilders.updateCommandBuilder() @@ -143,14 +145,14 @@ public static String otp(AtCommandExecutor executor) throws AtException { /** * Performs the enrollment request commands for a new application / device set of keys. * NOTE The result of this command will be a pending request that must be approved - * ({@link #approve(AtCommandExecutor, AtKeys, EnrollmentId)}) using keys that have access - * to the manage namespace (the original keys from onboard) and then completed - * ({@link #complete(AtCommandExecutor, AtSign, AtKeys)}). + * ({@link #approve(AtCommandExecutor, AtCommandExecutorContext, EnrollmentId)}) using keys that + * have access to the manage namespace (the original keys from onboard) and then completed + * ({@link #complete(AtCommandExecutor, AtCommandExecutorContext)}). * * @param executor An executor to an atserver command interface. - * @param atSign The AtSign that corresponds to the executor. - * @param keys The {@link AtKeys} for the {@link AtSign}, this should have the APKAM keys populated. - * The rest of the fields will be set once enrollment is complated. + * @param context The connection context; supplies the atSign and the {@link AtKeys} being + * enrolled, which should have the APKAM keys populated. The rest of the fields will be set + * once enrollment is completed. * @param otp A one time password. * @param appName The app name for this enrollment. * @param deviceName The device name for this enrollment. @@ -160,14 +162,15 @@ public static String otp(AtCommandExecutor executor) throws AtException { * @throws AtException If any of the commands fail. */ public static AtKeys enroll(AtCommandExecutor executor, - AtSign atSign, - AtKeys keys, + AtCommandExecutorContext context, String otp, String appName, String deviceName, Map namespaces) throws Exception { + AtSign atSign = context.getAtSign(); + AtKeys keys = context.getKeys(); checkNotNull(keys.getApkamPublicKey(), "apkam public key not set"); checkNotNull(keys.getApkamSymmetricKey(), "apkam symmetric key not set"); @@ -206,18 +209,20 @@ public static AtKeys enroll(AtCommandExecutor executor, * has been approved. * * @param executor An executor to an atserver command interface. - * @param atSign The AtSign that corresponds to the executor. - * @param keys The {@link AtKeys} for the {@link AtSign}, this should have the APKAM keys populated. - * The rest of the fields will be set once enrollment is completed. - * @return A new copy of the {@link AtKeys} that has the private encrypt key and self encrypt key - * set. + * @param context The connection context; supplies the atSign and the {@link AtKeys} being + * enrolled, which should have the APKAM keys populated. The rest of the fields will be set + * once enrollment is completed. + * @return A new copy of the {@link AtKeys} with the private encrypt key and self encrypt key set. * These need to be persisted by the caller. * @throws AtException If any of the commands fail. */ - public static AtKeys complete(AtCommandExecutor executor, AtSign atSign, AtKeys keys) throws AtException { + public static AtKeys complete(AtCommandExecutor executor, AtCommandExecutorContext context) throws AtException { + + AtSign atSign = context.getAtSign(); + AtKeys keys = context.getKeys(); // attempt to authenticate with PKAM, this will succeed once the enroll request is approved - AuthenticationCommands.authenticateWithPkam(executor, atSign, keys); + AuthenticationCommands.authenticateWithPkam(executor, context); // Use the keys:get command to obtain the private encryption key and self encryption key String selfEncryptKey = keysGetSelfEncryptKey(executor, atSign, keys); @@ -263,12 +268,16 @@ public static List list(AtCommandExecutor executor, String status) * Performs the enrollment approve commands for a new application / device set of keys. * * @param executor An executor to an atserver command interface. - * @param keys The {@link AtKeys} for the {@link AtSign} that have the authority to manage - * enrollment requests. Typically the first set of keys from the onboard. + * @param context The connection context; supplies the {@link AtKeys} that have the authority to + * manage enrollment requests. Typically the first set of keys from the onboard. * @param enrollmentId The {@link EnrollmentId} to approve. * @throws AtException If any of the commands fail. */ - public static void approve(AtCommandExecutor executor, AtKeys keys, EnrollmentId enrollmentId) throws AtException { + public static void approve(AtCommandExecutor executor, + AtCommandExecutorContext context, + EnrollmentId enrollmentId) + throws AtException { + AtKeys keys = context.getKeys(); try { // fetch the request and decrypt the apkam symmetric key that will be used encrypt the shared keys @@ -321,8 +330,7 @@ public static void deny(AtCommandExecutor executor, EnrollmentId enrollmentId) t } /** - * Performs the enrollment revoke commands for a previously approved application / device set of - * keys. + * Performs the enrollment revoke commands for a previously approved application / device key set. * * @param executor An executor to an atserver command interface. * @param enrollmentId The {@link EnrollmentId} to revoke. @@ -333,8 +341,7 @@ public static void revoke(AtCommandExecutor executor, EnrollmentId enrollmentId) } /** - * Performs the enrollment revoke commands for a previously revoked application / device set of - * keys. + * Performs the enrollment unrevoke commands for a previously revoked application / device key set. * * @param executor An executor to an atserver command interface. * @param enrollmentId The {@link EnrollmentId} to unrevoke. diff --git a/at_client/src/main/java/org/atsign/client/impl/commands/Notifications.java b/at_client/src/main/java/org/atsign/client/impl/commands/Notifications.java index e2e8dba1..de7beee6 100644 --- a/at_client/src/main/java/org/atsign/client/impl/commands/Notifications.java +++ b/at_client/src/main/java/org/atsign/client/impl/commands/Notifications.java @@ -12,8 +12,8 @@ import java.util.regex.Pattern; import org.atsign.client.api.AtCommandExecutor; +import org.atsign.client.api.AtCommandExecutorContext; import org.atsign.client.api.AtEvents; -import org.atsign.client.api.AtKeys; import org.atsign.client.api.AtSign; import org.atsign.client.impl.exceptions.AtException; @@ -31,66 +31,41 @@ public class Notifications { /** * Creates a {@link Consumer} that can be passed to {@link AtCommandExecutor#onReady(Consumer)} to - * request notifications. NOTE monitoring is contingent on authentication to this will - * authenticate - * with pkam prior to sending the monitor command. + * request notifications. NOTE monitoring is contingent on authentication so this will + * authenticate with pkam prior to sending the monitor command. * - * @param atSign The {@link AtSign} to authenticate. + * @param context The connection context; supplies the atSign / keys / client config and the + * {@code from:} challenge. * @param options optional arguments that influence the server behavior. - * @param keys The {@link AtKeys} to authenticate with. * @param consumer A consumer that will be invoked with each notification. * @return A consumer that can be provided as OnReady argument. */ - public static Consumer monitor(AtSign atSign, + public static Consumer monitor(AtCommandExecutorContext context, MonitorOptions options, - AtKeys keys, - Map config, Consumer consumer) { - return throwOnReadyException(executor -> monitor(executor, atSign, options, keys, config, consumer)); + return throwOnReadyException(executor -> monitor(executor, context, options, consumer)); } /** * Sends the commands to perform PKAM authentication followed by monitor command. * * @param executor The {@link AtCommandExecutor} to use. - * @param atSign The {@link AtSign} to authenticate. + * @param context The connection context; supplies the atSign / keys / client config and the + * {@code from:} challenge. * @param options optional arguments that influence the server behavior. - * @param keys The {@link AtKeys} to authenticate with. * @param consumer A consumer that will be invoked with each notification. * @throws AtException If any of the commands fail. */ public static void monitor(AtCommandExecutor executor, - AtSign atSign, + AtCommandExecutorContext context, MonitorOptions options, - AtKeys keys, - Consumer consumer) - throws AtException { - monitor(executor, atSign, options, keys, null, consumer); - } - - /** - * Sends the commands to perform PKAM authentication followed by monitor command. - * - * @param executor The {@link AtCommandExecutor} to use. - * @param atSign The {@link AtSign} to authenticate. - * @param options optional arguments that influence the server behavior. - * @param keys The {@link AtKeys} to authenticate with. - * @param config The map of configuration values to send with the from command. - * @param consumer A consumer that will be invoked with each notification. - * notifications. - * @throws AtException If any of the commands fail. - */ - public static void monitor(AtCommandExecutor executor, - AtSign atSign, - MonitorOptions options, - AtKeys keys, - Map config, Consumer consumer) throws AtException { try { - // authenticate - authenticateWithPkam(executor, atSign, keys, config); + // authenticate; the context supplies the client config and any challenge already retained by + // the initial from: on this connection + authenticateWithPkam(executor, context); // send monitor command String command = CommandBuilders.monitorCommandBuilder().options(options).build(); diff --git a/at_client/src/main/java/org/atsign/client/impl/commands/PublicKeyCommands.java b/at_client/src/main/java/org/atsign/client/impl/commands/PublicKeyCommands.java index 6ce4df93..f1ed4e25 100644 --- a/at_client/src/main/java/org/atsign/client/impl/commands/PublicKeyCommands.java +++ b/at_client/src/main/java/org/atsign/client/impl/commands/PublicKeyCommands.java @@ -11,6 +11,7 @@ import org.atsign.client.api.AtClient.GetRequestOptions; import org.atsign.client.api.AtCommandExecutor; +import org.atsign.client.api.AtCommandExecutorContext; import org.atsign.client.api.AtKeys; import org.atsign.client.api.AtSign; import org.atsign.client.api.Keys.PublicKey; @@ -27,34 +28,38 @@ public class PublicKeyCommands { * Get the String value associated with a public key. * * @param executor The {@link AtCommandExecutor} to use. - * @param atSign The AtSign that corresponds to the executor. + * @param context The connection context; supplies the atSign. * @param key The {@link PublicKey} * @param options If set then can be used to bypass caches. * @return The associated value. * @throws AtException If any of the commands fail or the key does not exist. */ - public static String get(AtCommandExecutor executor, AtSign atSign, PublicKey key, GetRequestOptions options) + public static String get(AtCommandExecutor executor, + AtCommandExecutorContext context, + PublicKey key, + GetRequestOptions options) throws AtException { - return get(executor, atSign, key, false, options); + return get(executor, context, key, false, options); } /** * Get the String value associated with a public key. * * @param executor The {@link AtCommandExecutor} to use. - * @param atSign The AtSign that corresponds to the executor. + * @param context The connection context; supplies the atSign. * @param key The {@link PublicKey} + * @param expectBinary If true then metadata will be checked * @param options If set then can be used to bypass caches. * @return The associated value. * @throws AtException If any of the commands fail or the key does not exist. */ public static String get(AtCommandExecutor executor, - AtSign atSign, + AtCommandExecutorContext context, PublicKey key, boolean expectBinary, GetRequestOptions options) throws AtException { - if (atSign.equals(key.sharedBy())) { + if (context.getAtSign().equals(key.sharedBy())) { return getSharedByMe(executor, key, expectBinary); } else { return getSharedByOther(executor, key, expectBinary, options); @@ -154,13 +159,15 @@ public static String getSharedByOther(AtCommandExecutor executor, * metadata using the AtSign's Private Encryption Key. * * @param executor The {@link AtCommandExecutor} to use. - * @param atSign The AtSign that corresponds to the executor. + * @param context The connection context; supplies the atSign and keys. * @param key The {@link PublicKey} * @param value The associated value. * @throws AtException If any of the commands fail or the key does not exist. */ - public static void put(AtCommandExecutor executor, AtSign atSign, AtKeys keys, PublicKey key, String value) + public static void put(AtCommandExecutor executor, AtCommandExecutorContext context, PublicKey key, String value) throws AtException { + AtSign atSign = context.getAtSign(); + AtKeys keys = context.getKeys(); checkAtSignCanPut(atSign, key); try { diff --git a/at_client/src/main/java/org/atsign/client/impl/commands/SelfKeyCommands.java b/at_client/src/main/java/org/atsign/client/impl/commands/SelfKeyCommands.java index bf967a27..7a0cf1bc 100644 --- a/at_client/src/main/java/org/atsign/client/impl/commands/SelfKeyCommands.java +++ b/at_client/src/main/java/org/atsign/client/impl/commands/SelfKeyCommands.java @@ -25,13 +25,14 @@ public class SelfKeyCommands { * Self Encryption Key. * * @param executor The {@link AtCommandExecutor} to use. - * @param atSign The AtSign that corresponds to the executor. + * @param context The connection context; supplies the atSign and keys. * @param key The {@link Keys.SelfKey} * @return The associated value. * @throws AtException If any of the commands fail or the key does not exist. */ - public static String get(AtCommandExecutor executor, AtSign atSign, AtKeys keys, SelfKey key) throws AtException { - return get(executor, atSign, keys, key, false); + public static String get(AtCommandExecutor executor, AtCommandExecutorContext context, SelfKey key) + throws AtException { + return get(executor, context, key, false); } /** @@ -39,18 +40,19 @@ public static String get(AtCommandExecutor executor, AtSign atSign, AtKeys keys, * Self Encryption Key. * * @param executor The {@link AtCommandExecutor} to use. - * @param atSign The AtSign that corresponds to the executor. + * @param context The connection context; supplies the atSign and keys. * @param key The {@link Keys.SelfKey} * @param expectBinary If true then metadata will be checked * @return The associated value. * @throws AtException If any of the commands fail or the key does not exist. */ public static String get(AtCommandExecutor executor, - AtSign atSign, - AtKeys keys, + AtCommandExecutorContext context, SelfKey key, boolean expectBinary) throws AtException { + AtSign atSign = context.getAtSign(); + AtKeys keys = context.getKeys(); checkAtSignCanGet(atSign, key); try { @@ -79,17 +81,18 @@ public static String get(AtCommandExecutor executor, /** * Set a String value to be associated with a self key. The value will be encrypted with the - * AtSign's - * Self Encryption Key. + * AtSign's Self Encryption Key. * * @param executor The {@link AtCommandExecutor} to use. - * @param atSign The AtSign that corresponds to the executor. + * @param context The connection context; supplies the atSign and keys. * @param key The {@link Keys.SelfKey} * @param value The associated value. * @throws AtException If any of the commands fail or the key does not exist. */ - public static void put(AtCommandExecutor executor, AtSign atSign, AtKeys keys, SelfKey key, String value) + public static void put(AtCommandExecutor executor, AtCommandExecutorContext context, SelfKey key, String value) throws AtException { + AtSign atSign = context.getAtSign(); + AtKeys keys = context.getKeys(); checkAtSignCanPut(atSign, key); try { diff --git a/at_client/src/main/java/org/atsign/client/impl/commands/SharedKeyCommands.java b/at_client/src/main/java/org/atsign/client/impl/commands/SharedKeyCommands.java index 6e3116cc..d83163f3 100644 --- a/at_client/src/main/java/org/atsign/client/impl/commands/SharedKeyCommands.java +++ b/at_client/src/main/java/org/atsign/client/impl/commands/SharedKeyCommands.java @@ -28,14 +28,14 @@ public class SharedKeyCommands { * key for the sharedBy-sharedWith relationship. * * @param executor The {@link AtCommandExecutor} to use. - * @param atSign The AtSign that corresponds to the executor. + * @param context The connection context; supplies the atSign and keys. * @param key The {@link Keys.SharedKey} * @return The associated value. * @throws AtException If any of the commands fail or the key does not exist. */ - - public static String get(AtCommandExecutor executor, AtSign atSign, AtKeys keys, SharedKey key) throws AtException { - return get(executor, atSign, keys, key, false); + public static String get(AtCommandExecutor executor, AtCommandExecutorContext context, SharedKey key) + throws AtException { + return get(executor, context, key, false); } /** @@ -43,46 +43,47 @@ public static String get(AtCommandExecutor executor, AtSign atSign, AtKeys keys, * key for the sharedBy-sharedWith relationship. * * @param executor The {@link AtCommandExecutor} to use. - * @param atSign The AtSign that corresponds to the executor. + * @param context The connection context; supplies the atSign and keys. * @param key The {@link Keys.SharedKey} * @param expectedBinary If true then lookup metadata will be checked * @return The associated value. * @throws AtException If any of the commands fail or the key does not exist. */ - - public static String get(AtCommandExecutor executor, AtSign atSign, AtKeys keys, SharedKey key, + public static String get(AtCommandExecutor executor, AtCommandExecutorContext context, SharedKey key, boolean expectedBinary) throws AtException { + AtSign atSign = context.getAtSign(); checkAtSignCanGet(atSign, key); if (key.sharedBy().equals(atSign)) { - return getSharedByMe(executor, keys, key, expectedBinary); + return getSharedByMe(executor, context, key, expectedBinary); } else if (key.sharedWith().equals(atSign)) { - return getSharedByOther(executor, keys, key, expectedBinary); + return getSharedByOther(executor, context, key, expectedBinary); } else { throw new IllegalArgumentException("the client atsign is neither the sharedBy or sharedWith"); } } /** - * Set a String value to be associated with a shared key. The value will be decrypted with a - * specific - * key for the sharedBy-sharedWith relationship. + * Set a String value to be associated with a shared key. The value will be encrypted with a key + * specific to the sharedBy-sharedWith relationship. * * @param executor The {@link AtCommandExecutor} to use. - * @param atSign The AtSign that corresponds to the executor. + * @param context The connection context; supplies the atSign and keys. * @param key The {@link Keys.SharedKey} * @param value The associated value. * @throws AtException If any of the commands fail or the key does not exist. */ - public static void put(AtCommandExecutor executor, AtSign atSign, AtKeys keys, SharedKey key, String value) + public static void put(AtCommandExecutor executor, AtCommandExecutorContext context, SharedKey key, String value) throws AtException { + AtSign atSign = context.getAtSign(); + AtKeys keys = context.getKeys(); checkAtSignCanPut(atSign, key); try { // get or create key for sharedBy - sharedWith - String aesKey = lookupEncryptKeySharedByMe(executor, keys, key); + String aesKey = lookupEncryptKeySharedByMe(executor, context, key); if (aesKey == null) { - aesKey = createEncryptKey(executor, keys, key); + aesKey = createEncryptKey(executor, context, key); } // encrypt the value @@ -102,7 +103,10 @@ public static void put(AtCommandExecutor executor, AtSign atSign, AtKeys keys, S } } - private static String getSharedByMe(AtCommandExecutor executor, AtKeys keys, SharedKey key, boolean expectBinary) + private static String getSharedByMe(AtCommandExecutor executor, + AtCommandExecutorContext context, + SharedKey key, + boolean expectBinary) throws AtException { try { @@ -115,7 +119,7 @@ private static String getSharedByMe(AtCommandExecutor executor, AtKeys keys, Sha } // get the encryption key that was previously created by "me" - String aesKey = checkNotNull(lookupEncryptKeySharedByMe(executor, keys, key), key + " not found"); + String aesKey = checkNotNull(lookupEncryptKeySharedByMe(executor, context, key), key + " not found"); // return decrypted value return aesDecryptFromBase64(llookupResponse.data, aesKey, llookupResponse.metaData.ivNonce()); @@ -125,7 +129,10 @@ private static String getSharedByMe(AtCommandExecutor executor, AtKeys keys, Sha } } - private static String getSharedByOther(AtCommandExecutor executor, AtKeys keys, SharedKey key, boolean expectBinary) + private static String getSharedByOther(AtCommandExecutor executor, + AtCommandExecutorContext context, + SharedKey key, + boolean expectBinary) throws AtException { try { @@ -140,9 +147,9 @@ private static String getSharedByOther(AtCommandExecutor executor, AtKeys keys, // get the encryption key that was created by the "other" String sharedEncryptionKey; if (lookupResponse.metaData.sharedKeyEnc() != null) { - sharedEncryptionKey = extractEncryptKeySharedByOther(lookupResponse, keys); + sharedEncryptionKey = extractEncryptKeySharedByOther(lookupResponse, context.getKeys()); } else { - sharedEncryptionKey = lookupEncryptKeySharedByOther(executor, keys, key); + sharedEncryptionKey = lookupEncryptKeySharedByOther(executor, context, key); } // return decrypted value @@ -172,13 +179,16 @@ private static String extractEncryptKeySharedByOther(LookupResponse lookupRespon * AtSign. This will automatically decrypt the value with the AtKeys Private Encryption Key. * * @param executor The {@link AtCommandExecutor} to use. - * @param keys The {@link AtKeys} for the {@link AtSign} that is the sharedBy in the relationship. + * @param context The connection context; supplies the keys of the sharedBy {@link AtSign}. * @param key The {@link Keys.SharedKey} * @return The symmetric encryption key (in base64). * @throws AtException If any of the commands fail or the key does not exist. */ - public static String lookupEncryptKeySharedByMe(AtCommandExecutor executor, AtKeys keys, SharedKey key) + public static String lookupEncryptKeySharedByMe(AtCommandExecutor executor, + AtCommandExecutorContext context, + SharedKey key) throws AtException { + AtKeys keys = context.getKeys(); try { String keyName = AtKeyNames.toSharedByMeKeyName(key.sharedWith()); @@ -217,13 +227,16 @@ public static String lookupEncryptKeySharedByMe(AtCommandExecutor executor, AtKe * AtSign. This will automatically decrypt the value with the AtKeys Private Encryption Key. * * @param executor The {@link AtCommandExecutor} to use. - * @param keys The {@link AtKeys} for the {@link AtSign} that is the sharedWith in the relationship. + * @param context The connection context; supplies the keys of the sharedWith {@link AtSign}. * @param key The {@link Keys.SharedKey} * @return The symmetric encryption key (in base64). * @throws AtException If any of the commands fail or the key does not exist. */ - public static String lookupEncryptKeySharedByOther(AtCommandExecutor executor, AtKeys keys, SharedKey key) + public static String lookupEncryptKeySharedByOther(AtCommandExecutor executor, + AtCommandExecutorContext context, + SharedKey key) throws AtException { + AtKeys keys = context.getKeys(); try { // check in Keys cache @@ -254,7 +267,11 @@ public static String lookupEncryptKeySharedByOther(AtCommandExecutor executor, A } } - private static String createEncryptKey(AtCommandExecutor executor, AtKeys keys, SharedKey key) throws AtException { + private static String createEncryptKey(AtCommandExecutor executor, + AtCommandExecutorContext context, + SharedKey key) + throws AtException { + AtKeys keys = context.getKeys(); try { // generate a new encrypt key diff --git a/at_client/src/test/java/org/atsign/client/api/AtCommandExecutorContextTest.java b/at_client/src/test/java/org/atsign/client/api/AtCommandExecutorContextTest.java new file mode 100644 index 00000000..f32f8b12 --- /dev/null +++ b/at_client/src/test/java/org/atsign/client/api/AtCommandExecutorContextTest.java @@ -0,0 +1,74 @@ +package org.atsign.client.api; + +import static org.atsign.client.api.AtSign.createAtSign; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.anEmptyMap; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +class AtCommandExecutorContextTest { + + @Test + void testConfigIsAnEmptyMapWhenNotSupplied() { + AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), null); + + assertThat(context.getConfig(), anEmptyMap()); + } + + @Test + void testConfigIsAnEmptyMapWhenNull() { + AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), null); + + assertThat(context.getConfig(), anEmptyMap()); + } + + @Test + void testConfigCannotBeModified() { + Map config = new HashMap<>(); + config.put("clientId", "abc"); + AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), null, config); + + assertThrows(UnsupportedOperationException.class, () -> context.getConfig().put("clientId", "hijacked")); + } + + @Test + void testConfigIsCopiedSoLaterCallerChangesDoNotLeakIn() { + Map config = new HashMap<>(); + config.put("clientId", "abc"); + AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), null, config); + + config.put("clientId", "changed"); + config.put("added", "later"); + + // what the connection sends is fixed at construction + assertThat(context.getConfig(), hasEntry("clientId", (Object) "abc")); + assertThat(context.getConfig(), not(hasEntry("added", (Object) "later"))); + } + + @Test + void testAnAtSignIsRequired() { + // a context is a connection's identity, and every connection issues from:@atSign + IllegalArgumentException ex = + assertThrows(IllegalArgumentException.class, () -> new AtCommandExecutorContext(null, null)); + + assertThat(ex.getMessage(), equalTo("atSign not set")); + } + + @Test + void testAContextHoldsAChallengeAtMostOnce() { + AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), null); + + context.setChallenge("challenge"); + + assertThat(context.consumeChallenge(), equalTo("challenge")); + assertThat(context.consumeChallenge(), nullValue()); + } +} diff --git a/at_client/src/test/java/org/atsign/client/impl/AtClientImplTest.java b/at_client/src/test/java/org/atsign/client/impl/AtClientImplTest.java index a645768d..e5b8bb58 100644 --- a/at_client/src/test/java/org/atsign/client/impl/AtClientImplTest.java +++ b/at_client/src/test/java/org/atsign/client/impl/AtClientImplTest.java @@ -29,6 +29,7 @@ class AtClientImplTest { private AtCommandExecutor executor; private AtClientImpl client; private AtSign atSign; + private AtCommandExecutorContext context; @BeforeEach void setUp() throws Exception { @@ -40,9 +41,9 @@ void setUp() throws Exception { .encryptKeyPair(generateRSAKeyPair()) .selfEncryptKey(generateAESKeyBase64()) .build(); + context = new AtCommandExecutorContext(atSign, keys); client = AtClientImpl.builder() - .atSign(atSign) - .keys(keys) + .context(context) .executor(executor) .eventBus(bus) .build(); @@ -54,8 +55,7 @@ void tearDown() {} @Test void testSetAtSignReturnsConstructorArg() { AtClientImpl client = AtClientImpl.builder() - .atSign(createAtSign("test")) - .keys(keys) + .context(context) .executor(executor) .eventBus(bus) .build(); @@ -65,8 +65,7 @@ void testSetAtSignReturnsConstructorArg() { @Test void testGetCommandExecutorReturnsConstructorArg() { AtClientImpl client = AtClientImpl.builder() - .atSign(createAtSign("test")) - .keys(keys) + .context(context) .executor(executor) .eventBus(bus) .build(); @@ -85,13 +84,14 @@ void testStartMonitor() throws Exception { // stub the executor so that onReady consumer successfully authenticates executor = TestExecutorBuilder.builder() - .stub("from:@alice", "data:challenge") + .stub("from:@alice:clientConfig:.*12345.*", "data:challenge") .stub("pkam:[^{].+", "data:success") .build(); + Map config = Collections.singletonMap("clientId", "12345"); + AtCommandExecutorContext contextAlice = new AtCommandExecutorContext(createAtSign("alice"), keys, config); AtClientImpl client = AtClientImpl.builder() - .atSign(createAtSign("alice")) - .keys(keys) + .context(contextAlice) .executor(executor) .eventBus(bus) .build(); @@ -127,13 +127,14 @@ void testStartMonitor() throws Exception { void testStopMonitor() throws Exception { // stub the executor so that onReady consumer successfully authenticates executor = TestExecutorBuilder.builder() - .stub("from:@alice", "data:challenge") + .stub("from:@alice:clientConfig:.*12345.*", "data:challenge") .stub("pkam:[^{].+", "data:success") .build(); + Map config = Collections.singletonMap("clientId", "12345"); + AtCommandExecutorContext contextAlice = new AtCommandExecutorContext(createAtSign("alice"), keys, config); AtClientImpl client = AtClientImpl.builder() - .atSign(createAtSign("alice")) - .keys(keys) + .context(contextAlice) .executor(executor) .eventBus(bus) .build(); @@ -193,7 +194,11 @@ void testGetSelfKey() throws Exception { .stubExecutionException("llookup:all:key3@test") .build(); - AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build(); + AtClientImpl client = AtClientImpl.builder() + .context(context) + .executor(executor) + .eventBus(bus) + .build(); Keys.SelfKey key1 = Keys.selfKeyBuilder().sharedBy(atSign).name("key1").build(); assertThat(client.get(key1), equalTo("hello me")); @@ -219,7 +224,11 @@ void testGetSelfKeyBinary() throws Exception { .stubExecutionException("llookup:all:key3@test") .build(); - AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build(); + AtClientImpl client = AtClientImpl.builder() + .context(context) + .executor(executor) + .eventBus(bus) + .build(); Keys.SelfKey key1 = Keys.selfKeyBuilder().sharedBy(atSign).name("key1").build(); assertThat(client.getBinary(key1), equalTo(bytes)); @@ -243,7 +252,11 @@ void testPutSelfKey() throws Exception { .stubExecutionException("update:dataSignature:.+:key3@test .+") .build(); - AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build(); + AtClientImpl client = AtClientImpl.builder() + .context(context) + .executor(executor) + .eventBus(bus) + .build(); Keys.SelfKey key1 = Keys.selfKeyBuilder().sharedBy(atSign).name("key1").build(); client.put(key1, "hello world"); @@ -265,7 +278,11 @@ void testPutSelfKeyBytes() throws Exception { .stubExecutionException("update:.+:key3@test .+") .build(); - AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build(); + AtClientImpl client = AtClientImpl.builder() + .context(context) + .executor(executor) + .eventBus(bus) + .build(); Keys.SelfKey key1 = Keys.selfKeyBuilder().sharedBy(atSign).name("key1").build(); client.put(key1, bytes); @@ -284,7 +301,11 @@ void testDeleteSelfKey() throws Exception { .stubExecutionException("delete:key3@test") .build(); - AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build(); + AtClientImpl client = AtClientImpl.builder() + .context(context) + .executor(executor) + .eventBus(bus) + .build(); Keys.SelfKey key1 = Keys.selfKeyBuilder().sharedBy(atSign).name("key1").build(); client.delete(key1); @@ -304,7 +325,11 @@ void testGetPublicKey() throws Exception { .stubLookupResponse("plookup:all:key4@another", "key4@test", "greetings from another world") .build(); - AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build(); + AtClientImpl client = AtClientImpl.builder() + .context(context) + .executor(executor) + .eventBus(bus) + .build(); Keys.PublicKey key1 = Keys.publicKeyBuilder().sharedBy(atSign).name("key1").build(); assertThat(client.get(key1), equalTo("hello world")); @@ -334,7 +359,11 @@ void testGetPublicKeyBinary() throws Exception { .stubLookupResponse("llookup:all:public:key5@test", "key5@test", Base2e15Utils.encode(bytes1)) .build(); - AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build(); + AtClientImpl client = AtClientImpl.builder() + .context(context) + .executor(executor) + .eventBus(bus) + .build(); Keys.PublicKey key1 = Keys.publicKeyBuilder().sharedBy(atSign).name("key1").build(); assertThat(client.getBinary(key1), equalTo(bytes1)); @@ -360,7 +389,11 @@ void testPutPublicKey() throws Exception { .stubExecutionException("update:dataSignature:.+:key3@test .+") .build(); - AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build(); + AtClientImpl client = AtClientImpl.builder() + .context(context) + .executor(executor) + .eventBus(bus) + .build(); Keys.PublicKey key1 = Keys.publicKeyBuilder().sharedBy(atSign).name("key1").build(); client.put(key1, "hello world"); @@ -383,7 +416,11 @@ void testPutPublicKeyBytes() throws Exception { .stubExecutionException("update:dataSignature:.+:key3@test .+") .build(); - AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build(); + AtClientImpl client = AtClientImpl.builder() + .context(context) + .executor(executor) + .eventBus(bus) + .build(); Keys.PublicKey key1 = Keys.publicKeyBuilder().sharedBy(atSign).name("key1").build(); client.put(key1, bytes); @@ -402,7 +439,11 @@ void testDeletePublicKey() throws Exception { .stubExecutionException("delete:public:key3@test") .build(); - AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build(); + AtClientImpl client = AtClientImpl.builder() + .context(context) + .executor(executor) + .eventBus(bus) + .build(); Keys.PublicKey key1 = Keys.publicKeyBuilder().sharedBy(atSign).name("key1").build(); client.delete(key1); @@ -432,7 +473,11 @@ void testGetSharedKey() throws Exception { "ivNonce", iv, "sharedKeyEnc", sharedKeyEnc) .build(); - AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build(); + AtClientImpl client = AtClientImpl.builder() + .context(context) + .executor(executor) + .eventBus(bus) + .build(); AtSign atSign2 = createAtSign("another"); Keys.SharedKey key1 = Keys.sharedKeyBuilder().sharedBy(atSign).sharedWith(atSign2).name("key1").build(); @@ -472,7 +517,11 @@ void testGetSharedKeyBinary() throws Exception { .stubLookupResponse("llookup:all:@another:key5@test", "key5@test", encrypted1, "ivNonce", iv) .build(); - AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build(); + AtClientImpl client = AtClientImpl.builder() + .context(context) + .executor(executor) + .eventBus(bus) + .build(); AtSign atSign2 = createAtSign("another"); Keys.SharedKey key1 = Keys.sharedKeyBuilder().sharedBy(atSign).sharedWith(atSign2).name("key1").build(); @@ -501,7 +550,11 @@ void testPutSharedKey() throws Exception { .stubExecutionException("update:.+:key3@test .+") .build(); - AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build(); + AtClientImpl client = AtClientImpl.builder() + .context(context) + .executor(executor) + .eventBus(bus) + .build(); AtSign atSign2 = createAtSign("another"); Keys.SharedKey key1 = Keys.sharedKeyBuilder().sharedBy(atSign).sharedWith(atSign2).name("key1").build(); @@ -527,7 +580,11 @@ void testPutSharedKeyBytes() throws Exception { .stubExecutionException("update:.+:key3@test .+") .build(); - AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build(); + AtClientImpl client = AtClientImpl.builder() + .context(context) + .executor(executor) + .eventBus(bus) + .build(); AtSign atSign2 = createAtSign("another"); Keys.SharedKey key1 = Keys.sharedKeyBuilder().sharedBy(atSign).sharedWith(atSign2).name("key1").build(); @@ -548,7 +605,11 @@ void testDeleteSharedKey() throws Exception { .stubExecutionException("delete:@another:key3@test") .build(); - AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build(); + AtClientImpl client = AtClientImpl.builder() + .context(context) + .executor(executor) + .eventBus(bus) + .build(); AtSign atSign2 = createAtSign("another"); Keys.SharedKey key1 = Keys.sharedKeyBuilder().sharedBy(atSign).sharedWith(atSign2).name("key1").build(); diff --git a/at_client/src/test/java/org/atsign/client/impl/AtClientsTest.java b/at_client/src/test/java/org/atsign/client/impl/AtClientsTest.java new file mode 100644 index 00000000..4cf16d0e --- /dev/null +++ b/at_client/src/test/java/org/atsign/client/impl/AtClientsTest.java @@ -0,0 +1,118 @@ +package org.atsign.client.impl; + +import static org.atsign.client.api.AtSign.createAtSign; +import static org.atsign.client.impl.util.EncryptionUtils.generateRSAKeyPair; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.matches; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.Collections; +import java.util.Map; + +import org.atsign.client.api.AtCommandExecutor; +import org.atsign.client.api.AtCommandExecutorContext; +import org.atsign.client.api.AtKeys; +import org.atsign.client.impl.commands.MonitorOptions; +import org.atsign.client.impl.commands.TestExecutorBuilder; +import org.atsign.client.impl.common.ReconnectStrategy; +import org.atsign.client.impl.exceptions.AtException; +import org.junit.jupiter.api.Test; + +class AtClientsTest { + + private static final String CLIENT_CONFIG_WITH_CLIENT_ID = "from:@alice:clientConfig:.*\"clientId\".*"; + + private AtKeys keys() throws Exception { + return AtKeys.builder().apkamKeyPair(generateRSAKeyPair()).build(); + } + + @Test + void testCreateContextEnrichesTheConfigWithTheClientIdentity() { + AtCommandExecutorContext context = AtClients.createContext(createAtSign("@alice"), null, null); + + // clientId plus the client-config.properties entries, even though the caller passed no + // config at all — this is what identifies the client to the At Server on the from: + assertThat(context.getConfig(), hasKey("clientId")); + assertThat(context.getConfig(), hasEntry("platform", "Java")); + assertThat(context.getConfig(), hasKey("version")); + } + + @Test + void testCreateContextKeepsCallerSuppliedConfigEntries() { + Map config = Collections.singletonMap("myOwnKey", "myOwnValue"); + + AtCommandExecutorContext context = AtClients.createContext(createAtSign("@alice"), null, config); + + assertThat(context.getConfig(), hasKey("clientId")); + assertThat(context.getConfig(), hasKey("myOwnKey")); + } + + @Test + void testMonitoringOnReadyIdentifiesTheClientInItsFromCommand() throws Exception { + // the stub only answers a from: that carries a clientConfig, so a bare from: fails the run + AtCommandExecutor executor = TestExecutorBuilder.builder() + .stub("from:@alice:clientConfig:.+", "data:challenge") + .stub("pkam:[^{].+", "data:success") + .build(); + AtCommandExecutorContext context = AtClients.createContext(createAtSign("@alice"), keys(), null); + + AtClients.createMonitoringOnReady(context, MonitorOptions.builder().build(), s -> { + }).accept(executor); + + verify(executor, times(1)).sendSync(matches(CLIENT_CONFIG_WITH_CLIENT_ID)); + verify(executor, times(1)).sendSync(matches("pkam:.*")); + } + + @Test + void testMonitoringOnReadyReusesAnAlreadyRetainedChallenge() throws Exception { + AtCommandExecutor executor = TestExecutorBuilder.builder() + .stub("pkam:[^{].+", "data:success") + .build(); + AtCommandExecutorContext context = AtClients.createContext(createAtSign("@alice"), keys(), null); + context.setChallenge("challenge"); + + AtClients.createMonitoringOnReady(context, MonitorOptions.builder().build(), s -> { + }).accept(executor); + + // no from: at all — the challenge the connection already holds is used instead, which is only + // possible because the monitoring path and the executor share one context + verify(executor, times(0)).sendSync(matches("from:.*")); + verify(executor, times(1)).sendSync(matches("pkam:.*")); + assertThat(context.consumeChallenge(), nullValue()); + } + + /** + * A proxy url so that the builder connects directly rather than resolving through the root server, + * pointing at a port nothing is listening on so that the build fails promptly and locally once it + * gets as far as connecting. + */ + private String unservedProxyUrl() throws IOException { + try (ServerSocket socket = new ServerSocket(0)) { + return "proxy:localhost:" + socket.getLocalPort(); + } + } + + @Test + void testCreateAtClientHandsTheExecutorBuilderTheSharedContextAndNothingElse() throws Exception { + String url = unservedProxyUrl(); + AtKeys keys = keys(); + + // the executor builder rejects a context combined with a loose atSign / keys / config, so + // an AtException (rather than an IllegalArgumentException) is what says this passes the + // connection's identity once, as the shared context + assertThrows(AtException.class, () -> AtClients.builder() + .atSign(createAtSign("@alice")) + .keys(keys) + .url(url) + .reconnect(ReconnectStrategy.NONE) + .awaitReadyMillis(500L) + .build()); + } +} diff --git a/at_client/src/test/java/org/atsign/client/impl/AtCommandExecutorsTest.java b/at_client/src/test/java/org/atsign/client/impl/AtCommandExecutorsTest.java new file mode 100644 index 00000000..5546e636 --- /dev/null +++ b/at_client/src/test/java/org/atsign/client/impl/AtCommandExecutorsTest.java @@ -0,0 +1,124 @@ +package org.atsign.client.impl; + +import static org.atsign.client.api.AtSign.createAtSign; +import static org.atsign.client.impl.util.EncryptionUtils.generateRSAKeyPair; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.Collections; + +import org.atsign.client.api.AtCommandExecutorContext; +import org.atsign.client.api.AtKeys; +import org.atsign.client.impl.common.ReconnectStrategy; +import org.atsign.client.impl.exceptions.AtException; +import org.junit.jupiter.api.Test; + +class AtCommandExecutorsTest { + + private static final String CONTEXT_WITH_LOOSE_ARGS = + "both context and one or more of atSign,keys and config set"; + private static final String ATSIGN_NOT_SET = "atSign not set"; + + private AtCommandExecutorContext aliceContext() { + return AtClients.createContext(createAtSign("@alice"), null, null); + } + + private AtKeys keys() throws Exception { + return AtKeys.builder().apkamKeyPair(generateRSAKeyPair()).build(); + } + + /** + * A proxy url so that the builder connects directly rather than resolving through the root server, + * pointing at a port nothing is listening on so that a build which gets as far as connecting fails + * promptly and locally. + */ + private String unservedProxyUrl() throws IOException { + try (ServerSocket socket = new ServerSocket(0)) { + return "proxy:localhost:" + socket.getLocalPort(); + } + } + + @Test + void testContextIsRejectedAlongsideAnAtSign() throws Exception { + String url = unservedProxyUrl(); + AtCommandExecutorContext context = aliceContext(); + + // the context is the connection's whole identity: a second, disagreeing atSign would connect + // to bob's atServer and then authenticate as alice + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> AtCommandExecutors.builder().url(url).context(context) + .atSign(createAtSign("@bob")).build()); + + assertEquals(CONTEXT_WITH_LOOSE_ARGS, ex.getMessage()); + } + + @Test + void testContextIsRejectedAlongsideKeys() throws Exception { + String url = unservedProxyUrl(); + AtCommandExecutorContext context = aliceContext(); + AtKeys keys = keys(); + + // the context carries the keys the onReady sequence authenticates with, so loose keys would + // be silently discarded rather than used + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> AtCommandExecutors.builder().url(url).context(context).keys(keys) + .build()); + + assertEquals(CONTEXT_WITH_LOOSE_ARGS, ex.getMessage()); + } + + @Test + void testContextIsRejectedAlongsideConfig() throws Exception { + String url = unservedProxyUrl(); + AtCommandExecutorContext context = aliceContext(); + + // likewise the context's config is the one the from: carries, so a loose config would be lost + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> AtCommandExecutors.builder() + .url(url) + .context(context) + .config(Collections.singletonMap("myOwnKey", "myOwnValue")) + .build()); + + assertEquals(CONTEXT_WITH_LOOSE_ARGS, ex.getMessage()); + } + + @Test + void testAnAtSignIsRequiredWhenNoContextIsSupplied() throws Exception { + String url = unservedProxyUrl(); + + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> AtCommandExecutors.builder().url(url).build()); + + assertEquals(ATSIGN_NOT_SET, ex.getMessage()); + } + + @Test + void testKeysAreRejectedWithoutAnAtSign() throws Exception { + String url = unservedProxyUrl(); + AtKeys keys = keys(); + + // there is no anonymous connection to fall back to, so keys alone are not a configuration + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> AtCommandExecutors.builder().url(url).keys(keys).build()); + + assertEquals(ATSIGN_NOT_SET, ex.getMessage()); + } + + @Test + void testAContextAloneIsASufficientConfiguration() throws Exception { + String url = unservedProxyUrl(); + AtCommandExecutorContext context = aliceContext(); + + // an AtException (rather than an IllegalArgumentException) means the identity and the endpoint + // were both satisfied from the context alone, and only the connection failed + assertThrows(AtException.class, () -> AtCommandExecutors.builder() + .url(url) + .context(context) + .reconnect(ReconnectStrategy.NONE) + .awaitReadyMillis(500L) + .build()); + } +} diff --git a/at_client/src/test/java/org/atsign/client/impl/cli/AbstractCliTest.java b/at_client/src/test/java/org/atsign/client/impl/cli/AbstractCliTest.java new file mode 100644 index 00000000..4f440f22 --- /dev/null +++ b/at_client/src/test/java/org/atsign/client/impl/cli/AbstractCliTest.java @@ -0,0 +1,48 @@ +package org.atsign.client.impl.cli; + +import static org.atsign.client.api.AtSign.createAtSign; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; +import java.net.ServerSocket; + +import org.atsign.client.api.AtCommandExecutor; +import org.atsign.client.api.AtCommandExecutorContext; +import org.atsign.client.impl.exceptions.AtException; +import org.junit.jupiter.api.Test; + +class AbstractCliTest { + + private static class TestCli extends AbstractCli { + @Override + protected TestCli self() { + return this; + } + + AtCommandExecutor connectSendingFrom(AtCommandExecutorContext context) throws AtException { + return createConnectionSendingFrom(context); + } + } + + /** + * A proxy url so that the connection is attempted directly rather than resolved through the root + * server, pointing at a port nothing is listening on so that it fails promptly and locally. + */ + private String unservedProxyUrl() throws IOException { + try (ServerSocket socket = new ServerSocket(0)) { + return "proxy:localhost:" + socket.getLocalPort(); + } + } + + @Test + void testCreateConnectionSendingFromPassesOnlyTheSharedContext() throws Exception { + TestCli cli = new TestCli().setAtSign(createAtSign("@alice")); + cli.setRootUrl(unservedProxyUrl()); + AtCommandExecutorContext context = cli.newConnectionContext(null); + + // the executor builder rejects a context combined with a loose atSign / keys / config, so an + // AtException (rather than an IllegalArgumentException) is what says the onboarding flow's + // context reaches the connection as the one identity it is built from + assertThrows(AtException.class, () -> cli.connectSendingFrom(context)); + } +} diff --git a/at_client/src/test/java/org/atsign/client/impl/cli/ActivateTest.java b/at_client/src/test/java/org/atsign/client/impl/cli/ActivateTest.java new file mode 100644 index 00000000..d583d89f --- /dev/null +++ b/at_client/src/test/java/org/atsign/client/impl/cli/ActivateTest.java @@ -0,0 +1,110 @@ +package org.atsign.client.impl.cli; + +import org.atsign.client.api.AtCommandExecutor; +import org.atsign.client.api.AtKeys; +import org.atsign.client.impl.commands.TestExecutorBuilder; +import org.atsign.client.impl.exceptions.AtUnauthenticatedException; +import org.atsign.client.impl.util.KeysUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.stream.Collectors; + +import static java.lang.String.format; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.atsign.client.api.AtSign.createAtSign; +import static org.atsign.client.impl.commands.EnrollCommandsTest.*; +import static org.atsign.client.impl.common.EnrollmentId.createEnrollmentId; +import static org.atsign.client.impl.util.EncryptionUtils.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ActivateTest { + + @TempDir + File keysDir; + private AtKeys keys; + private File keysFile; + private String selfKey; + private String privateKey; + + @BeforeEach + public void setUp() throws Exception { + keys = AtKeys.builder() + .apkamKeyPair(generateRSAKeyPair()) + .apkamSymmetricKey(generateAESKeyBase64()) + .selfEncryptKey(generateAESKeyBase64()) + .enrollmentId(createEnrollmentId("12345")) + .build(); + keysFile = new File(keysDir, "@alice_key.atKeys"); + KeysUtils.saveKeys(keys, keysFile); + + selfKey = aesEncryptToBase64(AES_KEY, keys.getApkamSymmetricKey(), IV); + privateKey = aesEncryptToBase64(RSA_KEY, keys.getApkamSymmetricKey(), IV); + } + + @Test + void testCompleteRetryEventuallySucceeds() throws Exception { + + List commands = new ArrayList<>(); + AtCommandExecutor executor = TestExecutorBuilder.builder() + .stub("from:@alice:clientConfig:.+", "data:challenge") + .stub("pkam:[^{].+", createPkamResponse(4)) + .stub("keys:get:keyName:12345.default_self_enc_key.__manage@alice", createGetKeyResponse(selfKey)) + .stub("keys:get:keyName:12345.default_enc_private_key.__manage@alice", createGetKeyResponse(privateKey)) + .record(commands::add) + .build(); + + Activate activate = new Activate().setAtSign(createAtSign("@alice")); + activate.setKeysFile(keysFile.getPath()); + + activate.complete(executor, 3, 0, MILLISECONDS); + + List fromCommands = commands.stream().filter(x -> x.startsWith("from:")).collect(Collectors.toList()); + assertThat(fromCommands.size(), equalTo(4)); + assertThat(fromCommands.stream().distinct().count(), equalTo(1L)); + + AtKeys newKeys = KeysUtils.loadKeys(keysFile); + assertThat(newKeys.getEncryptPrivateKey(), notNullValue()); + } + + @Test + void testCompleteRetryEventuallyGivesUp() throws Exception { + + AtCommandExecutor executor = TestExecutorBuilder.builder() + .stub("from:@alice:clientConfig:.+", "data:challenge") + .stub("pkam:[^{].+", createPkamResponse(4)) + .stub("keys:get:keyName:12345.default_self_enc_key.__manage@alice", createGetKeyResponse(selfKey)) + .stub("keys:get:keyName:12345.default_enc_private_key.__manage@alice", createGetKeyResponse(privateKey)) + .build(); + + Activate activate = new Activate().setAtSign(createAtSign("@alice")); + activate.setKeysFile(keysFile.getPath()); + + assertThrows(AtUnauthenticatedException.class, () -> activate.complete(executor, 2, 0, MILLISECONDS)); + } + + private static Function createPkamResponse(int after) { + AtomicInteger invocations = new AtomicInteger(); + return x -> { + if (invocations.incrementAndGet() == after) { + return "data:success"; + } else { + return "error:AT0401:enrollment_id:12345 is pending"; + } + }; + } + + private static Function createGetKeyResponse(String key) { + return x -> format("data:{\"value\":\"%s\",\"iv\":\"%s\"}", key, IV); + } +} diff --git a/at_client/src/test/java/org/atsign/client/impl/commands/AuthenticationCommandsTest.java b/at_client/src/test/java/org/atsign/client/impl/commands/AuthenticationCommandsTest.java index 65c11cb4..0ace6c5c 100644 --- a/at_client/src/test/java/org/atsign/client/impl/commands/AuthenticationCommandsTest.java +++ b/at_client/src/test/java/org/atsign/client/impl/commands/AuthenticationCommandsTest.java @@ -9,6 +9,7 @@ import java.util.Collections; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import static org.atsign.client.api.AtSign.createAtSign; import static org.atsign.client.impl.common.EnrollmentId.createEnrollmentId; @@ -29,9 +30,8 @@ public void testAuthenticateWithCramDoesNotThrowException() throws Exception { .stub("cram:7e91508d5.+", "data:success") .build(); - AuthenticationCommands.authenticateWithCram(executor, - new AtCommandExecutorContext(createAtSign("@alice"), null, null), - "secret"); + AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), null); + AuthenticationCommands.authenticateWithCram(executor, context, "secret"); } @Test @@ -41,13 +41,9 @@ public void testAuthenticateWithCramFailThrowsExpectedException() throws Excepti .stub("cram:.+", "error:AT0401:deliberate") .build(); + AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), null); Exception ex = assertThrows(AtUnauthenticatedException.class, - () -> AuthenticationCommands.authenticateWithCram( - executor, - new AtCommandExecutorContext( - createAtSign("@alice"), null, - null), - "secret")); + () -> AuthenticationCommands.authenticateWithCram(executor, context, "secret")); assertThat(ex.getMessage(), containsString("deliberate")); } @@ -59,22 +55,24 @@ public void testAuthenticateWithPkamDoesNotThrowException() throws Exception { .stub("pkam:[^{].+", "data:success") .build(); - AuthenticationCommands.authenticateWithPkam(executor, createAtSign("@alice"), keys); + AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), keys); + AuthenticationCommands.authenticateWithPkam(executor, context); } @Test - public void testAuthenticateWithApkamWithEnrollmentId() throws Exception { + public void testAuthenticateWithApkamWithEnrollmentIdDoesNotThrowException() throws Exception { AtKeys keys = AtKeys.builder().apkamKeyPair(generateRSAKeyPair()).enrollmentId(createEnrollmentId("12345")).build(); AtCommandExecutor executor = TestExecutorBuilder.builder() .stub("from:@alice", "data:challenge") .stub("pkam:signingAlgo:rsa2048:hashingAlgo:sha256:enrollmentId:12345:.+", "data:success") .build(); - AuthenticationCommands.authenticateWithPkam(executor, createAtSign("@alice"), keys); + AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), keys); + AuthenticationCommands.authenticateWithPkam(executor, context); } @Test - public void testAuthenticateWithApkamWithConfig() throws Exception { + public void testAuthenticateWithApkamWithConfigDoesNotThrowException() throws Exception { AtKeys keys = AtKeys.builder().apkamKeyPair(generateRSAKeyPair()).enrollmentId(createEnrollmentId("12345")).build(); AtCommandExecutor executor = TestExecutorBuilder.builder() .stub("from:@alice:clientConfig:.+", "data:challenge") @@ -82,7 +80,8 @@ public void testAuthenticateWithApkamWithConfig() throws Exception { .build(); Map config = Collections.singletonMap("clientVersion", "1.2.3"); - AuthenticationCommands.authenticateWithPkam(executor, createAtSign("@alice"), keys, config); + AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), keys, config); + AuthenticationCommands.authenticateWithPkam(executor, context); } @Test @@ -92,10 +91,10 @@ public void testAuthenticateWithApkamFailThrowsExpectedException() throws Except .stub("from:@alice", "data:challenge") .stub("pkam:.+", "error:AT0401:deliberate") .build(); + AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), keys); Exception ex = assertThrows(AtUnauthenticatedException.class, - () -> AuthenticationCommands.authenticateWithPkam(executor, createAtSign("@alice"), - keys)); + () -> AuthenticationCommands.authenticateWithPkam(executor, context)); assertThat(ex.getMessage(), containsString("deliberate")); } @@ -106,10 +105,10 @@ public void testPkamAuthenticatorThrowsOnReadyException() throws Exception { .stub("from:@alice", "data:challenge") .stub("pkam:.+", "error:AT0401:deliberate") .build(); + AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), keys); Exception ex = assertThrows(Exception.class, - () -> AuthenticationCommands.pkamAuthenticator(createAtSign("@alice"), keys, null) - .accept(executor)); + () -> AuthenticationCommands.pkamAuthenticator(context).accept(executor)); assertThat(ex, instanceOf(AtOnReadyException.class)); assertThat(ex.getMessage(), containsString("deliberate")); } @@ -119,7 +118,7 @@ public void testSendFromIssuesFromAndRetainsChallenge() throws Exception { AtCommandExecutor executor = TestExecutorBuilder.builder() .stub("from:@alice", "data:challenge") .build(); - AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), null, null); + AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), null); AuthenticationCommands.sendFrom(context).accept(executor); @@ -134,7 +133,7 @@ public void testPkamAuthenticatorReusesTheInitialFromChallenge() throws Exceptio .stub("from:@alice", "data:challenge") .stub("pkam:[^{].+", "data:success") .build(); - AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), keys, null); + AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), keys); // the from: sender runs first (as wired by createOnReady), then PKAM reuses its challenge AuthenticationCommands.sendFrom(context).accept(executor); @@ -146,6 +145,25 @@ public void testPkamAuthenticatorReusesTheInitialFromChallenge() throws Exceptio assertThat(context.consumeChallenge(), is(nullValue())); } + @Test + public void testAuthenticateWithCramReusesTheInitialFromChallenge() throws Exception { + AtCommandExecutor executor = TestExecutorBuilder.builder() + .stub("from:@alice", "data:challenge") + .stub("cram:7e91508d5.+", "data:success") + .build(); + + AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), null); + + // the from: sender runs first (as wired by createOnReady), then CRAM reuses its challenge + AuthenticationCommands.sendFrom(context).accept(executor); + AuthenticationCommands.authenticateWithCram(executor, context, "secret"); + + // exactly one from: for the whole connection — CRAM did not issue a second one + verify(executor, times(1)).sendSync(matches("from:.*")); + verify(executor, times(1)).sendSync(matches("cram:.*")); + assertThat(context.consumeChallenge(), is(nullValue())); + } + @Test public void testPkamAuthenticatorIssuesItsOwnFromWhenNoChallengeRetained() throws Exception { AtKeys keys = AtKeys.builder().apkamKeyPair(generateRSAKeyPair()).build(); @@ -153,7 +171,7 @@ public void testPkamAuthenticatorIssuesItsOwnFromWhenNoChallengeRetained() throw .stub("from:@alice", "data:challenge") .stub("pkam:[^{].+", "data:success") .build(); - AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), keys, null); + AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), keys); // no prior sendFrom: the authenticator must issue its own from: to obtain a challenge AuthenticationCommands.pkamAuthenticator(context).accept(executor); @@ -164,7 +182,7 @@ public void testPkamAuthenticatorIssuesItsOwnFromWhenNoChallengeRetained() throw @Test public void testRetainedChallengeIsConsumedAtMostOnce() { - AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), null, null); + AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), null); context.setChallenge("challenge"); // single-use: the first consumer gets it, a second (e.g. a further auth on the same connection) @@ -172,4 +190,18 @@ public void testRetainedChallengeIsConsumedAtMostOnce() { assertThat(context.consumeChallenge(), is("challenge")); assertThat(context.consumeChallenge(), is(nullValue())); } + + @Test + public void testFromReplacesUnconsumedChallenge() throws Exception { + AtomicInteger invocations = new AtomicInteger(); + AtCommandExecutor executor = TestExecutorBuilder.builder() + .stub("from:@alice", m -> "data:challenge" + invocations.incrementAndGet()) + .build(); + AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), null); + + AuthenticationCommands.sendFrom(context).accept(executor); + AuthenticationCommands.sendFrom(context).accept(executor); + + assertThat(context.consumeChallenge(), equalTo("challenge2")); + } } diff --git a/at_client/src/test/java/org/atsign/client/impl/commands/EnrollCommandsTest.java b/at_client/src/test/java/org/atsign/client/impl/commands/EnrollCommandsTest.java index f3f4a8d6..92a5ed83 100644 --- a/at_client/src/test/java/org/atsign/client/impl/commands/EnrollCommandsTest.java +++ b/at_client/src/test/java/org/atsign/client/impl/commands/EnrollCommandsTest.java @@ -66,9 +66,10 @@ public void testOnboardThrowsExceptionIfSigningPublicKeyIsMissing() throws Excep .stub("scan", "data:[\"signing_publickey@gary\"]") .build(); + AtCommandExecutorContext context = new AtCommandExecutorContext(atSign, keys); + Exception ex = assertThrows(Exception.class, - () -> EnrollCommands.onboard(executor, new AtCommandExecutorContext(atSign, keys, null), - "secret", "app", "device", false)); + () -> EnrollCommands.onboard(executor, context, "secret", "app", "device", false)); assertThat(ex.getMessage(), containsString("not connected to the atsign's at server")); } @@ -107,8 +108,9 @@ public void testOnboard() throws Exception { .stub("update:public:publickey@alice .+", "data:1") .build(); - AtKeys newKeys = EnrollCommands.onboard(executor, new AtCommandExecutorContext(atSign, keys, null), "secret", - "app", "device", false); + AtCommandExecutorContext context = new AtCommandExecutorContext(atSign, keys); + + AtKeys newKeys = EnrollCommands.onboard(executor, context, "secret", "app", "device", false); assertThat(newKeys, is(not(sameInstance(keys)))); assertThat(newKeys.getEnrollmentId(), equalTo(createEnrollmentId("904dcbf7"))); @@ -123,7 +125,7 @@ public void testOnboardReusesTheConnectionFromChallengeForCram() throws Exceptio .build(); // simulate the connection having issued from: on connect (sendFrom) and retained the challenge - AtCommandExecutorContext context = new AtCommandExecutorContext(atSign, keys, null); + AtCommandExecutorContext context = new AtCommandExecutorContext(atSign, keys); context.setChallenge("challenge"); AtCommandExecutor executor = TestExecutorBuilder.builder() @@ -157,7 +159,9 @@ public void testEnroll() throws Exception { .stub("enroll:request\\{.+}", "data:{\"enrollmentId\":\"759acb09\",\"status\":\"pending\"}") .build(); - AtKeys newKeys = EnrollCommands.enroll(executor, atSign, keys, "OTP123", "app", "device", singletonMap("ns", "rw")); + AtCommandExecutorContext context = new AtCommandExecutorContext(atSign, keys); + + AtKeys newKeys = EnrollCommands.enroll(executor, context, "OTP123", "app", "device", singletonMap("ns", "rw")); assertThat(newKeys, is(not(sameInstance(keys)))); assertThat(newKeys.getEnrollmentId(), equalTo(createEnrollmentId("759acb09"))); @@ -186,7 +190,9 @@ public void testComplete() throws Exception { .stub("keys:get:keyName:12345.default_enc_private_key.__manage@alice", privateEncryptKeysGetResponse) .build(); - AtKeys newKeys = EnrollCommands.complete(executor, atSign, keys); + AtCommandExecutorContext context = new AtCommandExecutorContext(atSign, keys); + + AtKeys newKeys = EnrollCommands.complete(executor, context); assertThat(newKeys, is(not(sameInstance(keys)))); assertThat(newKeys.getEncryptPrivateKey(), notNullValue()); @@ -210,7 +216,10 @@ public void testApprove() throws Exception { "data:{\"status\":\"approved\",\"enrollmentId\":\"12345\"}") .build(); - EnrollCommands.approve(executor, keys, createEnrollmentId("12345")); + // approve reads only the keys from the context, but a context always carries an atSign + AtCommandExecutorContext context = new AtCommandExecutorContext(createAtSign("@alice"), keys); + + EnrollCommands.approve(executor, context, createEnrollmentId("12345")); } @Test diff --git a/at_client/src/test/java/org/atsign/client/impl/commands/NotificationsTest.java b/at_client/src/test/java/org/atsign/client/impl/commands/NotificationsTest.java index 53bf64f2..26db1fde 100644 --- a/at_client/src/test/java/org/atsign/client/impl/commands/NotificationsTest.java +++ b/at_client/src/test/java/org/atsign/client/impl/commands/NotificationsTest.java @@ -13,6 +13,7 @@ import java.util.function.Consumer; import org.atsign.client.api.AtCommandExecutor; +import org.atsign.client.api.AtCommandExecutorContext; import org.atsign.client.api.AtEvents; import org.atsign.client.api.AtKeys; import org.atsign.client.api.AtSign; @@ -29,11 +30,12 @@ class NotificationsTest { void testMonitorSendExpectedCommands() throws Exception { AtSign atSign = createAtSign("colin"); AtKeys keys = AtKeys.builder().apkamKeyPair(generateRSAKeyPair()).build(); + AtCommandExecutorContext context = new AtCommandExecutorContext(atSign, keys); AtCommandExecutor executor = mock(AtCommandExecutor.class); stubAuthentication(executor, atSign); Consumer consumer = mock(Consumer.class); - Notifications.monitor(executor, atSign, null, keys, consumer); + Notifications.monitor(executor, context, null, consumer); verify(executor).sendSync(eq("monitor"), eq(consumer)); } @@ -42,6 +44,7 @@ void testMonitorSendExpectedCommands() throws Exception { void testMonitorWrapsConsumer() throws Exception { AtSign atSign = createAtSign("colin"); AtKeys keys = AtKeys.builder().apkamKeyPair(generateRSAKeyPair()).build(); + AtCommandExecutorContext context = new AtCommandExecutorContext(atSign, keys); AtCommandExecutor executor = mock(AtCommandExecutor.class); stubAuthentication(executor, atSign); Consumer consumer = mock(Consumer.class); @@ -49,8 +52,8 @@ void testMonitorWrapsConsumer() throws Exception { throw new AtTimeoutException("deliberate"); }).when(executor).sendSync(eq("monitor"), Mockito.any(Consumer.class)); - Exception ex = - assertThrows(Exception.class, () -> Notifications.monitor(atSign, null, keys, null, consumer).accept(executor)); + Exception ex = assertThrows(Exception.class, + () -> Notifications.monitor(context, null, consumer).accept(executor)); assertThat(ex, instanceOf(AtOnReadyException.class)); } diff --git a/at_client/src/test/java/org/atsign/client/impl/commands/PublicKeyCommandsTest.java b/at_client/src/test/java/org/atsign/client/impl/commands/PublicKeyCommandsTest.java index f7c4bd95..9b105646 100644 --- a/at_client/src/test/java/org/atsign/client/impl/commands/PublicKeyCommandsTest.java +++ b/at_client/src/test/java/org/atsign/client/impl/commands/PublicKeyCommandsTest.java @@ -10,6 +10,7 @@ import org.atsign.client.api.AtClient.GetRequestOptions; import org.atsign.client.api.AtCommandExecutor; +import org.atsign.client.api.AtCommandExecutorContext; import org.atsign.client.api.AtKeys; import org.atsign.client.api.AtSign; import org.atsign.client.api.Keys; @@ -25,6 +26,10 @@ class PublicKeyCommandsTest { private AtKeys keys; private AtSign atSign; + // the key is shared by @gary, so @gary reads it with llookup and anyone else with plookup + private AtCommandExecutorContext contextGary; + private AtCommandExecutorContext contextColin; + @BeforeEach public void setup() throws Exception { keys = AtKeys.builder() @@ -35,6 +40,8 @@ public void setup() throws Exception { .sharedBy(atSign) .name("test") .build(); + contextGary = new AtCommandExecutorContext(atSign, keys); + contextColin = new AtCommandExecutorContext(createAtSign("colin"), null); } @Test @@ -43,7 +50,7 @@ void testGetSharedByMe() throws Exception { .stubLookupResponse("llookup:all:public:test@gary", "public:test@gary", "hello world") .build(); - String actual = PublicKeyCommands.get(executor, createAtSign("gary"), key, null); + String actual = PublicKeyCommands.get(executor, contextGary, key, null); assertThat(actual, equalTo("hello world")); } @@ -54,7 +61,7 @@ void testGetCachedSharedByMe() throws Exception { .stubLookupResponse("llookup:all:public:test@gary", "cached:public:test@gary", "hello world") .build(); - PublicKeyCommands.get(executor, createAtSign("gary"), key, null); + PublicKeyCommands.get(executor, contextGary, key, null); assertThat(key.metadata().isCached(), is(true)); } @@ -65,7 +72,7 @@ void testGetSharedByMeNoSuchKey() throws Exception { .stub("llookup:all:public:test@gary", "error:AT0015:deliberate") .build(); - assertThrows(AtKeyNotFoundException.class, () -> PublicKeyCommands.get(executor, createAtSign("gary"), key, null)); + assertThrows(AtKeyNotFoundException.class, () -> PublicKeyCommands.get(executor, contextGary, key, null)); } @Test @@ -74,7 +81,7 @@ void testGetSharedByMeExecutionException() throws Exception { .stub("llookup:all:public:test@gary", new ExecutionException("deliberate", null)) .build(); - assertThrows(RuntimeException.class, () -> PublicKeyCommands.get(executor, createAtSign("gary"), key, null)); + assertThrows(RuntimeException.class, () -> PublicKeyCommands.get(executor, contextGary, key, null)); } @Test @@ -83,7 +90,7 @@ void testGetSharedByOther() throws Exception { .stubLookupResponse("plookup:all:test@gary", "public:test@gary", "hello world") .build(); - String actual = PublicKeyCommands.get(executor, createAtSign("colin"), key, null); + String actual = PublicKeyCommands.get(executor, contextColin, key, null); assertThat(actual, equalTo("hello world")); } @@ -94,7 +101,7 @@ void testGetCachedSharedByOther() throws Exception { .stubLookupResponse("plookup:all:test@gary", "cached:public:test@gary", "hello world") .build(); - PublicKeyCommands.get(executor, createAtSign("colin"), key, null); + PublicKeyCommands.get(executor, contextColin, key, null); assertThat(key.metadata().isCached(), is(true)); } @@ -106,20 +113,18 @@ void testGetSharedByOtherBypassCache() throws Exception { .build(); GetRequestOptions options = GetRequestOptions.builder().bypassCache(true).build(); - String actual = PublicKeyCommands.get(executor, createAtSign("colin"), key, options); + String actual = PublicKeyCommands.get(executor, contextColin, key, options); assertThat(actual, equalTo("hello world")); } - - @Test void testGetSharedByOtherNoSuchKey() throws Exception { AtCommandExecutor executor = TestExecutorBuilder.builder() .stub("plookup:all:test@gary", "error:AT0015:deliberate") .build(); - assertThrows(AtKeyNotFoundException.class, () -> PublicKeyCommands.get(executor, createAtSign("colin"), key, null)); + assertThrows(AtKeyNotFoundException.class, () -> PublicKeyCommands.get(executor, contextColin, key, null)); } @Test @@ -128,7 +133,7 @@ void testGetSharedByOtherExecutionException() throws Exception { .stubExecutionException("plookup:all:test@gary") .build(); - assertThrows(RuntimeException.class, () -> PublicKeyCommands.get(executor, createAtSign("colin"), key, null)); + assertThrows(RuntimeException.class, () -> PublicKeyCommands.get(executor, contextColin, key, null)); } @Test @@ -137,7 +142,7 @@ void testPutSendsExpectedCommands() throws Exception { .stub("update:dataSignature:.+:isEncrypted:false:public:test@gary hello world", "data:123") .build(); - PublicKeyCommands.put(executor, createAtSign("gary"), keys, key, "hello world"); + PublicKeyCommands.put(executor, contextGary, key, "hello world"); } @Test @@ -146,12 +151,8 @@ void testPutThrowExceptionIfCommandFails() throws Exception { .stub("update:dataSignature:.+:isEncrypted:false:public:test@gary hello world", "error:AT0001:deliberate") .build(); - AtKeys keys = AtKeys.builder() - .encryptKeyPair(EncryptionUtils.generateRSAKeyPair()) - .build(); - assertThrows(AtServerRuntimeException.class, - () -> PublicKeyCommands.put(executor, createAtSign("gary"), keys, key, "hello world")); + () -> PublicKeyCommands.put(executor, contextGary, key, "hello world")); } @Test @@ -160,11 +161,7 @@ void testPutExecutionException() throws Exception { .stubExecutionException("update:dataSignature:.+:isEncrypted:false:public:test@gary hello world") .build(); - AtKeys keys = AtKeys.builder() - .encryptKeyPair(EncryptionUtils.generateRSAKeyPair()) - .build(); - assertThrows(RuntimeException.class, - () -> PublicKeyCommands.put(executor, createAtSign("gary"), keys, key, "hello world")); + () -> PublicKeyCommands.put(executor, contextGary, key, "hello world")); } } diff --git a/at_client/src/test/java/org/atsign/client/impl/commands/SelfKeyCommandsTest.java b/at_client/src/test/java/org/atsign/client/impl/commands/SelfKeyCommandsTest.java index 7d9fb61c..c5ebb611 100644 --- a/at_client/src/test/java/org/atsign/client/impl/commands/SelfKeyCommandsTest.java +++ b/at_client/src/test/java/org/atsign/client/impl/commands/SelfKeyCommandsTest.java @@ -10,6 +10,7 @@ import org.atsign.client.api.AtKeys; import org.atsign.client.api.AtCommandExecutor; +import org.atsign.client.api.AtCommandExecutorContext; import org.atsign.client.api.AtSign; import org.atsign.client.api.Keys; import org.atsign.client.impl.exceptions.AtServerRuntimeException; @@ -22,6 +23,7 @@ class SelfKeyCommandsTest { private AtKeys keys; private AtSign atSign; + private AtCommandExecutorContext context; @BeforeEach public void setup() throws Exception { @@ -30,6 +32,7 @@ public void setup() throws Exception { .encryptKeyPair(generateRSAKeyPair()) .build(); atSign = createAtSign("gary"); + context = new AtCommandExecutorContext(atSign, keys); key = Keys.selfKeyBuilder() .sharedBy(atSign) .name("test") @@ -45,7 +48,7 @@ void testGet() throws Exception { .stubLookupResponse("llookup:all:test@gary", "test@gary", encrypted, "ivNonce", iv) .build(); - String actual = SelfKeyCommands.get(executor, atSign, keys, key); + String actual = SelfKeyCommands.get(executor, context, key); assertThat(actual, equalTo("hello me")); } @@ -56,7 +59,7 @@ void testGetException() throws Exception { .stub("llookup:all:test@gary", "error:AT0001:deliberate") .build(); - assertThrows(AtServerRuntimeException.class, () -> SelfKeyCommands.get(executor, atSign, keys, key)); + assertThrows(AtServerRuntimeException.class, () -> SelfKeyCommands.get(executor, context, key)); } @Test @@ -65,7 +68,7 @@ void testGetExecutionException() throws Exception { .stubExecutionException("llookup:all:test@gary") .build(); - assertThrows(RuntimeException.class, () -> SelfKeyCommands.get(executor, atSign, keys, key)); + assertThrows(RuntimeException.class, () -> SelfKeyCommands.get(executor, context, key)); } @Test @@ -74,7 +77,7 @@ void testPut() throws Exception { .stub("update:dataSignature:.+:isEncrypted:true:ivNonce:.+:test@gary .+", "data:123") .build(); - SelfKeyCommands.put(executor, atSign, keys, key, "hello world"); + SelfKeyCommands.put(executor, context, key, "hello world"); verify(executor).sendSync(argThat(s -> !s.contains("hello world"))); } @@ -84,7 +87,8 @@ void testPutAtException() throws Exception { .stub("update:dataSignature:.+:isEncrypted:true:ivNonce:.+:test@gary .+", "error:AT0001:deliberate") .build(); - assertThrows(AtServerRuntimeException.class, () -> SelfKeyCommands.put(executor, atSign, keys, key, "hello world")); + assertThrows(AtServerRuntimeException.class, + () -> SelfKeyCommands.put(executor, context, key, "hello world")); } @Test @@ -93,7 +97,8 @@ void testPutExecutionException() throws Exception { .stubExecutionException("update:dataSignature:.+:isEncrypted:true:ivNonce:.+:test@gary .+") .build(); - assertThrows(RuntimeException.class, () -> SelfKeyCommands.put(executor, atSign, keys, key, "hello world")); + assertThrows(RuntimeException.class, + () -> SelfKeyCommands.put(executor, context, key, "hello world")); } } diff --git a/at_client/src/test/java/org/atsign/client/impl/commands/SharedKeyCommandsTest.java b/at_client/src/test/java/org/atsign/client/impl/commands/SharedKeyCommandsTest.java index 0c85db67..f62a6410 100644 --- a/at_client/src/test/java/org/atsign/client/impl/commands/SharedKeyCommandsTest.java +++ b/at_client/src/test/java/org/atsign/client/impl/commands/SharedKeyCommandsTest.java @@ -10,6 +10,7 @@ import static org.mockito.Mockito.verify; import org.atsign.client.api.AtCommandExecutor; +import org.atsign.client.api.AtCommandExecutorContext; import org.atsign.client.api.AtKeys; import org.atsign.client.api.Keys; import org.atsign.client.api.Metadata; @@ -25,6 +26,10 @@ class SharedKeyCommandsTest { private AtKeys keys; + // the key is shared by @gary with @colin, so these are the two perspectives it can be read from + private AtCommandExecutorContext contextGary; + private AtCommandExecutorContext contextColin; + @BeforeEach public void setup() throws Exception { keys = AtKeys.builder() @@ -36,6 +41,8 @@ public void setup() throws Exception { .sharedWith(createAtSign("colin")) .name("test") .build(); + contextGary = new AtCommandExecutorContext(createAtSign("gary"), keys); + contextColin = new AtCommandExecutorContext(createAtSign("colin"), keys); } @Test @@ -48,7 +55,7 @@ void testGetSharedByMe() throws Exception { .stubLookupResponse("llookup:all:@colin:test@gary", "@colin:test@gary", encrypted, "ivNonce", iv) .build(); - String actual = SharedKeyCommands.get(executor, createAtSign("gary"), keys, key); + String actual = SharedKeyCommands.get(executor, contextGary, key); assertThat(actual, equalTo("hello colin")); } @@ -57,9 +64,10 @@ void testGetSharedByMe() throws Exception { void testGetNotSharedByMeOrWithMeThrowsException() throws Exception { AtCommandExecutor executor = TestExecutorBuilder.builder() .build(); + AtCommandExecutorContext contextAlice = new AtCommandExecutorContext(createAtSign("alice"), keys); RuntimeException ex = - assertThrows(RuntimeException.class, () -> SharedKeyCommands.get(executor, createAtSign("alice"), keys, key)); + assertThrows(RuntimeException.class, () -> SharedKeyCommands.get(executor, contextAlice, key)); assertThat(ex.getMessage(), containsString("@alice is neither the sharedBy or sharedWith of @colin:test@gary")); } @@ -73,7 +81,7 @@ void testGetSharedByMeCachedKey() throws Exception { .stubLookupResponse("llookup:all:@colin:test@gary", "@colin:test@gary", encrypted, "ivNonce", iv) .build(); - String actual = SharedKeyCommands.get(executor, createAtSign("gary"), keys, key); + String actual = SharedKeyCommands.get(executor, contextGary, key); assertThat(actual, equalTo("hello colin")); } @@ -86,8 +94,7 @@ void testGetSharedByMeServerException() throws Exception { .stub("llookup:all:@colin:test@gary", "error:AT0001:deliberate") .build(); - assertThrows(AtServerRuntimeException.class, - () -> SharedKeyCommands.get(executor, createAtSign("gary"), keys, key)); + assertThrows(AtServerRuntimeException.class, () -> SharedKeyCommands.get(executor, contextGary, key)); } @Test @@ -98,7 +105,7 @@ void testGetSharedByMeExecutionException() throws Exception { .stubExecutionException("llookup:all:@colin:test@gary") .build(); - assertThrows(RuntimeException.class, () -> SharedKeyCommands.get(executor, createAtSign("gary"), keys, key)); + assertThrows(RuntimeException.class, () -> SharedKeyCommands.get(executor, contextGary, key)); } @Test @@ -116,7 +123,7 @@ void getGetSharedByOther() throws Exception { "ivNonce", iv, "sharedKeyEnc", sharedKeyEnc, "pubKeyHash", hash) .build(); - String actual = SharedKeyCommands.get(executor, createAtSign("colin"), keys, key); + String actual = SharedKeyCommands.get(executor, contextColin, key); assertThat(actual, equalTo("hello colin")); } @@ -132,7 +139,7 @@ void getGetSharedByOtherBackwardCompatibilityCase() throws Exception { .stub("lookup:shared_key@gary", "data:" + sharedKeyEnc) .build(); - String actual = SharedKeyCommands.get(executor, createAtSign("colin"), keys, key); + String actual = SharedKeyCommands.get(executor, contextColin, key); assertThat(actual, equalTo("hello colin")); } @@ -152,8 +159,7 @@ void getGetSharedByOtherThrowsExceptionForPubKeyHashMismatch() throws Exception "ivNonce", iv, "sharedKeyEnc", sharedKeyEnc, "pubKeyHash", hash) .build(); - assertThrows(AtPublicKeyChangeException.class, - () -> SharedKeyCommands.get(executor, createAtSign("colin"), keys, key)); + assertThrows(AtPublicKeyChangeException.class, () -> SharedKeyCommands.get(executor, contextColin, key)); } @Test @@ -164,7 +170,7 @@ void testPutWhenSharedKeyAlreadyExists() throws Exception { .stub("update:isEncrypted:true:ivNonce:.+:@colin:test@gary .+", "data:123") .build(); - SharedKeyCommands.put(executor, createAtSign("gary"), keys, key, "hello colin"); + SharedKeyCommands.put(executor, contextGary, key, "hello colin"); verify(executor).sendSync(argThat(s -> s.contains("update:") && !s.contains("hello colin"))); } @@ -178,7 +184,7 @@ void testPutWhenSharedKeyDoesNotAlreadyExists() throws Exception { .stub("update:isEncrypted:true:sharedKeyEnc:.+:ivNonce:.+:@colin:test@gary .+", "data:3") .build(); - SharedKeyCommands.put(executor, createAtSign("gary"), keys, key, "hello colin"); + SharedKeyCommands.put(executor, contextGary, key, "hello colin"); verify(executor).sendSync(argThat(s -> s.contains("update:") && s.contains(":pubKeyHash:"))); verify(executor).sendSync(argThat(s -> s.contains("update:") && s.contains(":pubKeyCS:"))); diff --git a/at_client/src/test/java/org/atsign/client/impl/commands/TestExecutorBuilder.java b/at_client/src/test/java/org/atsign/client/impl/commands/TestExecutorBuilder.java index 0f224e69..ddea674d 100644 --- a/at_client/src/test/java/org/atsign/client/impl/commands/TestExecutorBuilder.java +++ b/at_client/src/test/java/org/atsign/client/impl/commands/TestExecutorBuilder.java @@ -8,6 +8,7 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -19,12 +20,19 @@ public class TestExecutorBuilder { - private Map mapping = new LinkedHashMap<>(); + private final Map mapping = new LinkedHashMap<>(); + private Consumer commandConsumer = c -> { + }; public static TestExecutorBuilder builder() { return new TestExecutorBuilder(); } + public TestExecutorBuilder record(Consumer consumer) { + this.commandConsumer = consumer; + return this; + } + public TestExecutorBuilder stub(String command, String response) { return stub(Pattern.compile(command), response); } @@ -67,6 +75,7 @@ public AtCommandExecutor build() throws ExecutionException, InterruptedException for (Map.Entry entry : mapping.entrySet()) { Matcher matcher = entry.getKey().matcher(command); if (matcher.matches()) { + commandConsumer.accept(command); if (entry.getValue() instanceof Throwable) { throw (Throwable) entry.getValue(); } else if (entry.getValue() instanceof Function) { diff --git a/at_client/src/test/java/org/atsign/cucumber/steps/ActivateSteps.java b/at_client/src/test/java/org/atsign/cucumber/steps/ActivateSteps.java index b667e47d..7b0328cd 100644 --- a/at_client/src/test/java/org/atsign/cucumber/steps/ActivateSteps.java +++ b/at_client/src/test/java/org/atsign/cucumber/steps/ActivateSteps.java @@ -15,6 +15,7 @@ import org.atsign.client.impl.cli.Activate; import org.atsign.client.api.AtCommandExecutor; +import org.atsign.client.api.AtCommandExecutorContext; import org.atsign.client.impl.commands.EnrollCommands; import org.atsign.client.impl.commands.KeyCommands; import org.atsign.client.impl.common.EnrollmentId; @@ -233,7 +234,8 @@ public void run() { try (AtCommandExecutor executor = createConnection(rootUrl, atSign, 0)) { - authenticateWithPkam(executor, atSign, KeysUtils.loadKeys(keysFile)); + AtCommandExecutorContext context = new AtCommandExecutorContext(atSign, KeysUtils.loadKeys(keysFile)); + authenticateWithPkam(executor, context); // delete keys that have been created matchDataJsonListOfStrings(executor.sendSync("scan")).stream()