From d2510ea06f132be5ba39d3f6df8cecc230507e80 Mon Sep 17 00:00:00 2001 From: akafredperry Date: Wed, 12 Aug 2026 15:36:04 +0100 Subject: [PATCH] refactor(auth)!: share one AtCommandExecutorContext per connection Standardize the command layer on (AtCommandExecutor, AtCommandExecutorContext) as its leading arguments, and create exactly one context per connection in AtClients.createAtClient, passing it to both the executor builder and AtClientImpl. The context cannot be created by AtClientImpl, which receives an already-built executor, and AtCommandExecutors needs it to wire onReady - so AtClients creates it and shares it. AtCommandExecutors.resolveContext uses one when supplied, ANON when there is no atSign, and otherwise builds one - which keeps standalone builder callers working and stops a second clientId being minted. This also fixes a lost-metadata defect: every client built with withMonitoring(true) sent a from: carrying no clientId and no client version, because AtClients passed the raw config to Notifications.monitor while the context held the enriched one, and an explicit onReady replaced the context-based sequence. AtClientsTest pins this - confirmed to fail with the previous wiring restored. AtCommandExecutorContext gains a two-arg constructor, and its config is now never null and never modifiable - it is copied on construction, so a later change to the caller's map cannot alter what the connection sends. atSign and keys stay nullable because both states are load-bearing: a connection to the atDirectory / root server has no atSign, and one that only issues from: (or authenticates with CRAM before any keys exist) has no keys. A context with no atSign holds no challenge at all, so the shared ANON instance cannot leak one connection's state into another; setChallenge and consumeChallenge throw instead. Commands that need no client identity are unchanged - KeyCommands, PublicKeyCommands.getSharedByMe/ByOther, and SharedKeyCommands.getEncryptKey, whose atSign is the other party's - since an unread parameter would be worse than uniformity. AtClientImpl now holds the context instead of its own atSign/keys/config copies. No asserted command string changes; test edits are call sites only. Also fixes javadoc left mis-wrapped by the 100-column comment formatter, and two incorrect doc comments: SharedKeyCommands.put said the value would be "decrypted", and EnrollCommands.unrevoke was documented as "revoke". BREAKING CHANGE: the command signatures taking loose atSign / keys / config are removed rather than deprecated. SelfKeyCommands.get/put, SharedKeyCommands.get/put, PublicKeyCommands.get/put, Notifications.monitor, AuthenticationCommands.pkamAuthenticator and AuthenticationCommands.authenticateWithPkam now take an AtCommandExecutorContext; EnrollCommands.enroll, complete and approve likewise; and AtClientImpl.builder() takes context(...) in place of atSign(...), keys(...) and config(...). Callers using AtClients.builder() are unaffected. Closes #428 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Jn2rsqSTPCBos8K9Xp2aAf --- .../client/api/AtCommandExecutorContext.java | 82 +++++++++++-- .../org/atsign/client/impl/AtClientImpl.java | 71 ++++++------ .../org/atsign/client/impl/AtClients.java | 38 +++++- .../client/impl/AtCommandExecutors.java | 32 ++++- .../atsign/client/impl/cli/AbstractCli.java | 1 + .../org/atsign/client/impl/cli/Activate.java | 10 +- .../impl/commands/AuthenticationCommands.java | 67 +++-------- .../client/impl/commands/EnrollCommands.java | 57 +++++---- .../client/impl/commands/Notifications.java | 51 +++----- .../impl/commands/PublicKeyCommands.java | 23 ++-- .../client/impl/commands/SelfKeyCommands.java | 23 ++-- .../impl/commands/SharedKeyCommands.java | 69 ++++++----- .../api/AtCommandExecutorContextTest.java | 78 +++++++++++++ .../atsign/client/impl/AtClientImplTest.java | 109 ++++++++++++++---- .../org/atsign/client/impl/AtClientsTest.java | 85 ++++++++++++++ .../commands/AuthenticationCommandsTest.java | 42 ++++--- .../impl/commands/EnrollCommandsTest.java | 24 ++-- .../impl/commands/NotificationsTest.java | 9 +- .../impl/commands/PublicKeyCommandsTest.java | 41 +++---- .../impl/commands/SelfKeyCommandsTest.java | 17 ++- .../impl/commands/SharedKeyCommandsTest.java | 30 +++-- .../atsign/cucumber/steps/ActivateSteps.java | 4 +- 22 files changed, 643 insertions(+), 320 deletions(-) create mode 100644 at_client/src/test/java/org/atsign/client/api/AtCommandExecutorContextTest.java create mode 100644 at_client/src/test/java/org/atsign/client/impl/AtClientsTest.java 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..8840848e 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,51 +2,110 @@ 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, or {@code null} for a connection that has none — one + * talking to the atDirectory / root server. {@code AtCommandExecutors} sends nothing on ready in + * that case. + */ 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, or is {@code null} when there is no + * {@code atSign} — such a connection never issues a {@code from:}, so it has nothing to retain. + */ @Getter(AccessLevel.NONE) @EqualsAndHashCode.Exclude @ToString.Exclude - AtomicReference challenge = new AtomicReference<>(); + AtomicReference challenge; + + /** + * The context for a connection with no identity — one talking to the atDirectory / root server. + * Such a connection never issues a {@code from:} and never authenticates, because + * {@code AtCommandExecutors} sends nothing on ready when there is no atSign. + * + *

