diff --git a/at_client/pom.xml b/at_client/pom.xml index a9e4c642..7b679b6c 100644 --- a/at_client/pom.xml +++ b/at_client/pom.xml @@ -19,12 +19,20 @@ ../config/checkstyle.xml - 0.77 - 0.7 + 0.80 + 0.80 + + + + src/main/resources + true + + + diff --git a/at_client/src/main/java/org/atsign/client/api/AtKeys.java b/at_client/src/main/java/org/atsign/client/api/AtKeys.java index 53d5aea2..f8ddeb35 100644 --- a/at_client/src/main/java/org/atsign/client/api/AtKeys.java +++ b/at_client/src/main/java/org/atsign/client/api/AtKeys.java @@ -1,8 +1,6 @@ package org.atsign.client.api; -import java.security.Key; import java.security.KeyPair; -import java.util.Base64; import java.util.Collections; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -11,6 +9,8 @@ import lombok.Value; import org.atsign.client.impl.common.EnrollmentId; +import static org.atsign.client.impl.util.EncryptionUtils.toStringBase64; + /** * An immutable class used to hold an {@link org.atsign.client.api.AtClient}s keys. These * are use for authentication and encryption. @@ -154,18 +154,14 @@ public Map getCache() { public static class AtKeysBuilder { public AtKeysBuilder encryptKeyPair(KeyPair keyPair) { - return this.encryptPublicKey(createStringBase64(keyPair.getPublic())) - .encryptPrivateKey(createStringBase64(keyPair.getPrivate())); + return this.encryptPublicKey(toStringBase64(keyPair.getPublic())) + .encryptPrivateKey(toStringBase64(keyPair.getPrivate())); } public AtKeysBuilder apkamKeyPair(KeyPair keyPair) { - return this.apkamPublicKey(createStringBase64(keyPair.getPublic())) - .apkamPrivateKey(createStringBase64(keyPair.getPrivate())); + return this.apkamPublicKey(toStringBase64(keyPair.getPublic())) + .apkamPrivateKey(toStringBase64(keyPair.getPrivate())); } } - private static String createStringBase64(Key key) { - return Base64.getEncoder().encodeToString(key.getEncoded()); - } - } diff --git a/at_client/src/main/java/org/atsign/client/api/Metadata.java b/at_client/src/main/java/org/atsign/client/api/Metadata.java index 117336b8..6012e16e 100644 --- a/at_client/src/main/java/org/atsign/client/api/Metadata.java +++ b/at_client/src/main/java/org/atsign/client/api/Metadata.java @@ -2,12 +2,13 @@ import java.time.OffsetDateTime; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import org.atsign.client.impl.util.JsonUtils; import lombok.Builder; import lombok.Value; import lombok.experimental.Accessors; import lombok.extern.jackson.Jacksonized; -import org.atsign.client.impl.util.JsonUtils; /** * Value class which models key metadata in the Atsign Platform @@ -41,8 +42,14 @@ public class Metadata { Boolean isCached; String sharedKeyEnc; String pubKeyCS; + PublicKeyHash pubKeyHash; String encoding; + String encKeyName; + String encAlgo; String ivNonce; + String skeEncKeyName; + String skeEncAlgo; + Boolean immutable; /** * A builder for instantiating {@link Metadata} instances. Note: Metadata is immutable so if you @@ -58,6 +65,7 @@ public static Metadata fromJson(String json) { } /** + * Ordering is crucial see at_commons\lib\src\verb\syntax.dart * * @return the encoded metadata fields as recognized by an At Server in an update command. */ @@ -70,12 +78,18 @@ public String toString() { .append(ccd != null ? ":ccd:" + ccd : "") .append(dataSignature != null ? ":dataSignature:" + dataSignature : "") .append(sharedKeyStatus != null ? ":sharedKeyStatus:" + sharedKeyStatus : "") - .append(sharedKeyEnc != null ? ":sharedKeyEnc:" + sharedKeyEnc : "") - .append(pubKeyCS != null ? ":pubKeyCS:" + pubKeyCS : "") .append(isBinary != null ? ":isBinary:" + isBinary : "") .append(isEncrypted != null ? ":isEncrypted:" + isEncrypted : "") + .append(sharedKeyEnc != null ? ":sharedKeyEnc:" + sharedKeyEnc : "") + .append(pubKeyCS != null ? ":pubKeyCS:" + pubKeyCS : "") + .append(pubKeyHash != null ? ":pubKeyHash:" + pubKeyHash.hash + ":hashingAlgo:" + pubKeyHash.hashingAlgo : "") .append(encoding != null ? ":encoding:" + encoding : "") + .append(encKeyName != null ? ":encKeyName:" + encKeyName : "") + .append(encAlgo != null ? ":encAlgo:" + encAlgo : "") .append(ivNonce != null ? ":ivNonce:" + ivNonce : "") + .append(skeEncKeyName != null ? ":skeEncKeyName:" + skeEncKeyName : "") + .append(skeEncAlgo != null ? ":skeEncAlgo:" + skeEncAlgo : "") + .append(immutable != null ? ":immutable:" + immutable : "") .toString(); } @@ -158,12 +172,30 @@ public static MetadataBuilder toMergedBuilder(Metadata md1, Metadata md2) { if (!setPubKeyCSIfNotNull(builder, md1.pubKeyCS)) { setPubKeyCSIfNotNull(builder, md2.pubKeyCS); } + if (!setPubKeyHashIfNotNull(builder, md1.pubKeyHash)) { + setPubKeyHashIfNotNull(builder, md2.pubKeyHash); + } if (!setEncodingIfNotNull(builder, md1.encoding)) { setEncodingIfNotNull(builder, md2.encoding); } + if (!setEncKeyNameIfNotNull(builder, md1.encKeyName)) { + setEncKeyNameIfNotNull(builder, md2.encKeyName); + } + if (!setEncAlgoIfNotNull(builder, md1.encAlgo)) { + setEncAlgoIfNotNull(builder, md2.encAlgo); + } if (!setIvNonceIfNotNull(builder, md1.ivNonce)) { setIvNonceIfNotNull(builder, md2.ivNonce); } + if (!setSkeEncKeyNameIfNotNull(builder, md1.skeEncKeyName)) { + setSkeEncKeyNameIfNotNull(builder, md2.skeEncKeyName); + } + if (!setSkeEncAlgoIfNotNull(builder, md1.skeEncAlgo)) { + setSkeEncAlgoIfNotNull(builder, md2.skeEncAlgo); + } + if (!setImmutableIfNotNull(builder, md1.immutable)) { + setImmutableIfNotNull(builder, md2.immutable); + } return builder; } @@ -339,6 +371,15 @@ public static boolean setPubKeyCSIfNotNull(MetadataBuilder builder, String value } } + public static boolean setPubKeyHashIfNotNull(MetadataBuilder builder, PublicKeyHash value) { + if (value != null) { + builder.pubKeyHash(value); + return true; + } else { + return false; + } + } + public static boolean setEncodingIfNotNull(MetadataBuilder builder, String value) { if (value != null) { builder.encoding(value); @@ -348,6 +389,24 @@ public static boolean setEncodingIfNotNull(MetadataBuilder builder, String value } } + public static boolean setEncKeyNameIfNotNull(MetadataBuilder builder, String value) { + if (value != null) { + builder.encKeyName(value); + return true; + } else { + return false; + } + } + + public static boolean setEncAlgoIfNotNull(MetadataBuilder builder, String value) { + if (value != null) { + builder.encAlgo(value); + return true; + } else { + return false; + } + } + public static boolean setIvNonceIfNotNull(MetadataBuilder builder, String value) { if (value != null) { builder.ivNonce(value); @@ -357,7 +416,46 @@ public static boolean setIvNonceIfNotNull(MetadataBuilder builder, String value) } } + public static boolean setSkeEncKeyNameIfNotNull(MetadataBuilder builder, String value) { + if (value != null) { + builder.skeEncKeyName(value); + return true; + } else { + return false; + } + } + + public static boolean setSkeEncAlgoIfNotNull(MetadataBuilder builder, String value) { + if (value != null) { + builder.skeEncAlgo(value); + return true; + } else { + return false; + } + } + + public static boolean setImmutableIfNotNull(MetadataBuilder builder, Boolean value) { + if (value != null) { + builder.immutable(value); + return true; + } else { + return false; + } + } + public static boolean isBinary(Metadata metadata) { return metadata.isBinary() != null && metadata.isBinary(); } + + /** + * Model a public key hash tuple, the hash value and the algorithm used to generate the digest. + */ + @Value + @Jacksonized + @Builder + @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) + public static class PublicKeyHash { + String hash; + String hashingAlgo; + } } 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 f97eb263..8622489a 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 @@ -52,6 +52,7 @@ public class AtClientImpl implements AtClient { private final AtSign atSign; private final AtKeys keys; + private final Map config; private final AtCommandExecutor executor; private final AtEventBus eventBus; private final AtomicBoolean isMonitoring = new AtomicBoolean(); @@ -68,9 +69,14 @@ public AtCommandExecutor getCommandExecutor() { } @Builder - public AtClientImpl(AtSign atSign, AtKeys keys, AtCommandExecutor executor, AtEventBus eventBus) { + public AtClientImpl(AtSign atSign, + AtKeys keys, + Map config, + AtCommandExecutor executor, + AtEventBus eventBus) { this.atSign = checkNotNull(atSign, "atSign not set"); this.keys = checkNotNull(keys, "keys not set"); + 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)); @@ -106,13 +112,13 @@ public void close() throws Exception { @Override public void startMonitor() { isMonitoring.compareAndSet(false, true); - executor.onReady(Notifications.monitor(atSign, keys, eventBusBridge::accept)); + executor.onReady(Notifications.monitor(atSign, keys, config, eventBusBridge::accept)); } @Override public void stopMonitor() { isMonitoring.compareAndSet(true, false); - executor.onReady(AuthenticationCommands.pkamAuthenticator(atSign, keys)); + executor.onReady(AuthenticationCommands.pkamAuthenticator(atSign, keys, config)); } @Override @@ -268,12 +274,18 @@ 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) { - String key = (String) eventData.get("key"); String encryptedValue = (String) eventData.get("value"); Map metadata = (Map) eventData.get("metadata"); String ivNonce = (String) metadata.get("ivNonce"); - SharedKey sk = org.atsign.client.api.Keys.sharedKeyBuilder().rawKey(key).build(); - String encryptKeySharedByOther = SharedKeyCommands.getEncryptKeySharedByOther(executor, keys, sk); + String encryptKeySharedByOther; + String sharedKeyEnc = (String) metadata.get("sharedKeyEnc"); + if (sharedKeyEnc != null) { + encryptKeySharedByOther = rsaDecryptFromBase64(sharedKeyEnc, keys.getEncryptPrivateKey()); + } else { + String key = (String) eventData.get("key"); + SharedKey sk = org.atsign.client.api.Keys.sharedKeyBuilder().rawKey(key).build(); + encryptKeySharedByOther = SharedKeyCommands.lookupEncryptKeySharedByOther(executor, keys, sk); + } String decryptedValue = aesDecryptFromBase64(encryptedValue, encryptKeySharedByOther, ivNonce); HashMap newEventData = new HashMap<>(eventData); newEventData.put("decryptedValue", decryptedValue); 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 df86a059..3f0f5e97 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 @@ -2,13 +2,14 @@ import static org.atsign.client.impl.common.Preconditions.checkNotNull; -import org.atsign.client.api.AtClient; -import org.atsign.client.api.AtCommandExecutor; -import org.atsign.client.api.AtKeys; -import org.atsign.client.api.AtSign; +import java.io.File; +import java.util.Map; + +import org.atsign.client.api.*; import org.atsign.client.impl.common.ReconnectStrategy; import org.atsign.client.impl.common.SimpleAtEventBus; import org.atsign.client.impl.common.SimpleReconnectStrategy; +import org.atsign.client.impl.exceptions.AtClientConfigException; import org.atsign.client.impl.exceptions.AtException; import org.atsign.client.impl.util.KeysUtils; @@ -34,6 +35,8 @@ public class AtClients { public static AtClient createAtClient(String url, AtSign atSign, AtKeys keys, + String keysPath, + Map config, Long timeoutMillis, Long awaitReadyMillis, ReconnectStrategy reconnect, @@ -42,12 +45,13 @@ public static AtClient createAtClient(String url, throws AtException { checkNotNull(atSign, "atSign not set"); - keys = keys != null ? keys : KeysUtils.loadKeys(atSign); + keys = keys != null ? keys : loadKeys(keysPath, atSign); AtCommandExecutor executor = AtCommandExecutors.builder() .url(url) .atSign(atSign) .keys(keys) + .config(config) .timeoutMillis(timeoutMillis) .awaitReadyMillis(awaitReadyMillis) .reconnect(reconnect) @@ -60,11 +64,26 @@ public static AtClient createAtClient(String url, return AtClientImpl.builder() .atSign(atSign) .keys(keys) + .config(config) .executor(executor) .eventBus(eventBus) .build(); } + private static AtKeys loadKeys(String path, AtSign atSign) throws AtClientConfigException { + if (path == null) { + return KeysUtils.loadKeys(atSign); + } + File f = new File(path); + if (!f.exists()) { + throw new AtClientConfigException(path + " does not exist"); + } + if (f.isDirectory()) { + return KeysUtils.loadKeys(KeysUtils.getKeysFile(atSign, path)); + } + return KeysUtils.loadKeys(f); + } + /** * A builder for instantiating {@link AtClient} implementations that are included in * this library. @@ -75,6 +94,8 @@ public static AtClient createAtClient(String url, * .atSign(...) // the AtSign that this client will authenticate as * .url(...) // the url for the root server or proxy (optional) * .keys(...) // the AtKeys that this client will use (optional) + * .keysPath(...) // the location for the AtKeys that this client will use (optional) + * .config(...) // the config map that will be passed during authentication (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) @@ -89,6 +110,9 @@ public static AtClient createAtClient(String url, * If keys is not set then the builder will default to attempting to load the keys * which correspond to the atSign field in ~/.atsign/keys (or the environment variable * / system property {@link KeysUtils#ATSIGN_KEYS_DIR} if set). + * If keysPath is set (and keys is not set) then the builder will attempt to load keys + * from the path value. If the provided value is not a directory it will simply load the file, + * otherwise it will look for a atKeys file for the atSign in the path. * If timeoutMillis is not set then builder will default to * {@link AtCommandExecutors#DEFAULT_TIMEOUT_MILLIS}. * If awaitReadyMillis is not set then the builder will default to @@ -100,4 +124,5 @@ public static AtClient createAtClient(String url, public static class AtClientBuilder { // required for javadoc } + } 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 625feab6..6b5a94e6 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 @@ -3,6 +3,12 @@ import static org.atsign.client.impl.common.Preconditions.checkNotNull; +import java.io.InputStream; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -16,6 +22,7 @@ import org.atsign.client.impl.netty.NettyAtCommandExecutor; import lombok.Builder; +import lombok.extern.slf4j.Slf4j; /** * Utility methods / builders for instantiating {@link AtCommandExecutor} implementations @@ -40,6 +47,7 @@ * NOTE: If reconnect is not set then the builder will default to a * {@link SimpleReconnectStrategy} */ +@Slf4j public class AtCommandExecutors { public static final long DEFAULT_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(5); @@ -48,6 +56,7 @@ public class AtCommandExecutors { public static AtCommandExecutor createCommandExecutor(String url, AtSign atSign, AtKeys keys, + Map config, Long timeoutMillis, Long awaitReadyMillis, ReconnectStrategy reconnect, @@ -66,7 +75,7 @@ public static AtCommandExecutor createCommandExecutor(String url, .awaitReadyMillis(defaultIfNotSet(awaitReadyMillis, DEFAULT_TIMEOUT_MILLIS)) .reconnect(defaultIfNotSet(reconnect)) .queueLimit(queueLimit) - .onReady(createOnReady(atSign, keys)) + .onReady(createOnReady(atSign, keys, createClientConfig(config))) .build(); } @@ -101,10 +110,27 @@ public static class AtCommandExecutorBuilder { // required for javadoc } - private static Consumer createOnReady(AtSign atSign, AtKeys keys) { + public static Map createClientConfig(Map config) { + Map result = new HashMap<>(); + result.put("clientId", UUID.randomUUID()); + Properties properties = new Properties(); + URL resource = AtCommandExecutors.class.getClassLoader().getResource("client-config.properties"); + try (InputStream in = resource.openStream()) { + properties.load(in); + properties.forEach((k, v) -> result.put(k.toString(), v.toString().replace("-SNAPSHOT", ""))); + } catch (Exception e) { + log.warn("unable to load client-config.properties"); + } + if (config != null) { + result.putAll(config); + } + return result; + } + + private static Consumer createOnReady(AtSign atSign, AtKeys keys, Map config) { Consumer onReady; if (atSign != null && keys != null) { - onReady = AuthenticationCommands.pkamAuthenticator(atSign, keys); + onReady = AuthenticationCommands.pkamAuthenticator(atSign, keys, config); } else { onReady = c -> { }; @@ -119,5 +145,4 @@ private static ReconnectStrategy defaultIfNotSet(ReconnectStrategy reconnect) { private static long defaultIfNotSet(Long l, long defaultValue) { return l != null ? l : defaultValue; } - } 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 153eab1f..b29e3179 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 @@ -96,7 +96,7 @@ protected AtCommandExecutor createConnection(String rootUrl, AtSign atSign, int protected AtCommandExecutor createAuthenticatedConnection(String rootUrl, AtSign atSign, int retries) throws AtException { return createCommandExecutorBuilder(rootUrl, atSign, retries, verbose) - .onReady(AuthenticationCommands.pkamAuthenticator(atSign, getKeys())) + .onReady(AuthenticationCommands.pkamAuthenticator(atSign, getKeys(), null)) .build(); } 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 b97b695b..b7d3e702 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 @@ -4,10 +4,12 @@ import static org.atsign.client.impl.commands.DataResponses.matchDataStringNoWhitespace; import static org.atsign.client.impl.commands.DataResponses.matchDataSuccess; import static org.atsign.client.impl.commands.ErrorResponses.throwExceptionIfError; +import static org.atsign.client.impl.util.EncryptionUtils.bytesToHex; 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; @@ -24,8 +26,8 @@ */ public class AuthenticationCommands { - public static Consumer pkamAuthenticator(AtSign atSign, AtKeys keys) { - return throwOnReadyException(executor -> authenticateWithPkam(executor, atSign, keys)); + public static Consumer pkamAuthenticator(AtSign atSign, AtKeys keys, Map config) { + return throwOnReadyException(executor -> authenticateWithPkam(executor, atSign, keys, config)); } /** @@ -38,10 +40,27 @@ public static Consumer pkamAuthenticator(AtSign atSign, AtKey */ public static void authenticateWithPkam(AtCommandExecutor executor, AtSign atSign, AtKeys keys) throws AtException { + authenticateWithPkam(executor, atSign, keys, null); + } + + /** + * 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. + * @param config The map of configuration values to send in the from command. + * @throws AtException If authentication fails. + */ + public static void authenticateWithPkam(AtCommandExecutor executor, + AtSign atSign, + AtKeys keys, + Map config) + throws AtException { try { // send a from command and expect to receive a challenge - String fromCommand = CommandBuilders.fromCommandBuilder().atSign(atSign).build(); + String fromCommand = CommandBuilders.fromCommandBuilder().atSign(atSign).config(config).build(); String fromResponse = executor.sendSync(fromCommand); String challenge = matchDataStringNoWhitespace(throwExceptionIfError(fromResponse)); @@ -105,15 +124,5 @@ private static String createDigest(String cramSecret, String challenge) throws A } } - private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray(); - public static String bytesToHex(byte[] bytes) { - char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = HEX_ARRAY[v >>> 4]; - hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; - } - return new String(hexChars); - } } diff --git a/at_client/src/main/java/org/atsign/client/impl/commands/CommandBuilders.java b/at_client/src/main/java/org/atsign/client/impl/commands/CommandBuilders.java index 64a860ce..b8f01d65 100644 --- a/at_client/src/main/java/org/atsign/client/impl/commands/CommandBuilders.java +++ b/at_client/src/main/java/org/atsign/client/impl/commands/CommandBuilders.java @@ -39,9 +39,13 @@ public class CommandBuilders { * @throws IllegalArgumentException If mandatory fields are not set or if field values conflict. */ @Builder(builderMethodName = "fromCommandBuilder", builderClassName = "FromCommandBuilder") - public static String from(AtSign atSign) { + public static String from(AtSign atSign, Map config) { checkNotNull(atSign, "atSign not set"); - return "from:" + atSign; + StringBuilder builder = new StringBuilder("from:").append(atSign); + if (config != null && !config.isEmpty()) { + builder.append(":clientConfig:").append(JsonUtils.writeValueAsString(config)); + } + return builder.toString(); } /** diff --git a/at_client/src/main/java/org/atsign/client/impl/commands/ErrorResponses.java b/at_client/src/main/java/org/atsign/client/impl/commands/ErrorResponses.java index 60b22e10..50a7c63e 100644 --- a/at_client/src/main/java/org/atsign/client/impl/commands/ErrorResponses.java +++ b/at_client/src/main/java/org/atsign/client/impl/commands/ErrorResponses.java @@ -3,6 +3,7 @@ import static org.atsign.client.impl.commands.AtExceptions.toTypedException; import static org.atsign.client.impl.common.Preconditions.checkNotNull; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -20,6 +21,8 @@ public class ErrorResponses { private static final Pattern ERROR_WITH_CODE = Pattern.compile("error:(AT\\d+)([^:]*):\\s*(.+)"); + private static final Pattern ERROR_WITH_JSON = Pattern.compile("error:(\\{.+})"); + /** * Use this to verify a "error:xxxx" response. * @@ -48,7 +51,12 @@ public static String throwExceptionIfError(String response) throws AtException { private static AtException getAtExceptionIfError(String response) throws AtException { checkNotNull(response); - Matcher matcher = ERROR_WITH_CODE.matcher(response); + Matcher matcher = ERROR_WITH_JSON.matcher(response); + if (matcher.matches()) { + Map map = Responses.decodeJsonMapOfObjects(matcher.group(1)); + return toTypedException((String) map.get("errorCode"), (String) map.get("errorDescription")); + } + matcher = ERROR_WITH_CODE.matcher(response); if (matcher.matches()) { return toTypedException(matcher.group(1), matcher.group(3)); } 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 1ba03664..91f7f711 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 @@ -38,8 +38,11 @@ public class Notifications { * @param keys The {@link AtKeys} to authenticate with. * @param consumer A consumer that will be invoked with each notification. */ - public static Consumer monitor(AtSign atSign, AtKeys keys, Consumer consumer) { - return throwOnReadyException(executor -> monitor(executor, atSign, keys, consumer)); + public static Consumer monitor(AtSign atSign, + AtKeys keys, + Map config, + Consumer consumer) { + return throwOnReadyException(executor -> monitor(executor, atSign, keys, config, consumer)); } /** @@ -51,12 +54,34 @@ public static Consumer monitor(AtSign atSign, AtKeys keys, Co * @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, AtKeys keys, Consumer consumer) + public static void monitor(AtCommandExecutor executor, + AtSign atSign, + AtKeys keys, + Consumer consumer) + throws AtException { + monitor(executor, atSign, 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 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. + * @throws AtException If any of the commands fail. + */ + public static void monitor(AtCommandExecutor executor, + AtSign atSign, + AtKeys keys, + Map config, + Consumer consumer) throws AtException { try { // authenticate - authenticateWithPkam(executor, atSign, keys); + authenticateWithPkam(executor, atSign, keys, config); // send monitor command executor.sendSync("monitor", consumer); 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 a70a9590..72765a25 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 @@ -13,8 +13,7 @@ import org.atsign.client.api.*; import org.atsign.client.api.Keys.SharedKey; -import org.atsign.client.impl.exceptions.AtException; -import org.atsign.client.impl.exceptions.AtKeyNotFoundException; +import org.atsign.client.impl.exceptions.*; import org.atsign.client.impl.util.EncryptionUtils; /** @@ -81,7 +80,7 @@ public static void put(AtCommandExecutor executor, AtSign atSign, AtKeys keys, S try { // get or create key for sharedBy - sharedWith - String aesKey = getEncryptKeySharedByMe(executor, keys, key); + String aesKey = lookupEncryptKeySharedByMe(executor, keys, key); if (aesKey == null) { aesKey = createEncryptKey(executor, keys, key); } @@ -115,8 +114,8 @@ private static String getSharedByMe(AtCommandExecutor executor, AtKeys keys, Sha checkTrue(Metadata.isBinary(llookupResponse.metaData), "metadata.isBinary not set to true"); } - // get my encrypt key for sharedBy sharedWith - String aesKey = checkNotNull(getEncryptKeySharedByMe(executor, keys, key), key + " not found"); + // get the encryption key that was previously created by "me" + String aesKey = checkNotNull(lookupEncryptKeySharedByMe(executor, keys, key), key + " not found"); // return decrypted value return aesDecryptFromBase64(llookupResponse.data, aesKey, llookupResponse.metaData.ivNonce()); @@ -138,17 +137,35 @@ private static String getSharedByOther(AtCommandExecutor executor, AtKeys keys, checkTrue(Metadata.isBinary(lookupResponse.metaData), "isBinary not set to true"); } - // get my encrypt key for sharedBy sharedWith - String shareEncryptionKey = getEncryptKeySharedByOther(executor, keys, key); + // get the encryption key that was created by the "other" + String sharedEncryptionKey; + if (lookupResponse.metaData.sharedKeyEnc() != null) { + sharedEncryptionKey = extractEncryptKeySharedByOther(lookupResponse, keys); + } else { + sharedEncryptionKey = lookupEncryptKeySharedByOther(executor, keys, key); + } // return decrypted value - return aesDecryptFromBase64(lookupResponse.data, shareEncryptionKey, lookupResponse.metaData.ivNonce()); + return aesDecryptFromBase64(lookupResponse.data, sharedEncryptionKey, lookupResponse.metaData.ivNonce()); } catch (ExecutionException | InterruptedException e) { throw new RuntimeException(e); } } + private static String extractEncryptKeySharedByOther(LookupResponse lookupResponse, AtKeys keys) + throws AtException { + String encryptedShareEncryptionKey = lookupResponse.metaData.sharedKeyEnc(); + if (lookupResponse.metaData.pubKeyHash() != null) { + String algo = lookupResponse.metaData.pubKeyHash().hashingAlgo(); + String hash = digest(keys.getEncryptPublicKey(), algo); + if (!hash.equals(lookupResponse.metaData.pubKeyHash().hash())) { + throw new AtPublicKeyChangeException("pubKeyHash mis-match"); + } + } + return rsaDecryptFromBase64(encryptedShareEncryptionKey, keys.getEncryptPrivateKey()); + } + /** * Get the specific encryption key which needs to be used for a sharedBy - sharedWith relationship * where the AtSign that the {@link AtCommandExecutor} has authenticated with is the sharedBy @@ -160,7 +177,7 @@ private static String getSharedByOther(AtCommandExecutor executor, AtKeys keys, * @return The symmetric encryption key (in base64). * @throws AtException If any of the commands fail or the key does not exist. */ - public static String getEncryptKeySharedByMe(AtCommandExecutor executor, AtKeys keys, SharedKey key) + public static String lookupEncryptKeySharedByMe(AtCommandExecutor executor, AtKeys keys, SharedKey key) throws AtException { try { @@ -205,7 +222,7 @@ public static String getEncryptKeySharedByMe(AtCommandExecutor executor, AtKeys * @return The symmetric encryption key (in base64). * @throws AtException If any of the commands fail or the key does not exist. */ - public static String getEncryptKeySharedByOther(AtCommandExecutor executor, AtKeys keys, SharedKey key) + public static String lookupEncryptKeySharedByOther(AtCommandExecutor executor, AtKeys keys, SharedKey key) throws AtException { try { @@ -250,9 +267,15 @@ private static String createEncryptKey(AtCommandExecutor executor, AtKeys keys, .sharedBy(key.sharedBy()) .value(encryptedForMe) .build(); + executor.sendSync(updateForUsCommand); - // get the other (sharedWith) atsign's public key + // get the other (sharedWith) atsign's public key (and compute the hash of that key) String otherPublicKey = getEncryptKey(executor, key.sharedWith()); + Metadata.PublicKeyHash hash = Metadata.PublicKeyHash.builder() + .hash(EncryptionUtils.digest(otherPublicKey, HASHING_ALGO_SHA512)) + .hashingAlgo(HASHING_ALGO_SHA512) + .build(); + String checksum = digest(otherPublicKey, "MD5"); // compose an update command to store this key encrypted with the other (sharedWith) atsign's public key String encryptedForOther = rsaEncryptToBase64(aesKey, otherPublicKey); @@ -263,11 +286,18 @@ private static String createEncryptKey(AtCommandExecutor executor, AtKeys keys, .ttr(TimeUnit.HOURS.toMillis(24)) .value(encryptedForOther) .build(); - - // send the update commands - executor.sendSync(updateForUsCommand); executor.sendSync(updateForOtherCommand); + // update the key metadata to include the shared encryption key encrypted with the other (sharedWith) + // atsign's public key plus the hash of that key to accommodate race conditions on public key changes + Metadata modifiedMetadata = key.metadata().toBuilder() + .sharedKeyEnc(encryptedForOther) + .pubKeyHash(hash) + .pubKeyCS(checksum) + .build(); + key.overwriteMetadata(modifiedMetadata); + + // store in my cache keys.put(AtKeyNames.toSharedByMeKeyName(key.sharedWith()), aesKey); // return the new diff --git a/at_client/src/main/java/org/atsign/client/impl/exceptions/AtPublicKeyChangeException.java b/at_client/src/main/java/org/atsign/client/impl/exceptions/AtPublicKeyChangeException.java new file mode 100644 index 00000000..9cd46e10 --- /dev/null +++ b/at_client/src/main/java/org/atsign/client/impl/exceptions/AtPublicKeyChangeException.java @@ -0,0 +1,14 @@ +package org.atsign.client.impl.exceptions; + +/** + * Occurs when sharedKeyEnc was encrypted with a public key that has now changed + */ +public class AtPublicKeyChangeException extends AtException { + public AtPublicKeyChangeException(String message) { + super(message); + } + + public AtPublicKeyChangeException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/at_client/src/main/java/org/atsign/client/impl/util/EncryptionUtils.java b/at_client/src/main/java/org/atsign/client/impl/util/EncryptionUtils.java index 2dcca30c..aa2306a5 100644 --- a/at_client/src/main/java/org/atsign/client/impl/util/EncryptionUtils.java +++ b/at_client/src/main/java/org/atsign/client/impl/util/EncryptionUtils.java @@ -1,5 +1,8 @@ package org.atsign.client.impl.util; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.atsign.client.impl.common.Preconditions.checkNotBlank; + import java.security.*; import java.security.spec.EncodedKeySpec; import java.security.spec.InvalidKeySpecException; @@ -15,14 +18,14 @@ import org.atsign.client.impl.exceptions.AtEncryptionException; import org.bouncycastle.jce.provider.BouncyCastleProvider; -import static java.nio.charset.StandardCharsets.UTF_8; - /** * Utility class which registers bouncycastle as a security {@link Provider} and provides * static methods for the various encryption functions required by Atsign client APIs */ public class EncryptionUtils { + private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray(); + /** * The signing algo "label" */ @@ -33,10 +36,44 @@ public class EncryptionUtils { */ public static final String HASHING_ALGO_SHA256 = "sha256"; + /** + * The hashing algo "label" + */ + public static final String HASHING_ALGO_SHA512 = "sha512"; + static { Security.addProvider(new BouncyCastleProvider()); } + /** + * Creates a digest for the given input using the given algo. + * + * @param input The text to digest. + * @param algo The digest algorithm to use. e.g. MD5 + * @return The digest as a hex string. + * @throws AtEncryptionException If algorithm cannot be found or any other error. + */ + public static String digest(String input, String algo) throws AtEncryptionException { + try { + MessageDigest md = MessageDigest.getInstance(toMessageDigestAlgorithm(algo)); + return bytesToHex(md.digest(checkNotBlank(input, "input blank").getBytes(UTF_8))); + } catch (IllegalArgumentException | NoSuchAlgorithmException e) { + throw new AtEncryptionException("failed to hash : " + e.getMessage(), e); + } + } + + private static String toMessageDigestAlgorithm(String algo) { + checkNotBlank(algo, "algo blank"); + switch (algo) { + case HASHING_ALGO_SHA256: + return "SHA-256"; + case HASHING_ALGO_SHA512: + return "SHA-512"; + default: + return algo; + } + } + /** * Encrypts a String with the AES Cipher and encodes as Base 64. * @@ -50,11 +87,12 @@ public static String aesEncryptToBase64(String input, String key, String iv) throws AtEncryptionException { try { Cipher cipher = createAesCipher(Cipher.ENCRYPT_MODE, key, iv); - byte[] encrypted = cipher.doFinal(input.getBytes(UTF_8)); + byte[] encrypted = cipher.doFinal(checkNotBlank(input, "input is blank").getBytes(UTF_8)); return Base64.getEncoder().encodeToString(encrypted); - } catch (NoSuchAlgorithmException | NoSuchProviderException | BadPaddingException | IllegalBlockSizeException - | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException e) { - throw new AtEncryptionException("AES encryption failed", e); + } catch (IllegalArgumentException | NoSuchAlgorithmException | NoSuchProviderException | BadPaddingException + | IllegalBlockSizeException | NoSuchPaddingException | InvalidKeyException + | InvalidAlgorithmParameterException e) { + throw new AtEncryptionException("AES encryption failed : " + e.getMessage(), e); } } @@ -70,11 +108,12 @@ public static String aesEncryptToBase64(String input, String key, String iv) public static String aesDecryptFromBase64(String input, String key, String iv) throws AtDecryptionException { try { Cipher cipher = createAesCipher(Cipher.DECRYPT_MODE, key, iv); - byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(input)); + byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(checkNotBlank(input, "input is blank"))); return new String(decrypted, UTF_8); - } catch (NoSuchAlgorithmException | NoSuchProviderException | NoSuchPaddingException | InvalidKeyException - | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) { - throw new AtDecryptionException("AES decryption failed", e); + } catch (IllegalArgumentException | NoSuchAlgorithmException | NoSuchProviderException | NoSuchPaddingException + | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException + | BadPaddingException e) { + throw new AtDecryptionException("AES decryption failed : " + e.getMessage(), e); } } @@ -124,12 +163,12 @@ public static String rsaDecryptFromBase64(String input, String key) throws AtDec PrivateKey privateKey = toPrivateKey(key); Cipher decryptCipher = Cipher.getInstance("RSA"); decryptCipher.init(Cipher.DECRYPT_MODE, privateKey); - byte[] decoded = Base64.getDecoder().decode(input.getBytes(UTF_8)); + byte[] decoded = Base64.getDecoder().decode(checkNotBlank(input, "input is blank").getBytes(UTF_8)); byte[] decryptedMessageBytes = decryptCipher.doFinal(decoded); return new String(decryptedMessageBytes, UTF_8); - } catch (NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException | InvalidKeyException - | IllegalBlockSizeException | BadPaddingException e) { - throw new AtDecryptionException("RSA decryption failed", e); + } catch (IllegalArgumentException | NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException + | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) { + throw new AtDecryptionException("RSA decryption failed : " + e.getMessage(), e); } } @@ -146,12 +185,12 @@ public static String rsaEncryptToBase64(String input, String key) throws AtEncry PublicKey publicKey = toPublicKey(key); Cipher encryptCipher = Cipher.getInstance("RSA"); encryptCipher.init(Cipher.ENCRYPT_MODE, publicKey); - byte[] clearTextBytes = input.getBytes(UTF_8); + byte[] clearTextBytes = checkNotBlank(input, "input is blank").getBytes(UTF_8); byte[] encryptedMessageBytes = encryptCipher.doFinal(clearTextBytes); return Base64.getEncoder().encodeToString(encryptedMessageBytes); - } catch (NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException | InvalidKeyException - | IllegalBlockSizeException | BadPaddingException e) { - throw new AtEncryptionException("RSA encryption failed", e); + } catch (IllegalArgumentException | NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException + | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) { + throw new AtEncryptionException("RSA encryption failed : " + e.getMessage(), e); } } @@ -168,35 +207,36 @@ public static String signSHA256RSA(String input, String key) throws AtEncryption PrivateKey pk = toPrivateKey(key); Signature privateSignature = Signature.getInstance("SHA256withRSA"); privateSignature.initSign(pk); - privateSignature.update(input.getBytes(UTF_8)); + privateSignature.update(checkNotBlank(input, "input is blank").getBytes(UTF_8)); byte[] signedBytes = privateSignature.sign(); return Base64.getEncoder().encodeToString(signedBytes); - } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException | SignatureException e) { - throw new AtEncryptionException("SHA256 sign failed", e); + } catch (IllegalArgumentException | NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException + | SignatureException e) { + throw new AtEncryptionException("SHA256 sign failed : " + e.getMessage(), e); } } private static PublicKey toPublicKey(String s) throws NoSuchAlgorithmException, InvalidKeySpecException { - byte[] keyBytes = Base64.getDecoder().decode(s.getBytes(UTF_8)); + byte[] keyBytes = Base64.getDecoder().decode(checkNotBlank(s, "key is blank").getBytes(UTF_8)); EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); KeyFactory rsaKeyFactory = KeyFactory.getInstance("RSA"); return rsaKeyFactory.generatePublic(keySpec); } private static PrivateKey toPrivateKey(String s) throws NoSuchAlgorithmException, InvalidKeySpecException { - byte[] keyBytes = Base64.getDecoder().decode(s.getBytes(UTF_8)); + byte[] keyBytes = Base64.getDecoder().decode(checkNotBlank(s, "key is blank").getBytes(UTF_8)); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); KeyFactory rsaKeyFactory = KeyFactory.getInstance("RSA"); return rsaKeyFactory.generatePrivate(keySpec); } private static SecretKey toSecretKey(String s) { - byte[] keyBytes = Base64.getDecoder().decode(s.getBytes()); + byte[] keyBytes = Base64.getDecoder().decode(checkNotBlank(s, "key is blank").getBytes()); return new SecretKeySpec(keyBytes, "AES"); } private static IvParameterSpec toIvParameterSpec(String s) { - byte[] ivBytes = Base64.getDecoder().decode(s.getBytes()); + byte[] ivBytes = Base64.getDecoder().decode(checkNotBlank(s, "iv is blank").getBytes()); return new IvParameterSpec(ivBytes); } @@ -213,6 +253,20 @@ public static String generateRandomIvBase64(int length) { return Base64.getEncoder().encodeToString(iv); } + public static String bytesToHex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = HEX_ARRAY[v >>> 4]; + hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; + } + return new String(hexChars); + } + + public static String toStringBase64(Key key) { + return Base64.getEncoder().encodeToString(key.getEncoded()); + } + private static Cipher createAesCipher(int mode, String keyBase64, String ivNonce) throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException { SecretKey key = toSecretKey(keyBase64); diff --git a/at_client/src/main/resources/client-config.properties b/at_client/src/main/resources/client-config.properties new file mode 100644 index 00000000..a84f8c51 --- /dev/null +++ b/at_client/src/main/resources/client-config.properties @@ -0,0 +1,2 @@ +version=${project.version} +platform=Java diff --git a/at_client/src/test/java/org/atsign/client/api/MetadataTest.java b/at_client/src/test/java/org/atsign/client/api/MetadataTest.java index fd8bb41e..2f5b52ca 100644 --- a/at_client/src/test/java/org/atsign/client/api/MetadataTest.java +++ b/at_client/src/test/java/org/atsign/client/api/MetadataTest.java @@ -1,14 +1,15 @@ package org.atsign.client.api; -import com.fasterxml.jackson.core.JsonProcessingException; -import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.time.OffsetDateTime; import java.time.ZoneOffset; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.assertThrows; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; class MetadataTest { @@ -130,6 +131,10 @@ void testMergeMd1FieldsTakePriorityOverMd2() { .isEncrypted(true).isBinary(true).namespaceAware(true) .dataSignature("ds1").sharedKeyStatus("sks1").sharedKeyEnc("ske1") .pubKeyCS("pkcs1").encoding("utf8").ivNonce("iv1") + .pubKeyHash(Metadata.PublicKeyHash.builder().hash("hash1").hashingAlgo("algo1").build()) + .encKeyName("encKeyName1").encAlgo("encAlgo1") + .skeEncKeyName("skeEncKeyName1").skeEncAlgo("skeEncAlgo1") + .immutable(true) .build(); Metadata md2 = Metadata.builder() @@ -138,13 +143,17 @@ void testMergeMd1FieldsTakePriorityOverMd2() { .isEncrypted(false).isBinary(false).namespaceAware(false) .dataSignature("ds2").sharedKeyStatus("sks2").sharedKeyEnc("ske2") .pubKeyCS("pkcs2").encoding("ascii").ivNonce("iv2") + .pubKeyHash(Metadata.PublicKeyHash.builder().hash("hash2").hashingAlgo("algo2").build()) + .encKeyName("encKeyName2").encAlgo("encAlgo2") + .skeEncKeyName("skeEncKeyName2").skeEncAlgo("skeEncAlgo2") + .immutable(false) .build(); Metadata merged = Metadata.merge(md1, md2); - assertThat(merged.ttl(), is(1L)); - assertThat(merged.ttb(), is(2L)); - assertThat(merged.ttr(), is(3L)); + assertThat(merged.ttl(), equalTo(1L)); + assertThat(merged.ttb(), equalTo(2L)); + assertThat(merged.ttr(), equalTo(3L)); assertThat(merged.ccd(), is(true)); assertThat(merged.isPublic(), is(true)); assertThat(merged.isHidden(), is(true)); @@ -152,12 +161,19 @@ void testMergeMd1FieldsTakePriorityOverMd2() { assertThat(merged.isEncrypted(), is(true)); assertThat(merged.isBinary(), is(true)); assertThat(merged.namespaceAware(), is(true)); - assertThat(merged.dataSignature(), is("ds1")); - assertThat(merged.sharedKeyStatus(), is("sks1")); - assertThat(merged.sharedKeyEnc(), is("ske1")); - assertThat(merged.pubKeyCS(), is("pkcs1")); - assertThat(merged.encoding(), is("utf8")); - assertThat(merged.ivNonce(), is("iv1")); + assertThat(merged.dataSignature(), equalTo("ds1")); + assertThat(merged.sharedKeyStatus(), equalTo("sks1")); + assertThat(merged.sharedKeyEnc(), equalTo("ske1")); + assertThat(merged.pubKeyCS(), equalTo("pkcs1")); + assertThat(merged.encoding(), equalTo("utf8")); + assertThat(merged.ivNonce(), equalTo("iv1")); + assertThat(merged.pubKeyHash(), + equalTo(Metadata.PublicKeyHash.builder().hash("hash1").hashingAlgo("algo1").build())); + assertThat(merged.encKeyName(), equalTo("encKeyName1")); + assertThat(merged.encAlgo(), equalTo("encAlgo1")); + assertThat(merged.skeEncKeyName(), equalTo("skeEncKeyName1")); + assertThat(merged.skeEncAlgo(), equalTo("skeEncAlgo1")); + assertThat(merged.immutable(), is(true)); } @Test @@ -173,13 +189,17 @@ void testMergeMd2FieldsUsedWhenMd1FieldsAreNull() { .pubKeyCS("pkcs2").encoding("ascii").ivNonce("iv2") .availableAt(now).expiresAt(now).refreshAt(now) .createdAt(now).updatedAt(now) + .pubKeyHash(Metadata.PublicKeyHash.builder().hash("hash2").hashingAlgo("algo2").build()) + .encKeyName("encKeyName2").encAlgo("encAlgo2") + .skeEncKeyName("skeEncKeyName2").skeEncAlgo("skeEncAlgo2") + .immutable(false) .build(); Metadata merged = Metadata.merge(md1, md2); - assertThat(merged.ttl(), is(1L)); - assertThat(merged.ttb(), is(2L)); - assertThat(merged.ttr(), is(3L)); + assertThat(merged.ttl(), equalTo(1L)); + assertThat(merged.ttb(), equalTo(2L)); + assertThat(merged.ttr(), equalTo(3L)); assertThat(merged.ccd(), is(true)); assertThat(merged.isPublic(), is(true)); assertThat(merged.isHidden(), is(true)); @@ -187,17 +207,24 @@ void testMergeMd2FieldsUsedWhenMd1FieldsAreNull() { assertThat(merged.isEncrypted(), is(true)); assertThat(merged.isBinary(), is(true)); assertThat(merged.namespaceAware(), is(true)); - assertThat(merged.dataSignature(), is("ds2")); - assertThat(merged.sharedKeyStatus(), is("sks2")); - assertThat(merged.sharedKeyEnc(), is("ske2")); - assertThat(merged.pubKeyCS(), is("pkcs2")); - assertThat(merged.encoding(), is("ascii")); - assertThat(merged.ivNonce(), is("iv2")); - assertThat(merged.availableAt(), is(now)); - assertThat(merged.expiresAt(), is(now)); - assertThat(merged.refreshAt(), is(now)); - assertThat(merged.createdAt(), is(now)); - assertThat(merged.updatedAt(), is(now)); + assertThat(merged.dataSignature(), equalTo("ds2")); + assertThat(merged.sharedKeyStatus(), equalTo("sks2")); + assertThat(merged.sharedKeyEnc(), equalTo("ske2")); + assertThat(merged.pubKeyCS(), equalTo("pkcs2")); + assertThat(merged.encoding(), equalTo("ascii")); + assertThat(merged.ivNonce(), equalTo("iv2")); + assertThat(merged.availableAt(), equalTo(now)); + assertThat(merged.expiresAt(), equalTo(now)); + assertThat(merged.refreshAt(), equalTo(now)); + assertThat(merged.createdAt(), equalTo(now)); + assertThat(merged.updatedAt(), equalTo(now)); + assertThat(merged.pubKeyHash(), + equalTo(Metadata.PublicKeyHash.builder().hash("hash2").hashingAlgo("algo2").build())); + assertThat(merged.encKeyName(), equalTo("encKeyName2")); + assertThat(merged.encAlgo(), equalTo("encAlgo2")); + assertThat(merged.skeEncKeyName(), equalTo("skeEncKeyName2")); + assertThat(merged.skeEncAlgo(), equalTo("skeEncAlgo2")); + assertThat(merged.immutable(), is(false)); } @Test @@ -225,6 +252,12 @@ void testMergeFieldRemainsNullWhenBothMd1AndMd2AreNull() { assertThat(merged.refreshAt(), is(nullValue())); assertThat(merged.createdAt(), is(nullValue())); assertThat(merged.updatedAt(), is(nullValue())); + assertThat(merged.pubKeyHash(), is(nullValue())); + assertThat(merged.encKeyName(), is(nullValue())); + assertThat(merged.encAlgo(), is(nullValue())); + assertThat(merged.skeEncKeyName(), is(nullValue())); + assertThat(merged.skeEncAlgo(), is(nullValue())); + assertThat(merged.immutable(), is(nullValue())); } @Test @@ -245,28 +278,28 @@ void testSetTtlIfNotNullReturnsFalseAndDoesNotOverwriteExistingValueWhenNull() { void testSetTtbIfNotNullReturnsTrueAndSetsValueWhenNotNull() { Metadata.MetadataBuilder b = Metadata.builder(); assertThat(Metadata.setTtbIfNotNull(b, 10L), is(true)); - assertThat(b.build().ttb(), is(10L)); + assertThat(b.build().ttb(), equalTo(10L)); } @Test void testSetTtbIfNotNullReturnsFalseAndDoesNotOverwriteExistingValueWhenNull() { Metadata.MetadataBuilder b = Metadata.builder().ttb(10L); assertThat(Metadata.setTtbIfNotNull(b, null), is(false)); - assertThat(b.build().ttb(), is(10L)); + assertThat(b.build().ttb(), equalTo(10L)); } @Test void testSetTtrIfNotNullReturnsTrueAndSetsValueWhenNotNull() { Metadata.MetadataBuilder b = Metadata.builder(); assertThat(Metadata.setTtrIfNotNull(b, 5L), is(true)); - assertThat(b.build().ttr(), is(5L)); + assertThat(b.build().ttr(), equalTo(5L)); } @Test void testSetTtrIfNotNullReturnsFalseAndDoesNotOverwriteExistingValueWhenNull() { Metadata.MetadataBuilder b = Metadata.builder().ttr(5L); assertThat(Metadata.setTtrIfNotNull(b, null), is(false)); - assertThat(b.build().ttr(), is(5L)); + assertThat(b.build().ttr(), equalTo(5L)); } @Test @@ -371,42 +404,42 @@ void testSetNamespaceAwareIfNotNullReturnsFalseAndDoesNotOverwriteExistingValueW void testSetDataSignatureIfNotNullReturnsTrueAndSetsValueWhenNotNull() { Metadata.MetadataBuilder b = Metadata.builder(); assertThat(Metadata.setDataSignatureIfNotNull(b, "sig"), is(true)); - assertThat(b.build().dataSignature(), is("sig")); + assertThat(b.build().dataSignature(), equalTo("sig")); } @Test void testSetDataSignatureIfNotNullReturnsFalseAndDoesNotOverwriteExistingValueWhenNull() { Metadata.MetadataBuilder b = Metadata.builder().dataSignature("sig"); assertThat(Metadata.setDataSignatureIfNotNull(b, null), is(false)); - assertThat(b.build().dataSignature(), is("sig")); + assertThat(b.build().dataSignature(), equalTo("sig")); } @Test void testSetSharedKeyStatusIfNotNullReturnsTrueAndSetsValueWhenNotNull() { Metadata.MetadataBuilder b = Metadata.builder(); assertThat(Metadata.setSharedKeyStatusIfNotNull(b, "ok"), is(true)); - assertThat(b.build().sharedKeyStatus(), is("ok")); + assertThat(b.build().sharedKeyStatus(), equalTo("ok")); } @Test void testSetSharedKeyStatusIfNotNullReturnsFalseAndDoesNotOverwriteExistingValueWhenNull() { Metadata.MetadataBuilder b = Metadata.builder().sharedKeyStatus("ok"); assertThat(Metadata.setSharedKeyStatusIfNotNull(b, null), is(false)); - assertThat(b.build().sharedKeyStatus(), is("ok")); + assertThat(b.build().sharedKeyStatus(), equalTo("ok")); } @Test void testSetSharedKeyEncIfNotNullReturnsTrueAndSetsValueWhenNotNull() { Metadata.MetadataBuilder b = Metadata.builder(); assertThat(Metadata.setSharedKeyEncIfNotNull(b, "encKey"), is(true)); - assertThat(b.build().sharedKeyEnc(), is("encKey")); + assertThat(b.build().sharedKeyEnc(), equalTo("encKey")); } @Test void testSetSharedKeyEncIfNotNullReturnsFalseAndDoesNotOverwriteExistingValueWhenNull() { Metadata.MetadataBuilder b = Metadata.builder().sharedKeyEnc("encKey"); assertThat(Metadata.setSharedKeyEncIfNotNull(b, null), is(false)); - assertThat(b.build().sharedKeyEnc(), is("encKey")); + assertThat(b.build().sharedKeyEnc(), equalTo("encKey")); } @Test @@ -420,34 +453,122 @@ void testSetPubKeyCSIfNotNullReturnsTrueAndSetsValueWhenNotNull() { void testSetPubKeyCSIfNotNullReturnsFalseAndDoesNotOverwriteExistingValueWhenNull() { Metadata.MetadataBuilder b = Metadata.builder().pubKeyCS("checksum"); assertThat(Metadata.setPubKeyCSIfNotNull(b, null), is(false)); - assertThat(b.build().pubKeyCS(), is("checksum")); + assertThat(b.build().pubKeyCS(), equalTo("checksum")); } @Test void testSetEncodingIfNotNullReturnsTrueAndSetsValueWhenNotNull() { Metadata.MetadataBuilder b = Metadata.builder(); assertThat(Metadata.setEncodingIfNotNull(b, "utf8"), is(true)); - assertThat(b.build().encoding(), is("utf8")); + assertThat(b.build().encoding(), equalTo("utf8")); } @Test void testSetEncodingIfNotNullReturnsFalseAndDoesNotOverwriteExistingValueWhenNull() { Metadata.MetadataBuilder b = Metadata.builder().encoding("utf8"); assertThat(Metadata.setEncodingIfNotNull(b, null), is(false)); - assertThat(b.build().encoding(), is("utf8")); + assertThat(b.build().encoding(), equalTo("utf8")); } @Test void testSetIvNonceIfNotNullReturnsTrueAndSetsValueWhenNotNull() { Metadata.MetadataBuilder b = Metadata.builder(); assertThat(Metadata.setIvNonceIfNotNull(b, "iv99"), is(true)); - assertThat(b.build().ivNonce(), is("iv99")); + assertThat(b.build().ivNonce(), equalTo("iv99")); } @Test void testSetIvNonceIfNotNullReturnsFalseAndDoesNotOverwriteExistingValueWhenNull() { Metadata.MetadataBuilder b = Metadata.builder().ivNonce("iv99"); assertThat(Metadata.setIvNonceIfNotNull(b, null), is(false)); - assertThat(b.build().ivNonce(), is("iv99")); + assertThat(b.build().ivNonce(), equalTo("iv99")); + } + + @Test + void testSetPubKeyHashIfNotNullReturnsTrueAndSetsValueWhenNotNull() { + Metadata.MetadataBuilder b = Metadata.builder(); + Metadata.PublicKeyHash hash = Metadata.PublicKeyHash.builder().hash("HASH").hashingAlgo("algo").build(); + assertThat(Metadata.setPubKeyHashIfNotNull(b, hash), is(true)); + assertThat(b.build().pubKeyHash(), equalTo(hash)); + } + + @Test + void testSetPubKeyHashIfNotNullReturnsFalseAndDoesNotOverwriteExistingValueWhenNull() { + Metadata.PublicKeyHash hash = Metadata.PublicKeyHash.builder().hash("HASH").hashingAlgo("algo").build(); + Metadata.MetadataBuilder b = Metadata.builder().pubKeyHash(hash); + assertThat(Metadata.setPubKeyHashIfNotNull(b, null), is(false)); + assertThat(b.build().pubKeyHash(), equalTo(hash)); + } + + @Test + void testSetEncKeyNameIfNotNullReturnsTrueAndSetsValueWhenNotNull() { + Metadata.MetadataBuilder b = Metadata.builder(); + assertThat(Metadata.setEncKeyNameIfNotNull(b, "encKeyName"), is(true)); + assertThat(b.build().encKeyName(), equalTo("encKeyName")); + } + + @Test + void testSetEncKeyNameIfNotNullReturnsFalseAndDoesNotOverwriteExistingValueWhenNull() { + Metadata.MetadataBuilder b = Metadata.builder().encKeyName("encKeyName"); + assertThat(Metadata.setEncKeyNameIfNotNull(b, null), is(false)); + assertThat(b.build().encKeyName(), equalTo("encKeyName")); + } + + @Test + void testSetEncAlgoIfNotNullReturnsTrueAndSetsValueWhenNotNull() { + Metadata.MetadataBuilder b = Metadata.builder(); + assertThat(Metadata.setEncAlgoIfNotNull(b, "encAlgo"), is(true)); + assertThat(b.build().encAlgo(), equalTo("encAlgo")); + } + + @Test + void testSetEncAlgoIfNotNullReturnsFalseAndDoesNotOverwriteExistingValueWhenNull() { + Metadata.MetadataBuilder b = Metadata.builder().encAlgo("encAlgo"); + assertThat(Metadata.setEncAlgoIfNotNull(b, null), is(false)); + assertThat(b.build().encAlgo(), equalTo("encAlgo")); + } + + @Test + void testSetSkeEncKeyNameIfNotNullReturnsTrueAndSetsValueWhenNotNull() { + Metadata.MetadataBuilder b = Metadata.builder(); + assertThat(Metadata.setSkeEncKeyNameIfNotNull(b, "skeEncAlgo"), is(true)); + assertThat(b.build().skeEncKeyName(), equalTo("skeEncAlgo")); + } + + @Test + void testSetSkeEncKeyNameIfNotNullReturnsFalseAndDoesNotOverwriteExistingValueWhenNull() { + Metadata.MetadataBuilder b = Metadata.builder().skeEncKeyName("skeEncAlgo"); + assertThat(Metadata.setSkeEncKeyNameIfNotNull(b, null), is(false)); + assertThat(b.build().skeEncKeyName(), equalTo("skeEncAlgo")); + } + + @Test + void testSetSkeEncAlgoIfNotNullReturnsTrueAndSetsValueWhenNotNull() { + Metadata.MetadataBuilder b = Metadata.builder(); + assertThat(Metadata.setSkeEncAlgoIfNotNull(b, "skeEncAlgo"), is(true)); + assertThat(b.build().skeEncAlgo(), equalTo("skeEncAlgo")); + } + + @Test + void testSetSkeEncAlgoIfNotNullReturnsFalseAndDoesNotOverwriteExistingValueWhenNull() { + Metadata.MetadataBuilder b = Metadata.builder().skeEncAlgo("skeEncAlgo"); + assertThat(Metadata.setSkeEncAlgoIfNotNull(b, null), is(false)); + assertThat(b.build().skeEncAlgo(), equalTo("skeEncAlgo")); + } + + + @Test + void testSetImmutableIfNotNullReturnsTrueAndSetsValueWhenNotNull() { + Metadata.MetadataBuilder b = Metadata.builder(); + assertThat(Metadata.setImmutableIfNotNull(b, false), is(true)); + assertThat(b.build().immutable(), is(false)); } + + @Test + void testSetImmutableIfNotNullReturnsFalseAndDoesNotOverwriteExistingValueWhenNull() { + Metadata.MetadataBuilder b = Metadata.builder().immutable(true); + assertThat(Metadata.setImmutableIfNotNull(b, null), is(false)); + assertThat(b.build().immutable(), is(true)); + } + } 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 8512fdc6..fbeb590a 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 @@ -420,14 +420,16 @@ void testGetSharedKey() throws Exception { String iv = generateRandomIvBase64(16); String encrypted1 = aesEncryptToBase64("hello from me", encryptKey, iv); String encrypted2 = aesEncryptToBase64("greetings from another world", encryptKey, iv); + String sharedKeyEnc = rsaEncryptToBase64(encryptKey, keys.getEncryptPublicKey()); AtCommandExecutor executor = TestExecutorBuilder.builder() - .stub("llookup:shared_key.another@test", "data:" + rsaEncryptToBase64(encryptKey, keys.getEncryptPublicKey())) - .stubLookupResponse("llookup:all:@another:key1@test", "key1@test", encrypted1, "ivNonce", iv) + .stub("llookup:shared_key.another@test", "data:" + sharedKeyEnc) + .stubLookupResponse("llookup:all:@another:key1@test", "key1@test", encrypted1, + "ivNonce", iv, "sharedKeyEnc", sharedKeyEnc) .stub("llookup:all:public:key2@test", "error:AT0001:deliberate") .stubExecutionException("llookup:all:public:key3@test") - .stub("lookup:shared_key@another", "data:" + rsaEncryptToBase64(encryptKey, keys.getEncryptPublicKey())) - .stubLookupResponse("lookup:all:key4@another", "key4@test", encrypted2, "ivNonce", iv) + .stubLookupResponse("lookup:all:key4@another", "key4@test", encrypted2, + "ivNonce", iv, "sharedKeyEnc", sharedKeyEnc) .build(); AtClientImpl client = AtClientImpl.builder().atSign(atSign).keys(keys).executor(executor).eventBus(bus).build(); @@ -458,13 +460,15 @@ void testGetSharedKeyBinary() throws Exception { String encrypted1 = aesEncryptToBase64(Base2e15Utils.encode(bytes1), encryptKey, iv); String encrypted2 = aesEncryptToBase64(Base2e15Utils.encode(bytes2), encryptKey, iv); + String sharedKeyEnc = rsaEncryptToBase64(encryptKey, keys.getEncryptPublicKey()); AtCommandExecutor executor = TestExecutorBuilder.builder() - .stub("llookup:shared_key.another@test", "data:" + rsaEncryptToBase64(encryptKey, keys.getEncryptPublicKey())) - .stub("lookup:shared_key@another", "data:" + rsaEncryptToBase64(encryptKey, keys.getEncryptPublicKey())) - .stubLookupResponse("llookup:all:@another:key1@test", "key1@test", encrypted1, "ivNonce", iv, "isBinary", true) + .stub("llookup:shared_key.another@test", "data:" + sharedKeyEnc) + .stubLookupResponse("llookup:all:@another:key1@test", "key1@test", encrypted1, + "ivNonce", iv, "isBinary", true, "sharedKeyEnc", sharedKeyEnc) .stub("llookup:all:public:key2@test", "error:AT0001:deliberate") .stubExecutionException("llookup:all:public:key3@test") - .stubLookupResponse("lookup:all:key4@another", "key4@test", encrypted2, "ivNonce", iv, "isBinary", true) + .stubLookupResponse("lookup:all:key4@another", "key4@test", encrypted2, + "ivNonce", iv, "isBinary", true, "sharedKeyEnc", sharedKeyEnc) .stubLookupResponse("llookup:all:@another:key5@test", "key5@test", encrypted1, "ivNonce", iv) .build(); diff --git a/at_client/src/test/java/org/atsign/client/impl/AtClientsIT.java b/at_client/src/test/java/org/atsign/client/impl/AtClientsIT.java new file mode 100644 index 00000000..eededce2 --- /dev/null +++ b/at_client/src/test/java/org/atsign/client/impl/AtClientsIT.java @@ -0,0 +1,95 @@ +package org.atsign.client.impl; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +import java.io.File; + +import org.atsign.client.api.AtClient; +import org.atsign.client.api.AtKeys; +import org.atsign.client.api.AtSign; +import org.atsign.client.impl.common.ReconnectStrategy; +import org.atsign.client.impl.util.KeysUtils; +import org.atsign.cucumber.helpers.Helpers; +import org.atsign.virtualenv.VirtualEnv; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class AtClientsIT { + + private static AtSign atSign; + private static AtKeys keys; + private static File keysFile; + public static String AT_SIGN_KEYS_DIR; + public static String ATSIGN_KEYS_SUFFIX; + + @BeforeAll + public static void classSetup() throws Exception { + if (!Helpers.isHostPortReachable("vip.ve.atsign.zone:64", SECONDS.toMillis(2))) { + VirtualEnv.setUp(); + } + atSign = AtSign.createAtSign("colin"); + keysFile = new File("target/at_demo_data/lib/assets/atkeys/@colin.atKeys"); + keys = KeysUtils.loadKeys(keysFile); + AT_SIGN_KEYS_DIR = KeysUtils.expectedKeysFilesLocation; + ATSIGN_KEYS_SUFFIX = KeysUtils.keysFileSuffix; + KeysUtils.expectedKeysFilesLocation = "target/at_demo_data/lib/assets/atkeys"; + KeysUtils.keysFileSuffix = ".atKeys"; + } + + @AfterAll + public static void classTeardown() { + KeysUtils.expectedKeysFilesLocation = AT_SIGN_KEYS_DIR; + KeysUtils.keysFileSuffix = ATSIGN_KEYS_SUFFIX; + } + + @Test + void testBuildWithoutKeys() throws Exception { + AtClients.AtClientBuilder builder = AtClients.builder() + .url("vip.ve.atsign.zone") + .atSign(atSign) + .reconnect(ReconnectStrategy.NONE); + try (AtClient client = builder.build()) { + assertThat(client.getAtKeys(".*", false).get(), is(not(empty()))); + } + } + + @Test + void testBuildWithKeys() throws Exception { + AtClients.AtClientBuilder builder = AtClients.builder() + .url("vip.ve.atsign.zone") + .atSign(atSign) + .keys(keys) + .reconnect(ReconnectStrategy.NONE); + try (AtClient client = builder.build()) { + assertThat(client.getAtKeys(".*", false).get(), is(not(empty()))); + } + } + + @Test + void testBuildWithKeysPathSetToDirectory() throws Exception { + AtClients.AtClientBuilder builder = AtClients.builder() + .url("vip.ve.atsign.zone") + .atSign(atSign) + .keysPath(keysFile.getParentFile().getAbsolutePath()) + .reconnect(ReconnectStrategy.NONE); + try (AtClient client = builder.build()) { + assertThat(client.getAtKeys(".*", false).get(), is(not(empty()))); + } + } + + @Test + void testBuildWithKeysPathSetToFile() throws Exception { + AtClients.AtClientBuilder builder = AtClients.builder() + .url("vip.ve.atsign.zone") + .atSign(atSign) + .keysPath(keysFile.getAbsolutePath()) + .reconnect(ReconnectStrategy.NONE); + try (AtClient client = builder.build()) { + assertThat(client.getAtKeys(".*", false).get(), is(not(empty()))); + } + } + +} 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 9b7519e1..c52d60dd 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 @@ -13,6 +13,9 @@ import org.atsign.client.impl.exceptions.AtUnauthenticatedException; import org.junit.jupiter.api.Test; +import java.util.Collections; +import java.util.Map; + public class AuthenticationCommandsTest { @Test @@ -60,6 +63,18 @@ public void testAuthenticateWithApkamWithEnrollmentId() throws Exception { AuthenticationCommands.authenticateWithPkam(executor, createAtSign("@alice"), keys); } + @Test + public void testAuthenticateWithApkamWithConfig() throws Exception { + AtKeys keys = AtKeys.builder().apkamKeyPair(generateRSAKeyPair()).enrollmentId(createEnrollmentId("12345")).build(); + AtCommandExecutor executor = TestExecutorBuilder.builder() + .stub("from:@alice:clientConfig:.+", "data:challenge") + .stub("pkam:signingAlgo:rsa2048:hashingAlgo:sha256:enrollmentId:12345:.+", "data:success") + .build(); + + Map config = Collections.singletonMap("clientVersion", "1.2.3"); + AuthenticationCommands.authenticateWithPkam(executor, createAtSign("@alice"), keys, config); + } + @Test public void testAuthenticateWithApkamFailThrowsExpectedException() throws Exception { AtKeys keys = AtKeys.builder().apkamKeyPair(generateRSAKeyPair()).enrollmentId(createEnrollmentId("12345")).build(); @@ -83,15 +98,9 @@ public void testPkamAuthenticatorThrowsOnReadyException() throws Exception { .build(); Exception ex = assertThrows(Exception.class, - () -> AuthenticationCommands.pkamAuthenticator(createAtSign("@alice"), keys) + () -> AuthenticationCommands.pkamAuthenticator(createAtSign("@alice"), keys, null) .accept(executor)); assertThat(ex, instanceOf(AtOnReadyException.class)); assertThat(ex.getMessage(), containsString("deliberate")); } - - @Test - public void testBytesToHex() throws Exception { - byte[] bytes = new byte[] {(byte) 0x00, (byte) 0x0f, (byte) 0xff}; - assertThat(AuthenticationCommands.bytesToHex(bytes), is("000fff")); - } } diff --git a/at_client/src/test/java/org/atsign/client/impl/commands/CommandBuildersTest.java b/at_client/src/test/java/org/atsign/client/impl/commands/CommandBuildersTest.java index 9136c86b..641197fe 100644 --- a/at_client/src/test/java/org/atsign/client/impl/commands/CommandBuildersTest.java +++ b/at_client/src/test/java/org/atsign/client/impl/commands/CommandBuildersTest.java @@ -1,13 +1,14 @@ package org.atsign.client.impl.commands; import static java.util.concurrent.TimeUnit.SECONDS; -import static org.atsign.client.impl.common.EnrollmentId.createEnrollmentId; import static org.atsign.client.api.AtSign.createAtSign; +import static org.atsign.client.impl.common.EnrollmentId.createEnrollmentId; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -36,7 +37,35 @@ public void testFromBuilderGeneratesExpectedOutput() { String command = CommandBuilders.fromCommandBuilder() .atSign(createAtSign("@bob")) .build(); - assertEquals("from:@bob", command); + assertThat(command, equalTo("from:@bob")); + } + + @Test + public void testFromBuilderWithEmptyConfigGeneratesExpectedOutput() { + + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> CommandBuilders.fromCommandBuilder().build()); + assertThat(ex.getMessage(), containsString("atSign not set")); + + String command = CommandBuilders.fromCommandBuilder() + .atSign(createAtSign("@bob")) + .config(new HashMap<>()) + .build(); + assertThat(command, equalTo("from:@bob")); + } + + @Test + public void testFromBuilderWithConfigGeneratesExpectedOutput() { + + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> CommandBuilders.fromCommandBuilder().build()); + assertThat(ex.getMessage(), containsString("atSign not set")); + + String command = CommandBuilders.fromCommandBuilder() + .atSign(createAtSign("@bob")) + .config(Collections.singletonMap("clientVersion", "1.2.3")) + .build(); + assertThat(command, equalTo("from:@bob:clientConfig:{\"clientVersion\":\"1.2.3\"}")); } @Test @@ -49,13 +78,13 @@ public void testCramBuilderGeneratesExpectedOutput() { String command = CommandBuilders.cramCommandBuilder() .digest("digest") .build(); - assertEquals("cram:digest", command); + assertThat(command, equalTo("cram:digest")); } @Test public void testPolBuilderGeneratesExpectedOutput() { String command = CommandBuilders.polCommandBuilder().build(); - assertEquals("pol", command); + assertThat(command, equalTo("pol")); } @Test @@ -68,7 +97,7 @@ public void testPkamBuilderGeneratesExpectedOutput() { String command = CommandBuilders.pkamCommandBuilder() .digest("digest") .build(); - assertEquals("pkam:digest", command); + assertThat(command, equalTo("pkam:digest")); } @Test @@ -95,7 +124,7 @@ public void testPkamBuilderGeneratesExpectedOutputWhenEnrollmentIdIsSet() { .signingAlgo("RSA") .hashingAlgo("SHA") .build(); - assertEquals("pkam:signingAlgo:RSA:hashingAlgo:SHA:enrollmentId:12345-6789:digest", command); + assertThat(command, equalTo("pkam:signingAlgo:RSA:hashingAlgo:SHA:enrollmentId:12345-6789:digest")); } @Test @@ -253,7 +282,7 @@ public void testUpdateBuilderGeneratesExpectedOutput() { .sharedBy(createAtSign("@bob")) .value("my Value 123") .build(); - assertEquals("update:test@bob my Value 123", command); + assertThat(command, equalTo("update:test@bob my Value 123")); // self key but shared with self command = CommandBuilders.updateCommandBuilder() @@ -262,7 +291,7 @@ public void testUpdateBuilderGeneratesExpectedOutput() { .sharedWith(createAtSign("bob")) .value("My value 123") .build(); - assertEquals("update:@bob:test@bob My value 123", command); + assertThat(command, equalTo("update:@bob:test@bob My value 123")); // public key command = CommandBuilders.updateCommandBuilder() @@ -271,7 +300,7 @@ public void testUpdateBuilderGeneratesExpectedOutput() { .isPublic(true) .value("my Value 123") .build(); - assertEquals("update:public:publickey@bob my Value 123", command); + assertThat(command, equalTo("update:public:publickey@bob my Value 123")); // cached public key command = CommandBuilders.updateCommandBuilder() @@ -281,7 +310,7 @@ public void testUpdateBuilderGeneratesExpectedOutput() { .isCached(true) .value("my Value 123") .build(); - assertEquals("update:cached:public:publickey@alice my Value 123", command); + assertThat(command, equalTo("update:cached:public:publickey@alice my Value 123")); // shared key command = CommandBuilders.updateCommandBuilder() @@ -290,7 +319,7 @@ public void testUpdateBuilderGeneratesExpectedOutput() { .sharedWith(createAtSign("@alice")) .value("my Value 123") .build(); - assertEquals("update:@alice:sharedkey@bob my Value 123", command); + assertThat(command, equalTo("update:@alice:sharedkey@bob my Value 123")); // with shared key SharedKey sk1 = Keys.sharedKeyBuilder() @@ -304,7 +333,8 @@ public void testUpdateBuilderGeneratesExpectedOutput() { .key(sk1) .value("myBinaryValue123456") .build(); - assertEquals("update:ttl:600000:isBinary:true:isEncrypted:true:@alice:test@bob myBinaryValue123456", command); + assertThat(command, + equalTo("update:ttl:600000:isBinary:true:isEncrypted:true:@alice:test@bob myBinaryValue123456")); // with public key PublicKey pk1 = Keys.publicKeyBuilder() @@ -316,7 +346,7 @@ public void testUpdateBuilderGeneratesExpectedOutput() { .key(pk1) .value("myValue123") .build(); - assertEquals("update:isEncrypted:false:cached:public:test@bob myValue123", command); + assertThat(command, equalTo("update:isEncrypted:false:cached:public:test@bob myValue123")); // with self key SelfKey sk2 = Keys.selfKeyBuilder() @@ -328,7 +358,7 @@ public void testUpdateBuilderGeneratesExpectedOutput() { .key(sk2) .value("myValue123") .build(); - assertEquals("update:ttl:600000:isEncrypted:true:test@bob myValue123", command); + assertThat(command, equalTo("update:ttl:600000:isEncrypted:true:test@bob myValue123")); // with self key (shared with self) AtSign bob = createAtSign("@bob"); @@ -342,7 +372,7 @@ public void testUpdateBuilderGeneratesExpectedOutput() { .key(sk3) .value("myValue123") .build(); - assertEquals("update:ttl:600000:isEncrypted:true:@bob:test@bob myValue123", command); + assertThat(command, equalTo("update:ttl:600000:isEncrypted:true:@bob:test@bob myValue123")); // private hidden key // TODO with private hidden key when implemented @@ -438,7 +468,7 @@ public void testLlookupBuilderGeneratesExpectedOutput() { .keyName("test") .sharedBy(createAtSign("@alice")) .build(); - assertEquals("llookup:test@alice", command); + assertThat(command, equalTo("llookup:test@alice")); // Type.METADATA self key command = CommandBuilders.llookupCommandBuilder() @@ -446,7 +476,7 @@ public void testLlookupBuilderGeneratesExpectedOutput() { .sharedBy(createAtSign("@alice")) .operation(LookupOperation.meta) .build(); - assertEquals("llookup:meta:test@alice", command); + assertThat(command, equalTo("llookup:meta:test@alice")); // hidden self key, meta command = CommandBuilders.llookupCommandBuilder() @@ -455,7 +485,7 @@ public void testLlookupBuilderGeneratesExpectedOutput() { .operation(LookupOperation.meta) .isHidden(true) .build(); - assertEquals("llookup:meta:_test@alice", command); + assertThat(command, equalTo("llookup:meta:_test@alice")); // Type.ALL public cached key command = CommandBuilders.llookupCommandBuilder() @@ -465,7 +495,7 @@ public void testLlookupBuilderGeneratesExpectedOutput() { .isPublic(true) .operation(LookupOperation.all) .build(); - assertEquals("llookup:all:cached:public:publickey@alice", command); + assertThat(command, equalTo("llookup:all:cached:public:publickey@alice")); // no key name IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, @@ -489,12 +519,12 @@ public void testLlookupBuilderGeneratesExpectedOutput() { .key(pk) .operation(LookupOperation.meta) .build(); - assertEquals("llookup:meta:public:publickey@bob", command); + assertThat(command, equalTo("llookup:meta:public:publickey@bob")); command = CommandBuilders.llookupCommandBuilder() .rawKey("public:publickey@bob") .operation(LookupOperation.meta) .build(); - assertEquals("llookup:meta:public:publickey@bob", command); + assertThat(command, equalTo("llookup:meta:public:publickey@bob")); // with shared key SharedKey sk = Keys.sharedKeyBuilder() @@ -506,7 +536,7 @@ public void testLlookupBuilderGeneratesExpectedOutput() { .key(sk) .operation(LookupOperation.none) .build(); - assertEquals("llookup:@alice:sharedkey@bob", command); + assertThat(command, equalTo("llookup:@alice:sharedkey@bob")); // with self key SelfKey selfKey1 = Keys.selfKeyBuilder().sharedBy(createAtSign("@bob")).name("test").build(); @@ -514,7 +544,7 @@ public void testLlookupBuilderGeneratesExpectedOutput() { .key(selfKey1) .operation(LookupOperation.all) .build(); // "llookup:all:test@bob" - assertEquals("llookup:all:test@bob", command); + assertThat(command, equalTo("llookup:all:test@bob")); // with self key (shared with self) AtSign as = createAtSign("@bob"); @@ -523,7 +553,7 @@ public void testLlookupBuilderGeneratesExpectedOutput() { .key(selfKey2) .operation(LookupOperation.all) .build(); - assertEquals("llookup:all:@bob:test@bob", command); + assertThat(command, equalTo("llookup:all:@bob:test@bob")); // with cached public key PublicKey pk2 = Keys.publicKeyBuilder() @@ -535,7 +565,7 @@ public void testLlookupBuilderGeneratesExpectedOutput() { .key(pk2) .operation(LookupOperation.all) .build(); - assertEquals("llookup:all:cached:public:publickey@bob", command); + assertThat(command, equalTo("llookup:all:cached:public:publickey@bob")); // with cached shared key SharedKey sk2 = Keys.sharedKeyBuilder() @@ -548,7 +578,7 @@ public void testLlookupBuilderGeneratesExpectedOutput() { .key(sk2) .operation(LookupOperation.none) .build(); - assertEquals("llookup:cached:@alice:sharedkey@bob", command); + assertThat(command, equalTo("llookup:cached:@alice:sharedkey@bob")); // with private hidden key // TODO: not implemented yet @@ -605,11 +635,11 @@ public void lookupVerbBuilderTest() { .keyName("test") .sharedBy(createAtSign("@alice")) .build(); - assertEquals("lookup:test@alice", command); + assertThat(command, equalTo("lookup:test@alice")); command = CommandBuilders.lookupCommandBuilder() .rawKey("test@alice") .build(); - assertEquals("lookup:test@alice", command); + assertThat(command, equalTo("lookup:test@alice")); // Type.METADATA command = CommandBuilders.lookupCommandBuilder() @@ -617,7 +647,7 @@ public void lookupVerbBuilderTest() { .sharedBy(createAtSign("@alice")) .operation(LookupOperation.meta) .build(); - assertEquals("lookup:meta:test@alice", command); + assertThat(command, equalTo("lookup:meta:test@alice")); // Type.ALL command = CommandBuilders.lookupCommandBuilder() @@ -625,7 +655,7 @@ public void lookupVerbBuilderTest() { .sharedBy(createAtSign("@alice")) .operation(LookupOperation.all) .build(); // "lookup:test@alice" - assertEquals("lookup:all:test@alice", command); + assertThat(command, equalTo("lookup:all:test@alice")); // no key name IllegalArgumentException ex = @@ -647,7 +677,7 @@ public void lookupVerbBuilderTest() { .key(sk) .operation(LookupOperation.meta) .build(); - assertEquals("lookup:meta:test@sharedby", command); + assertThat(command, equalTo("lookup:meta:test@sharedby")); } @Test @@ -673,11 +703,11 @@ public void plookupVerbBuilderTest() { .keyName("publickey") .sharedBy(createAtSign("@alice")) .build(); // "plookup:publickey@alice" - assertEquals("plookup:publickey@alice", command); + assertThat(command, equalTo("plookup:publickey@alice")); command = CommandBuilders.plookupCommandBuilder() .rawKey("publickey@alice") .build(); // "plookup:publickey@alice" - assertEquals("plookup:publickey@alice", command); + assertThat(command, equalTo("plookup:publickey@alice")); // Type.METADATA command = CommandBuilders.plookupCommandBuilder() @@ -685,7 +715,7 @@ public void plookupVerbBuilderTest() { .sharedBy(createAtSign("@alice")) .operation(LookupOperation.meta) .build(); - assertEquals("plookup:meta:publickey@alice", command); + assertThat(command, equalTo("plookup:meta:publickey@alice")); // Type.ALL command = CommandBuilders.plookupCommandBuilder() @@ -693,7 +723,7 @@ public void plookupVerbBuilderTest() { .sharedBy(createAtSign("@alice")) .operation(LookupOperation.all) .build(); - assertEquals("plookup:all:publickey@alice", command); + assertThat(command, equalTo("plookup:all:publickey@alice")); // no key IllegalArgumentException ex = @@ -714,7 +744,7 @@ public void plookupVerbBuilderTest() { .key(pk) .operation(LookupOperation.all) .build(); - assertEquals("plookup:all:publickey@bob", command); + assertThat(command, equalTo("plookup:all:publickey@bob")); // bypasscache true command = CommandBuilders.plookupCommandBuilder() @@ -723,7 +753,7 @@ public void plookupVerbBuilderTest() { .bypassCache(true) .operation(LookupOperation.all) .build(); - assertEquals("plookup:bypassCache:true:all:publickey@alice", command); + assertThat(command, equalTo("plookup:bypassCache:true:all:publickey@alice")); } @Test @@ -751,11 +781,11 @@ public void deleteVerbBuilderTest() { .keyName("publickey") .sharedBy(createAtSign("@alice")) .build(); - assertEquals("delete:public:publickey@alice", command); + assertThat(command, equalTo("delete:public:publickey@alice")); command = CommandBuilders.deleteCommandBuilder() .rawKey("public:publickey@alice") .build(); - assertEquals("delete:public:publickey@alice", command); + assertThat(command, equalTo("delete:public:publickey@alice")); // delete a cached public key command = CommandBuilders.deleteCommandBuilder() @@ -764,14 +794,14 @@ public void deleteVerbBuilderTest() { .keyName("publickey") .sharedBy(createAtSign("@bob")) .build(); - assertEquals("delete:cached:public:publickey@bob", command); + assertThat(command, equalTo("delete:cached:public:publickey@bob")); // delete a self key command = CommandBuilders.deleteCommandBuilder() .keyName("test") .sharedBy(createAtSign("@alice")) .build(); - assertEquals("delete:test@alice", command); + assertThat(command, equalTo("delete:test@alice")); // delete a hidden self key command = CommandBuilders.deleteCommandBuilder() @@ -779,7 +809,7 @@ public void deleteVerbBuilderTest() { .keyName("test") .sharedBy(createAtSign("@alice")) .build(); - assertEquals("delete:_test@alice", command); + assertThat(command, equalTo("delete:_test@alice")); // delete a shared key command = CommandBuilders.deleteCommandBuilder() @@ -787,7 +817,7 @@ public void deleteVerbBuilderTest() { .sharedBy(createAtSign("@alice")) .sharedWith(createAtSign("@bob")) .build(); - assertEquals("delete:@bob:test@alice", command); + assertThat(command, equalTo("delete:@bob:test@alice")); // delete a cached shared key command = CommandBuilders.deleteCommandBuilder() @@ -796,7 +826,7 @@ public void deleteVerbBuilderTest() { .sharedBy(createAtSign("@alice")) .sharedWith(createAtSign("@bob")) .build(); - assertEquals("delete:cached:@bob:test@alice", command); + assertThat(command, equalTo("delete:cached:@bob:test@alice")); // missing key name IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, @@ -816,7 +846,7 @@ public void deleteVerbBuilderTest() { command = CommandBuilders.deleteCommandBuilder() .key(selfKey) .build(); - assertEquals("delete:test@alice", command); + assertThat(command, equalTo("delete:test@alice")); // with public key PublicKey pk = Keys.publicKeyBuilder().sharedBy(createAtSign("@bob")).name("publickey").build(); @@ -833,7 +863,7 @@ public void deleteVerbBuilderTest() { command = CommandBuilders.deleteCommandBuilder() .key(sk) .build(); - assertEquals("delete:@bob:test@alice", command); + assertThat(command, equalTo("delete:@bob:test@alice")); } @Test @@ -880,45 +910,45 @@ public void scanVerbBuilderTest() { // Test not setting any parameters String command = CommandBuilders.scanCommandBuilder().build(); - assertEquals("scan", command); + assertThat(command, equalTo("scan")); // Test setting just regex command = CommandBuilders.scanCommandBuilder().regex("*.public") .build(); - assertEquals("scan *.public", command); + assertThat(command, equalTo("scan *.public")); // Test setting just fromAtSign command = CommandBuilders.scanCommandBuilder() .fromAtSign(createAtSign("@other")) .build(); - assertEquals("scan:@other", command); + assertThat(command, equalTo("scan:@other")); // Test seting just showHidden command = CommandBuilders.scanCommandBuilder() .showHidden(true) .build(); - assertEquals("scan:showHidden:true", command); + assertThat(command, equalTo("scan:showHidden:true")); // Test setting regex & fromAtSign command = CommandBuilders.scanCommandBuilder() .regex("*.public") .fromAtSign(createAtSign("@other")) .build(); - assertEquals("scan:@other *.public", command); + assertThat(command, equalTo("scan:@other *.public")); // Test setting regex & showHidden command = CommandBuilders.scanCommandBuilder() .regex("*.public") .showHidden(true) .build(); - assertEquals("scan:showHidden:true *.public", command); + assertThat(command, equalTo("scan:showHidden:true *.public")); // Test setting fromAtSign & showHidden command = CommandBuilders.scanCommandBuilder() .fromAtSign(createAtSign("@other")) .showHidden(true) .build(); - assertEquals("scan:showHidden:true:@other", command); + assertThat(command, equalTo("scan:showHidden:true:@other")); // Test setting regex & fromAtSign & showHidden command = CommandBuilders.scanCommandBuilder() @@ -926,7 +956,7 @@ public void scanVerbBuilderTest() { .fromAtSign(createAtSign("@other")) .showHidden(true) .build(); - assertEquals("scan:showHidden:true:@other *.public", command); + assertThat(command, equalTo("scan:showHidden:true:@other *.public")); } @Test @@ -945,7 +975,7 @@ public void testNotifyTextBuilderGeneratesTheExpectedOutput() { .recipient(createAtSign("@test")) .text("Hi") .build(); - assertEquals("notify:messageType:text:@test:Hi", command); + assertThat(command, equalTo("notify:messageType:text:@test:Hi")); } @Test @@ -990,14 +1020,14 @@ public void notifyKeyChangeBuilderTest() { .recipient(createAtSign("recipient")) .key("phone") .build(); - assertEquals("notify:update:messageType:key:@recipient:phone@sender", command); + assertThat(command, equalTo("notify:update:messageType:key:@recipient:phone@sender")); // test command with a fully formed key command = CommandBuilders.notifyKeyChangeCommandBuilder() .operation(NotifyOperation.update) .key("@recipient:phone@sender") .build(); - assertEquals("notify:update:messageType:key:@recipient:phone@sender", command); + assertThat(command, equalTo("notify:update:messageType:key:@recipient:phone@sender")); // test command when ttr and value are present command = CommandBuilders.notifyKeyChangeCommandBuilder() @@ -1006,7 +1036,7 @@ public void notifyKeyChangeBuilderTest() { .ttr(1000L) .value("cache_me") .build(); - assertEquals("notify:update:messageType:key:ttr:1000:@recipient:phone@sender:cache_me", command); + assertThat(command, equalTo("notify:update:messageType:key:ttr:1000:@recipient:phone@sender:cache_me")); } @Test @@ -1019,7 +1049,7 @@ public void notificationStatusVerbBuilderTest() { String command = CommandBuilders.notifyStatusCommandBuilder() .notificationId("n1234").build(); - assertEquals("notify:status:n1234", command); + assertThat(command, equalTo("notify:status:n1234")); } @Test @@ -1326,7 +1356,7 @@ void testSubsequentEnrollRequestReturnsExpectedCommand() { @Test public void testOtpBuilderGeneratesExpectedOutput() { String command = CommandBuilders.otpCommandBuilder().build(); - assertEquals("otp:get", command); + assertThat(command, equalTo("otp:get")); } @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 93610411..d4a6ce67 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 @@ -1,7 +1,7 @@ package org.atsign.client.impl.commands; -import static org.atsign.client.impl.util.EncryptionUtils.generateRSAKeyPair; 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.*; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -12,9 +12,9 @@ import java.util.concurrent.ExecutionException; import java.util.function.Consumer; +import org.atsign.client.api.AtCommandExecutor; import org.atsign.client.api.AtEvents; import org.atsign.client.api.AtKeys; -import org.atsign.client.api.AtCommandExecutor; import org.atsign.client.api.AtSign; import org.atsign.client.impl.exceptions.AtOnReadyException; import org.atsign.client.impl.exceptions.AtTimeoutException; @@ -49,7 +49,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, keys, consumer).accept(executor)); + Exception ex = + assertThrows(Exception.class, () -> Notifications.monitor(atSign, keys, null, consumer).accept(executor)); assertThat(ex, instanceOf(AtOnReadyException.class)); } 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 c694bfd2..0c85db67 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 @@ -1,7 +1,7 @@ package org.atsign.client.impl.commands; -import static org.atsign.client.impl.util.EncryptionUtils.*; import static org.atsign.client.api.AtSign.createAtSign; +import static org.atsign.client.impl.util.EncryptionUtils.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; @@ -9,10 +9,13 @@ import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.verify; -import org.atsign.client.api.AtKeys; import org.atsign.client.api.AtCommandExecutor; +import org.atsign.client.api.AtKeys; import org.atsign.client.api.Keys; +import org.atsign.client.api.Metadata; +import org.atsign.client.impl.exceptions.AtPublicKeyChangeException; import org.atsign.client.impl.exceptions.AtServerRuntimeException; +import org.atsign.client.impl.util.EncryptionUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -103,9 +106,30 @@ void getGetSharedByOther() throws Exception { String encryptKey = generateAESKeyBase64(); String iv = generateRandomIvBase64(16); String encrypted = aesEncryptToBase64("hello colin", encryptKey, iv); + String sharedKeyEnc = rsaEncryptToBase64(encryptKey, keys.getEncryptPublicKey()); + Metadata.PublicKeyHash hash = Metadata.PublicKeyHash.builder() + .hash(EncryptionUtils.digest(keys.getEncryptPublicKey(), HASHING_ALGO_SHA512)) + .hashingAlgo(HASHING_ALGO_SHA512) + .build(); + AtCommandExecutor executor = TestExecutorBuilder.builder() + .stubLookupResponse("lookup:all:test@gary", "@colin:test@gary", encrypted, + "ivNonce", iv, "sharedKeyEnc", sharedKeyEnc, "pubKeyHash", hash) + .build(); + + String actual = SharedKeyCommands.get(executor, createAtSign("colin"), keys, key); + + assertThat(actual, equalTo("hello colin")); + } + + @Test + void getGetSharedByOtherBackwardCompatibilityCase() throws Exception { + String encryptKey = generateAESKeyBase64(); + String iv = generateRandomIvBase64(16); + String encrypted = aesEncryptToBase64("hello colin", encryptKey, iv); + String sharedKeyEnc = rsaEncryptToBase64(encryptKey, keys.getEncryptPublicKey()); AtCommandExecutor executor = TestExecutorBuilder.builder() .stubLookupResponse("lookup:all:test@gary", "@colin:test@gary", encrypted, "ivNonce", iv) - .stub("lookup:shared_key@gary", "data:" + rsaEncryptToBase64(encryptKey, keys.getEncryptPublicKey())) + .stub("lookup:shared_key@gary", "data:" + sharedKeyEnc) .build(); String actual = SharedKeyCommands.get(executor, createAtSign("colin"), keys, key); @@ -113,6 +137,25 @@ void getGetSharedByOther() throws Exception { assertThat(actual, equalTo("hello colin")); } + @Test + void getGetSharedByOtherThrowsExceptionForPubKeyHashMismatch() throws Exception { + String encryptKey = generateAESKeyBase64(); + String iv = generateRandomIvBase64(16); + String encrypted = aesEncryptToBase64("hello colin", encryptKey, iv); + String sharedKeyEnc = rsaEncryptToBase64(encryptKey, keys.getEncryptPublicKey()); + Metadata.PublicKeyHash hash = Metadata.PublicKeyHash.builder() + .hash("XXX") + .hashingAlgo(HASHING_ALGO_SHA512) + .build(); + AtCommandExecutor executor = TestExecutorBuilder.builder() + .stubLookupResponse("lookup:all:test@gary", "@colin:test@gary", encrypted, + "ivNonce", iv, "sharedKeyEnc", sharedKeyEnc, "pubKeyHash", hash) + .build(); + + assertThrows(AtPublicKeyChangeException.class, + () -> SharedKeyCommands.get(executor, createAtSign("colin"), keys, key)); + } + @Test void testPutWhenSharedKeyAlreadyExists() throws Exception { String encryptKey = generateAESKeyBase64(); @@ -126,15 +169,18 @@ void testPutWhenSharedKeyAlreadyExists() throws Exception { } @Test - void testPutWhenSharedKeDoesNotAlreadyExists() throws Exception { + void testPutWhenSharedKeyDoesNotAlreadyExists() throws Exception { AtCommandExecutor executor = TestExecutorBuilder.builder() .stub("llookup:shared_key.colin@gary", "error:AT0015:deliberate") .stub("plookup:publickey@colin", "data:" + keys.getEncryptPublicKey()) .stub("update:shared_key.colin@gary .+", "data:1") .stub("update:ttr:86400000:@colin:shared_key@gary .+", "data:2") - .stub("update:isEncrypted:true:ivNonce:.+:@colin:test@gary .+", "data:3") + .stub("update:isEncrypted:true:sharedKeyEnc:.+:ivNonce:.+:@colin:test@gary .+", "data:3") .build(); SharedKeyCommands.put(executor, createAtSign("gary"), keys, key, "hello colin"); + + verify(executor).sendSync(argThat(s -> s.contains("update:") && s.contains(":pubKeyHash:"))); + verify(executor).sendSync(argThat(s -> s.contains("update:") && s.contains(":pubKeyCS:"))); } } diff --git a/at_client/src/test/java/org/atsign/client/impl/util/EncryptionUtilsTest.java b/at_client/src/test/java/org/atsign/client/impl/util/EncryptionUtilsTest.java index 5d69eafc..ce4ae458 100644 --- a/at_client/src/test/java/org/atsign/client/impl/util/EncryptionUtilsTest.java +++ b/at_client/src/test/java/org/atsign/client/impl/util/EncryptionUtilsTest.java @@ -1,9 +1,16 @@ package org.atsign.client.impl.util; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertThrows; +import java.security.KeyPair; +import java.security.Signature; +import java.util.Base64; + +import org.atsign.client.impl.exceptions.AtDecryptionException; +import org.atsign.client.impl.exceptions.AtEncryptionException; import org.junit.jupiter.api.Test; class EncryptionUtilsTest { @@ -21,4 +28,148 @@ void testAesEncryptionWithRandomInitialisationVector() throws Exception { assertThat(decrypted, equalTo(text)); } + @Test + void testAesEncryptToBase64ThrowsExpectedExceptions() throws Exception { + String key = EncryptionUtils.generateAESKeyBase64(); + String text = "mary had a little lamb"; + String iv = EncryptionUtils.generateRandomIvBase64(16); + + AtEncryptionException ex = assertThrows(AtEncryptionException.class, + () -> EncryptionUtils.aesEncryptToBase64(null, key, iv)); + assertThat(ex.getMessage(), containsString("AES encryption failed : input is blank")); + + ex = assertThrows(AtEncryptionException.class, + () -> EncryptionUtils.aesEncryptToBase64(text, null, iv)); + assertThat(ex.getMessage(), containsString("AES encryption failed : key is blank")); + + ex = assertThrows(AtEncryptionException.class, + () -> EncryptionUtils.aesEncryptToBase64(text, key, null)); + assertThat(ex.getMessage(), containsString("AES encryption failed : iv is blank")); + } + + @Test + void testAesDecryptFromBase64ThrowsExpectedExceptions() throws Exception { + String key = EncryptionUtils.generateAESKeyBase64(); + String text = "mary had a little lamb"; + String iv = EncryptionUtils.generateRandomIvBase64(16); + String encrypted = EncryptionUtils.aesEncryptToBase64(text, key, iv); + + AtDecryptionException ex = assertThrows(AtDecryptionException.class, + () -> EncryptionUtils.aesDecryptFromBase64(null, key, iv)); + assertThat(ex.getMessage(), containsString("AES decryption failed : input is blank")); + + ex = assertThrows(AtDecryptionException.class, + () -> EncryptionUtils.aesDecryptFromBase64(encrypted, null, iv)); + assertThat(ex.getMessage(), containsString("AES decryption failed : key is blank")); + + ex = assertThrows(AtDecryptionException.class, + () -> EncryptionUtils.aesDecryptFromBase64(encrypted, key, null)); + assertThat(ex.getMessage(), containsString("AES decryption failed : iv is blank")); + } + + @Test + void testRsaEncryptToBase64AndRsaDecryptFromBase64() throws Exception { + KeyPair keyPair = EncryptionUtils.generateRSAKeyPair(); + String publicKeyBase64 = EncryptionUtils.toStringBase64(keyPair.getPublic()); + String privateKeyBase64 = EncryptionUtils.toStringBase64(keyPair.getPrivate()); + String text = "mary had a little lamb"; + + String encrypted = EncryptionUtils.rsaEncryptToBase64(text, publicKeyBase64); + assertThat(encrypted, not(equalTo(text))); + + String decrypted = EncryptionUtils.rsaDecryptFromBase64(encrypted, privateKeyBase64); + assertThat(decrypted, equalTo(text)); + } + + @Test + void testRsaEncryptToBase64ThrowExpectedExceptions() throws Exception { + KeyPair keyPair = EncryptionUtils.generateRSAKeyPair(); + String publicKeyBase64 = EncryptionUtils.toStringBase64(keyPair.getPublic()); + String text = "mary had a little lamb"; + + AtEncryptionException ex = assertThrows(AtEncryptionException.class, + () -> EncryptionUtils.rsaEncryptToBase64(null, publicKeyBase64)); + assertThat(ex.getMessage(), containsString("RSA encryption failed : input is blank")); + + ex = assertThrows(AtEncryptionException.class, + () -> EncryptionUtils.rsaEncryptToBase64(text, null)); + assertThat(ex.getMessage(), containsString("RSA encryption failed : key is blank")); + } + + @Test + void testRsaDecryptFromBase64ThrowExpectedExceptions() throws Exception { + KeyPair keyPair = EncryptionUtils.generateRSAKeyPair(); + String publicKeyBase64 = EncryptionUtils.toStringBase64(keyPair.getPublic()); + String privateKeyBase64 = EncryptionUtils.toStringBase64(keyPair.getPrivate()); + String text = "mary had a little lamb"; + String encrypted = EncryptionUtils.rsaEncryptToBase64(text, publicKeyBase64); + + AtDecryptionException ex = assertThrows(AtDecryptionException.class, + () -> EncryptionUtils.rsaDecryptFromBase64(null, privateKeyBase64)); + assertThat(ex.getMessage(), containsString("RSA decryption failed : input is blank")); + + ex = assertThrows(AtDecryptionException.class, + () -> EncryptionUtils.rsaDecryptFromBase64(encrypted, null)); + assertThat(ex.getMessage(), containsString("RSA decryption failed : key is blank")); + } + + @Test + void testSignSHA256RSA() throws Exception { + KeyPair keyPair = EncryptionUtils.generateRSAKeyPair(); + String privateKeyBase64 = EncryptionUtils.toStringBase64(keyPair.getPrivate()); + String text = "mary had a little lamb"; + + String signature = EncryptionUtils.signSHA256RSA(text, privateKeyBase64); + assertThat(signature, not(equalTo(text))); + + Signature verifier = Signature.getInstance("SHA256withRSA"); + verifier.initVerify(keyPair.getPublic()); + verifier.update(text.getBytes(UTF_8)); + + assertThat(verifier.verify(Base64.getDecoder().decode(signature)), is(true)); + } + + @Test + void testSignSHA256RSAThrowExpectedExceptions() throws Exception { + KeyPair keyPair = EncryptionUtils.generateRSAKeyPair(); + String privateKeyBase64 = EncryptionUtils.toStringBase64(keyPair.getPrivate()); + String text = "mary had a little lamb"; + + AtEncryptionException ex = assertThrows(AtEncryptionException.class, + () -> EncryptionUtils.signSHA256RSA(null, privateKeyBase64)); + assertThat(ex.getMessage(), containsString("SHA256 sign failed : input is blank")); + + ex = assertThrows(AtEncryptionException.class, + () -> EncryptionUtils.signSHA256RSA(text, null)); + assertThat(ex.getMessage(), containsString("SHA256 sign failed : key is blank")); + } + + @Test + void testDigest() throws Exception { + String text = "mary had a little lamb"; + + String digest = EncryptionUtils.digest(text, "MD5"); + assertThat(digest, not(equalTo(text))); + } + + @Test + void testDigestThrowsExpectedExceptions() throws Exception { + String text = "mary had a little lamb"; + + AtEncryptionException ex = assertThrows(AtEncryptionException.class, () -> EncryptionUtils.digest(text, "XXX")); + assertThat(ex.getMessage(), containsString("failed to hash : XXX MessageDigest not available")); + + ex = assertThrows(AtEncryptionException.class, () -> EncryptionUtils.digest(null, "MD5")); + assertThat(ex.getMessage(), containsString("failed to hash : input blank")); + + ex = assertThrows(AtEncryptionException.class, () -> EncryptionUtils.digest("text", null)); + assertThat(ex.getMessage(), containsString("failed to hash : algo blank")); + } + + @Test + public void testBytesToHex() { + byte[] bytes = new byte[] {(byte) 0x00, (byte) 0x0f, (byte) 0xff}; + assertThat(EncryptionUtils.bytesToHex(bytes), is("000fff")); + } + } diff --git a/at_client/src/test/resources/features/Monitor.feature b/at_client/src/test/resources/features/Monitor.feature index b3c5cb0b..4d803fd2 100644 --- a/at_client/src/test/resources/features/Monitor.feature +++ b/at_client/src/test/resources/features/Monitor.feature @@ -28,7 +28,6 @@ Feature: AtClient API Monitor tests And @colin AtClient.delete for SharedKey test shared with @gary Then @gary AtClient monitor receives the following | Event Type | messageType | from | to | operation | key | decryptedValue | - | sharedKeyNotification | MessageType.key | @colin | @gary | update | @gary:shared_key@colin | | | updateNotification | MessageType.key | @colin | @gary | update | @gary:test@colin | | | decryptedUpdateNotification | MessageType.key | @colin | @gary | update | @gary:test@colin | hello world | | deleteNotification | MessageType.key | @colin | @gary | delete | @gary:test@colin | | diff --git a/pom.xml b/pom.xml index b816cfbe..1135db42 100644 --- a/pom.xml +++ b/pom.xml @@ -61,9 +61,11 @@ 5.5.0 2.2 7.14.0 - 1.2.0 2.0.3 4.2.0 + + + 1.2.0