Skip to content

Commit

Permalink
Merge pull request #13 from IonicDev/release/2.9
Browse files Browse the repository at this point in the history
2.9 release
  • Loading branch information
rick-ionic committed Oct 22, 2020
2 parents e77d116 + b5a772d commit b7e7dd4
Show file tree
Hide file tree
Showing 72 changed files with 2,982 additions and 98 deletions.
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,54 @@ Visit [Getting Started Tutorial](https://dev.ionic.com/getting-started) for a gu

## Release Notes

### 2.9.0

### New Features

#### Deterministic Encryption Support
The SDK has added support for deterministic encryption, allowing users to perform equality comparisons on encrypted
data values.

#### Space-Efficient Encryption of Small Binary Data Values
A new `BinaryCipher` family of cipher implementations has been added to the SDK. These implementations allow for
the protection of small binary data values like the existing `ChunkCipher` implementations, but without the
additional space needed to encode ciphertexts using the base64 algorithm.

#### GraalVM Support
The SDK library JAR now includes additional metadata to enable usage in [GraalVM](https://www.graalvm.org/)
native binary applications.

#### Support for Machina Identity Assertion Functions
The class `com.ionic.sdk.agent.Agent` now includes additional APIs in support of Machina identity provider operations:
- `CreateIdentityAssertion` allows a Machina-enabled device to generate an assertion, which can be used
to prove that it has a valid enrollment in the given keyspace.
- `ValidateIdentityAssertion` allows a non-Machina-enabled device to verify the assertion.

#### KeyServices Implementations
Additional implementations of the interface `KeyServices` are available in the distributable
source (including the test source).
- `KeyServicesMinimal` is a partial implementation of `KeyServices`, with default methods.
- `KeyServicesSingleKey` wraps a single cryptography key, useful for deterministic encryption scenarios.
- `LoopbackAgent` implements `KeyServices`, with a `KeyVaultBase` as a backing persistent store of key data.
- `TestKeyServices` wraps access to a single fixed key, with no network access. Useful in unit test scenarios.

#### Additional Documentation Included with Release Distributable
The SDK release distributable now includes the following documents, in markdown and html formats:
- `README`, describing high-level SDK project functionality
- `LICENSE`, providing the Machina license agreement for Ionic resources
- `CHANGELOG`, with line items providing summary information about the issues included in each release
- `RELEASE_NOTES`, detailing the features and fixes included in the release

#### Documentation Improvements
- Additional detail describes the data model associated with the APIs `Agent.loadProfiles()`
and `Agent.saveProfiles()`.

### Corrected Issues
- `GetKey` transaction responses now include any `KeyObligations` specified by the Machina service.
- Extraneous information about the data associated with a serialized `ProfilePersistor` is now logged at an
appropriate log level.
- `DeviceProfilePersistorPassword` now documents the minimum password length requirement.

### 2.8.0

#### Abstract Class KeyServicesMinimal
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.ionic</groupId>
<artifactId>ionic-sdk</artifactId>
<version>2.6.0</version>
<version>2.9.0</version>
<packaging>jar</packaging>
<name>Ionic Java SDK</name>
<description>The Ionic Java SDK provides an easy-to-use interface to the Ionic Platform.</description>
Expand Down
79 changes: 76 additions & 3 deletions src/main/java/com/ionic/sdk/agent/Agent.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
import com.ionic.sdk.agent.data.MetadataMap;
import com.ionic.sdk.agent.hfp.Fingerprint;
import com.ionic.sdk.agent.key.KeyAttributesMap;
import com.ionic.sdk.agent.request.createassertion.CreateIdentityAssertionRequest;
import com.ionic.sdk.agent.request.createassertion.CreateIdentityAssertionResponse;
import com.ionic.sdk.agent.request.createassertion.CreateIdentityAssertionTransaction;
import com.ionic.sdk.agent.request.createassertion.IdentityAssertionValidator;
import com.ionic.sdk.agent.request.createassertion.data.AssertionUtils;
import com.ionic.sdk.agent.request.createassertion.data.IdentityAssertion;
import com.ionic.sdk.agent.request.createassertion.data.PubkeyResolver;
import com.ionic.sdk.agent.request.createdevice.CreateDeviceRequest;
import com.ionic.sdk.agent.request.createdevice.CreateDeviceResponse;
import com.ionic.sdk.agent.request.createdevice.CreateDeviceTransaction;
Expand All @@ -30,6 +37,8 @@
import com.ionic.sdk.agent.transaction.AgentTransactionUtil;
import com.ionic.sdk.core.date.DateTime;
import com.ionic.sdk.core.value.Value;
import com.ionic.sdk.device.DeviceUtils;
import com.ionic.sdk.device.create.saml.DeviceEnrollment;
import com.ionic.sdk.device.profile.DeviceProfile;
import com.ionic.sdk.device.profile.persistor.DeviceProfiles;
import com.ionic.sdk.device.profile.persistor.ProfilePersistor;
Expand Down Expand Up @@ -76,7 +85,15 @@
* objects may be added to the working set of the Agent through the {@link #addProfile(DeviceProfile)} API. Updates to
* the DeviceProfile working set may be saved through the {@link #saveProfiles(ProfilePersistor)} API.
* <p>
* Machina Tools SDK for Java does not have the concept of an default ProfilePersistor, so use of
* Implementations of {@link ProfilePersistor} are typically backed by a file on a filesystem available to
* the {@link Agent} process. The agent maintains an in-memory cache of {@link DeviceProfile} objects for use during
* cryptography operations. The API {@link #loadProfiles(ProfilePersistor)} causes the agent cache
* of DeviceProfile objects to be replaced with those retrieved from the file. The
* API {@link #saveProfiles(ProfilePersistor)} causes the serialized ProfilePersistor file to be replaced
* with the records from the agent cache. No internal reference to a ProfilePersistor is maintained by
* the agent.
* <p>
* Machina Tools SDK for Java does not have the concept of a default ProfilePersistor, so use of
* the {@link #loadProfiles()} and {@link #saveProfiles()} APIs will cause an {@link IonicException} to be thrown.
* <p>
* Sample (simple agent instantiation and usage):
Expand Down Expand Up @@ -124,8 +141,8 @@
* </pre>
* <p>
* Keyspace Name Service (KNS), based on DNS, allows Machina clients to resolve API endpoint information by
* providing a valid four-character keyspace ID. It can be used to update existing {@link DeviceProfile} records (for
* instance, in the case of a Machina tenant migration). This may be persisted to the filesystem via a subsequent
* providing a valid four-character keyspace ID. KNS can be used to update existing {@link DeviceProfile} records (for
* instance, in the case of a Machina tenant migration). Updates may be persisted to the filesystem via a subsequent
* call to {@link Agent#saveProfiles(ProfilePersistor)}.
* <p>
* Sample (update in-memory copy of active profile's server string, loaded from filesystem):
Expand Down Expand Up @@ -638,6 +655,25 @@ private GetKeyspaceResponse getKeyspaceInternal(final GetKeyspaceRequest request
return response;
}

/**
* Creates a new device record for use with subsequent requests to Ionic.com.
*
* @param assertion a (pre-generated) SAML assertion (proof of validated authentication to keyspace)
* @param keyspace the four character Machina keyspace that is the target of the enrollment
* @param makeDeviceActive assign the newly created device as the active device for the agent
* @return the response output data object, containing the created device profile
* @throws IonicException if an error occurs
*/
public final CreateDeviceResponse createDevice(
final byte[] assertion, final String keyspace, final boolean makeDeviceActive) throws IonicException {
final GetKeyspaceResponse getKeyspaceResponse = getKeyspaceInternal(new GetKeyspaceRequest(keyspace));
final String enrollmentURL = getKeyspaceResponse.getFirstEnrollmentURL();
final DeviceEnrollment deviceEnrollment = new DeviceEnrollment(DeviceUtils.toUrl(enrollmentURL));
final CreateDeviceResponse createDeviceResponse = deviceEnrollment.enroll(assertion);
addProfileInternal(createDeviceResponse.getDeviceProfile(), makeDeviceActive);
return createDeviceResponse;
}

/**
* Creates a new device record for use with subsequent requests to Ionic.com.
*
Expand Down Expand Up @@ -1013,6 +1049,43 @@ private LogMessagesResponse logMessageInternal(final LogMessagesRequest request)
return response;
}

/**
* Creates an Identity Assertion using the active {@link DeviceProfile}. Assertions are used as proof of
* membership in an Ionic keyspace. They can be created by Ionic devices, and validated by any other system.
*
* @param request the identity assertion request input data object
* @return the identity assertion response output data object
* @throws IonicException if an error occurs during servicing of the request
*/
public CreateIdentityAssertionResponse createIdentityAssertion(
final CreateIdentityAssertionRequest request) throws IonicException {
final CreateIdentityAssertionResponse response = new CreateIdentityAssertionResponse();
final CreateIdentityAssertionTransaction transaction =
new CreateIdentityAssertionTransaction(new VbeProtocol(this), request, response);
transaction.run();
return response;
}

/**
* Verify the validity of an Identity Assertion. This may be used to verify that the assertion was
* generated by a previously enrolled Ionic Machina device.
*
* @param pubkeyBase64 the public key component of the RSA keypair used to generate the identity assertion;
* if null, the key is retrieved via a Keyspace Name Service (KNS) lookup
* @param assertionBase64 the container for the supplied Identity Assertion data
* @param nonce the single-use token incorporated into the canonical form / digest of the assertion
* @return a container object holding the data from the unwrapped assertionBase64 input
* @throws IonicException on failure to validate the specified assertion
*/
public IdentityAssertion validateIdentityAssertion(
final String pubkeyBase64, final String assertionBase64, final String nonce) throws IonicException {
final IdentityAssertion assertion = new IdentityAssertion(assertionBase64);
final String keyspace = AssertionUtils.toKeyspace(assertion.getSigner());
final IdentityAssertionValidator validator = new IdentityAssertionValidator(
(pubkeyBase64 == null) ? new PubkeyResolver(this).getPublicKeyKeyspace(keyspace) : pubkeyBase64);
return validator.validate(assertionBase64, null, null, nonce);
}

/**
* Initialize the agent with default configuration and default profile loader.
*
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/ionic/sdk/agent/SdkVersion.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ private static String getVersionStringInternal() {
/**
* Define the SDK minor version number.
*/
public static final int IONIC_SDK_MINOR = 8;
public static final int IONIC_SDK_MINOR = 9;

/**
* Define the SDK patch version number.
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/com/ionic/sdk/agent/VbeProtocol.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ public VbeProtocol(final Agent agent, final DeviceProfile deviceProfile) {
this.deviceProfile = deviceProfile;
}

/**
* @return the private AES key shared between the enrolled device and EI (Enterprise Infrastructure)
*/
public byte[] getKeyEi() {
final DeviceProfile activeProfile = agent.getActiveProfile();
return (activeProfile == null) ? null : activeProfile.getAesCdEiProfileKey();
}

@Override
public boolean isInitialized() {
return agent.isInitialized();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package com.ionic.sdk.agent.cipher.binary;

import com.ionic.sdk.agent.cipher.data.DecryptAttributes;
import com.ionic.sdk.agent.cipher.data.EncryptAttributes;
import com.ionic.sdk.agent.request.createkey.CreateKeysResponse;
import com.ionic.sdk.agent.request.getkey.GetKeysResponse;
import com.ionic.sdk.core.codec.Transcoder;
import com.ionic.sdk.core.io.BytePattern;
import com.ionic.sdk.error.IonicException;
import com.ionic.sdk.error.SdkData;
import com.ionic.sdk.error.SdkError;
import com.ionic.sdk.key.KeyServices;

import java.nio.ByteBuffer;

/**
* Cipher implementation specialized for dealing with binary data. As with
* {@link com.ionic.sdk.agent.cipher.chunk.ChunkCipherAbstract} implementations, the Machina key tag is incorporated
* into the output ciphertext. This makes the plaintext recoverable, given only the ciphertext as input.
*/
public abstract class BinaryCipherAbstract {

/**
* Key services implementation; used to broker key transactions and crypto operations.
*/
private final KeyServices keyServices;

/**
* Constructor.
*
* @param keyServices the key services implementation
*/
public BinaryCipherAbstract(final KeyServices keyServices) {
this.keyServices = keyServices;
}

/**
* @return the key services implementation; used to broker key transactions and crypto operations
*/
public final KeyServices getKeyServices() {
return keyServices;
}

/**
* Encrypt the input plaintext using the provided {@link EncryptAttributes}.
*
* @param plainText the plaintext to be encrypted
* @param encryptAttributes (in/out) the parameters to be used for the encryption; the encryption result parameters
* @return the encoded ciphertext for the given input
* @throws IonicException on cryptography failures
*/
public final byte[] encrypt(final byte[] plainText, final EncryptAttributes encryptAttributes)
throws IonicException {
// check input
SdkData.checkNotNull(keyServices, KeyServices.class.getName());
SdkData.checkNotNull(plainText, byte[].class.getName());
SdkData.checkNotNull(encryptAttributes, EncryptAttributes.class.getName());
// obtain Machina key
final CreateKeysResponse createKeysResponse = keyServices.createKey(encryptAttributes.getKeyAttributes(),
encryptAttributes.getKeyAttributes(), encryptAttributes.getMetadata());
final CreateKeysResponse.Key createKey = createKeysResponse.getFirstKey(); // one key expected
encryptAttributes.setKeyResponse(createKey);
final ByteBuffer plainBuffer = ByteBuffer.wrap(plainText);
// perform implementation-specific encryption
return encryptInternal(plainBuffer, encryptAttributes);
}

/**
* Implementation-specific algorithm for encryption of input plaintext.
*
* @param plainBuffer the plaintext to be encrypted
* @param encryptAttributes (in/out) the parameters to be used for the encryption; the encryption result parameters
* @return the encoded ciphertext for the given input
* @throws IonicException on cryptography failures
*/
abstract byte[] encryptInternal(ByteBuffer plainBuffer, EncryptAttributes encryptAttributes) throws IonicException;

/**
* Decrypt the input ciphertext.
*
* @param cipherText the ciphertext to be decrypted
* @param decryptAttributes (in/out) the parameters to be used in the decryption; the decryption result parameters
* @return the original plaintext associated with the given ciphertext input
* @throws IonicException on cryptography failures
*/
public final byte[] decrypt(final byte[] cipherText, final DecryptAttributes decryptAttributes)
throws IonicException {
final byte[] delimiterExpected = Transcoder.utf8().decode(DELIMITER);
// check input
SdkData.checkNotNull(keyServices, KeyServices.class.getName());
SdkData.checkNotNull(cipherText, byte[].class.getName());
SdkData.checkNotNull(decryptAttributes, DecryptAttributes.class.getName());
// setup for cryptography (allocate space)
final ByteBuffer cipherBuffer = ByteBuffer.wrap(cipherText);
final int offsetDelimiter = BytePattern.findIn(cipherText, 0, delimiterExpected);
final byte[] keyTagBytes = new byte[offsetDelimiter];
cipherBuffer.get(keyTagBytes);
final String keyTag = Transcoder.utf8().encode(keyTagBytes);
final byte delimiter = cipherBuffer.get();
SdkData.checkTrue(delimiter == delimiterExpected[0], SdkError.ISAGENT_INVALIDVALUE);
// obtain Machina key
final GetKeysResponse getKeysResponse = keyServices.getKey(keyTag, decryptAttributes.getMetadata());
final GetKeysResponse.Key key = getKeysResponse.getFirstKey();
decryptAttributes.setKeyResponse(key);
SdkData.checkTrue(key.getId().equals(keyTag), SdkError.ISAGENT_BADRESPONSE);
// perform implementation-specific decryption
return decryptInternal(cipherBuffer, decryptAttributes);
}

/**
* Implementation-specific algorithm for decryption of input ciphertext.
*
* @param cipherBuffer the ciphertext to be decrypted
* @param decryptAttributes (in/out) the parameters to be used in the decryption; the decryption result parameters
* @return the original plaintext associated with the given ciphertext input
* @throws IonicException on cryptography failures
*/
abstract byte[] decryptInternal(ByteBuffer cipherBuffer, DecryptAttributes decryptAttributes) throws IonicException;

/**
* The defined delimiter for this cipher type.
*/
public static final String DELIMITER = "~";
}
Loading

0 comments on commit b7e7dd4

Please sign in to comment.