+ * This single instance is shared. It holds no challenge, and {@link #setChallenge(String)} and + * {@link #consumeChallenge()} both throw rather than let one connection's state reach another. + */ + public static final AtCommandExecutorContext ANON = new AtCommandExecutorContext(null, null, null); + + /** + * 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, or null if it has none + * @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, or null if it has none + * @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 = atSign; + this.keys = keys; + this.config = config != null ? unmodifiableMap(new LinkedHashMap<>(config)) : emptyMap(); + this.challenge = atSign != null ? new AtomicReference<>() : null; + } /** * Retains the challenge returned by the initial {@code from:}, so the authentication that follows * on the same connection can reuse it. * * @param challenge the challenge from the server's {@code from:} response + * @throws IllegalArgumentException if this context cannot hold a challenge (see {@link #ANON}) */ public void setChallenge(String challenge) { - this.challenge.set(challenge); + checkNotNull(this.challenge).set(challenge); } /** @@ -55,8 +114,9 @@ public void setChallenge(String challenge) { * same connection gets {@code null} and must issue its own {@code from:}. * * @return the retained challenge, or {@code null} if none is available + * @throws IllegalArgumentException if this context cannot hold a challenge (see {@link #ANON}) */ public String consumeChallenge() { - return challenge.getAndSet(null); + return checkNotNull(challenge).getAndSet(null); } } 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..12177034 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,21 @@ 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.getAtSign(), "atSign 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 +91,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 +114,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 +145,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 +165,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 +190,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 +200,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 +260,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 +271,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 +282,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 +292,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 +310,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 +328,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..09612ff3 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,20 @@ 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 +86,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..bea00bd5 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 @@ -58,6 +58,7 @@ public class AtCommandExecutors { public static AtCommandExecutor createCommandExecutor(String url, AtSign atSign, AtKeys keys, + AtCommandExecutorContext context, Consumer onReady, Map config, Long timeoutMillis, @@ -71,9 +72,7 @@ public static AtCommandExecutor createCommandExecutor(String 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()) @@ -82,7 +81,7 @@ public static AtCommandExecutor createCommandExecutor(String url, .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(); } @@ -96,6 +95,7 @@ public static AtCommandExecutor createCommandExecutor(String url, * .url(...) // the url for the root server or proxy (optional) * .atSign(...) // the AtSign that this client will authenticate as (optional) * .keys(...) // the AtKeys that this client will use (optional) + * .context(...) // the connection context to share with the caller (optional) * .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 +112,9 @@ 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. Set it 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. */ public static class AtCommandExecutorBuilder { // required for javadoc @@ -134,6 +137,27 @@ public static Map createClientConfig(Map config) return result; } + /** + * 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 AtCommandExecutorContext resolveContext(AtCommandExecutorContext context, + AtSign atSign, + AtKeys keys, + Map config) { + if (context != null) { + return context; + } else if (atSign == null) { + return AtCommandExecutorContext.ANON; + } else { + return new AtCommandExecutorContext(atSign, keys, createClientConfig(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 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..1e7621eb 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 @@ -114,6 +114,7 @@ protected AtCommandExecutor createConnectionSendingFrom(AtCommandExecutorContext 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..d2f13d74 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) @@ -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); } 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..5f4ab9f3 --- /dev/null +++ b/at_client/src/test/java/org/atsign/client/api/AtCommandExecutorContextTest.java @@ -0,0 +1,78 @@ +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 testAnonHasNoIdentityAndNoConfig() { + assertThat(AtCommandExecutorContext.ANON.getAtSign(), nullValue()); + assertThat(AtCommandExecutorContext.ANON.getKeys(), nullValue()); + assertThat(AtCommandExecutorContext.ANON.getConfig(), anEmptyMap()); + } + + @Test + void testChallengeApiThrowsExceptionsForAnonymousContext() { + assertThrows(IllegalArgumentException.class, () -> AtCommandExecutorContext.ANON.setChallenge("challenge")); + assertThrows(IllegalArgumentException.class, () -> AtCommandExecutorContext.ANON.consumeChallenge()); + } + + @Test + void testAContextWithAnIdentityHoldsAChallengeAtMostOnce() { + 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..a6e4bc08 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(); @@ -89,9 +88,9 @@ void testStartMonitor() throws Exception { .stub("pkam:[^{].+", "data:success") .build(); + AtCommandExecutorContext contextAlice = new AtCommandExecutorContext(createAtSign("alice"), keys); AtClientImpl client = AtClientImpl.builder() - .atSign(createAtSign("alice")) - .keys(keys) + .context(contextAlice) .executor(executor) .eventBus(bus) .build(); @@ -131,9 +130,9 @@ void testStopMonitor() throws Exception { .stub("pkam:[^{].+", "data:success") .build(); + AtCommandExecutorContext contextAlice = new AtCommandExecutorContext(createAtSign("alice"), keys); AtClientImpl client = AtClientImpl.builder() - .atSign(createAtSign("alice")) - .keys(keys) + .context(contextAlice) .executor(executor) .eventBus(bus) .build(); @@ -193,7 +192,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 +222,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 +250,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 +276,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 +299,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 +323,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 +357,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 +387,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 +414,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 +437,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 +471,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 +515,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 +548,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 +578,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 +603,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..faf6bd1d --- /dev/null +++ b/at_client/src/test/java/org/atsign/client/impl/AtClientsTest.java @@ -0,0 +1,85 @@ +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.mockito.ArgumentMatchers.matches; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +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.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()); + } +} 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..4b96691b 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 @@ -29,9 +29,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 +40,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 +54,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 testAuthenticateWithApkamWithEnrollmentIdDoesNotThrowExceptio() 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 +79,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 +90,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 +104,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 +117,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 +132,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); @@ -153,7 +151,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 +162,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) 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..81040f59 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,9 @@ public void testApprove() throws Exception { "data:{\"status\":\"approved\",\"enrollmentId\":\"12345\"}") .build(); - EnrollCommands.approve(executor, keys, createEnrollmentId("12345")); + AtCommandExecutorContext context = new AtCommandExecutorContext(null, 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/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()