Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>
* 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.
*
* <p>
* 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<String, Object> 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<String> challenge = new AtomicReference<>();
AtomicReference<String> 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.
*
* <p>
* 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<String, Object> 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);
}

/**
Expand All @@ -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);
}
}
71 changes: 35 additions & 36 deletions at_client/src/main/java/org/atsign/client/impl/AtClientImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@
*
* <pre>
* AtClientImplBuilder builder = AtClientImpl.builder()
* .atSign(...)
* .keys(...)
* .context(...)
* .executor(...)
* .eventBus(...);
*
Expand All @@ -50,18 +49,16 @@
@Slf4j
public class AtClientImpl implements AtClient {

private final AtSign atSign;
private final AtKeys keys;
private MonitorOptions monitorOptions;
private final Map<String, Object> config;
private final AtCommandExecutorContext context;
private final MonitorOptions monitorOptions;
private final AtCommandExecutor executor;
private final AtEventBus eventBus;
private final AtomicBoolean isMonitoring = new AtomicBoolean();
private final Notifications.EventBusBridge eventBusBridge;

@Override
public AtSign getAtSign() {
return atSign;
return context.getAtSign();
}

@Override
Expand All @@ -70,23 +67,21 @@ public AtCommandExecutor getCommandExecutor() {
}

@Builder
public AtClientImpl(AtSign atSign,
AtKeys keys,
public AtClientImpl(AtCommandExecutorContext context,
boolean withMonitoring,
MonitorOptions monitorOptions,
Map<String, Object> 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);
}

/**
Expand All @@ -96,13 +91,16 @@ public AtClientImpl(AtSign atSign,
* <pre>
*
* 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();
* }
* </pre>
*
* <b>NOTE</b> 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
Expand All @@ -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
Expand All @@ -147,17 +145,17 @@ public int publishEvent(AtEventType eventType, Map<String, Object> 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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -262,6 +260,7 @@ private void onSharedKeyNotification(Map<String, Object> 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());
Expand All @@ -272,6 +271,7 @@ private void onSharedKeyNotification(Map<String, Object> eventData) throws AtDec
private void onUpdateNotification(Map<String, Object> 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<String, Object> metadata = (Map<String, Object>) eventData.get("metadata");
String ivNonce = (String) metadata.get("ivNonce");
Expand All @@ -282,7 +282,7 @@ private void onUpdateNotification(Map<String, Object> 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<String, Object> newEventData = new HashMap<>(eventData);
Expand All @@ -292,8 +292,8 @@ private void onUpdateNotification(Map<String, Object> 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;
Expand All @@ -310,9 +310,8 @@ public static CompletableFuture<String> 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;
Expand All @@ -329,8 +328,8 @@ public static CompletableFuture<byte[]> 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;
Expand Down
Loading
Loading