A Java Implementation of XQ Message SDK, V.2
- Installation
- Generating API Keys
- Basic Usage
- Encrypt
- Decrypt
- FileEncrypt
- FileDecrypt
- Authorize
- CodeValidator
- RevokeKeyAccess
- GrantUserAccess
- AuthorizeAlias
- Dashboard Mangement
- Manage Keys Yourself
In order to utilize the XQ SDK and interact with XQ servers you will need both the General
and Dashboard
API keys. To generate these keys, follow these steps:
- Go to your XQ management portal.
- Select or create an application.
- Create a
General
key for the XQ framework API. - Create a
Dashboard
key for the XQ dashboard API.
Once a key has been obtained from XQ Message it must be inserted it into these files:
config.properties
dev-config.properties
test-config.properties
The config properties for the API keys are called
- com.xq-msg.sdk.v2.xq-api-key
- com.xq-msg.sdk.v2.dashboard-api-key
Debug/RunConfig:
Test Kind: Class
Class: com.xqmsg.sdk.v2.XQSDKTests
VM Options:
-
-Dmode=test
(loads: test/resources/test-config.properties) -
-Dclear-cache=false|true
(re-use access tokens from previous run or create new ones each time) -
-Dxqsdk-user.email=username@domain-name.com
(validation pins will be sent to this email address) -
-Dxqsdk-alias-user=123456790
(test alias id) -
-Dxqsdk-user2.email=username@domain-name.com
(an additional email for tests involvingmerge tokens
) -
-Dxqsdk-recipients.email=username@domain-name.com
(an additional email, needed for tests involvingrecipients
)
Note: You only need to generate one SDK instance for use across your application.
(Encrypt.java) The text to be encrypted should be submitted along with the email addresses of the intended recipients, as well as the amount of time that the message should be available.
String userEmail = "me@email.com";
One can list specific recipients all of which have to be authorized XQ users ...
List recipients = List.of("jane@email.com", "jack@email.com");
or specific recipients which are authorized alias users ...
List recipients = List.of("123@alias.local", "456@alias.local");
or anyone who has a copy of the locator token
List recipients = List.of(AuthorizeAlias.ANY_AUTHORIZED);
final String messageToEncrypt =
"The first stanza of Pushkin's Bronze Horseman (Russian):\n" +
"На берегу пустынных волн\n" +
"Стоял он, дум великих полн,\n" +
"И вдаль глядел. Пред ним широко\n" +
"Река неслася; бедный чёлн\n" +
"По ней стремился одиноко.\n" +
"По мшистым, топким берегам\n" +
"Чернели избы здесь и там,\n" +
"Приют убогого чухонца;\n" +
"И лес, неведомый лучам\n" +
"В тумане спрятанного солнца,\n" +
"Кругом шумел.\n";
Encrypt
.with(sdk, AlgorithmEnum.OTPv2) // Either "OTPv2" or "AES"
.supplyAsync(Optional.of(Map.of(Encrypt.USER, userEmail,
Encrypt.TEXT, messageToEncrypt,
Encrypt.RECIPIENTS, recipients,
Encrypt.MESSAGE_EXPIRATION_HOURS, 5)))
.thenApply(
(ServerResponse response) -> {
switch (response.status) {
case Ok: {
String locatorToken = (String) response.payload.get(Encrypt.LOCATOR_KEY);
String encryptedText = (String) response.payload.get(Encrypt.ENCRYPTED_TEXT);
// Store the locator key and encryptted text somehwere
return response;
}
default: {
//Something went wrong
logger.severe(String.format("failed , reason: %s", response.moreInfo()));
return response;
}
}
}
).get();
To decrypt a message, the encrypted payload must be provided, along with the locator token received from XQ during encryption. The authenticated user must be one of the recipients that the message was originally sent to ( or the sender themselves).
String locatorToken = "";// obtained from a preceding Encrypt call
String encryptedText = "";// obtained from a preceding Encrypt call
ServerResponse decryptResponse = Decrypt
.with(sdk, AlgorithmEnum.OTPv2)
.supplyAsync(Optional.of(Map.of(Decrypt.LOCATOR_TOKEN, locatorToken, Decrypt.ENCRYPTED_TEXT, encryptedText)))
.thenApply(
(ServerResponse response) -> {
switch (response.status) {
case Ok: {
String decryptedText = (String) response.payload.get(ServerResponse.DATA);
//Do something with the decrypted text
return response;
}
default: {
//Something went wrong
logger.severe(String.format("failed , reason: %s", response.moreInfo()));
return response;
}
}
}).get();
Here, a File
object containing the data for encryption must be provided. Like message encryption, a list of recipients who will be able to decrypt the file, as well as the amount of time before expiration must also be provided.
final Path sourceSpec = Paths.get("path/to/original/file/my-original-file.txt"));
final Path targetSpec = Paths.get("path/to/encrypted/file/my-encrypted-file.txt.xqf"));
String userEmail = "me@email.com";
Integer expiration = 5;
One can list specific recipients all of which have to be authorized XQ users ...
List recipients = List.of("jane@email.com", "jack@email.com");
or specific recipients which are authorized alias users ...
List recipients = List.of("123@alias.local", "456@alias.local");
or anyone who has a copy of the locator token
List recipients = List.of(AuthorizeAlias.ANY_AUTHORIZED);
Path encryptedFilePath = FileEncrypt.with(sdk, AlgorithmEnum.OTPv2)
.supplyAsync(Optional.of(Map.of(FileEncrypt.USER, userEmail,
FileEncrypt.RECIPIENTS, recipients,
FileEncrypt.MESSAGE_EXPIRATION_HOURS, expiration,
FileEncrypt.SOURCE_FILE_PATH, sourceSpec,
FileEncrypt.TARGET_FILE_PATH, targetSpec)))
.thenApply((response) -> {
switch (response.status) {
case Ok: {
var encryptFilePath = (Path) response.payload.get(ServerResponse.DATA);
// Do something with the encrypted file
return encryptFilePath;
}
default: {
// Something went wrong
logger.warning(String.format("failed , reason: %s", response.moreInfo()));
return null;
}
}
}).get();
}
To decrypt a file, the URI to the XQ encrypted file must be provided. The user decrypting the file must be one of the recipients original specified ( or the sender ).
final Path sourceSpec = Paths.get(String.format("path/to/encrypted/file/my-encrypted-file.txt.xqf"));
final Path targetSpec = Paths.get("path/to/decrypted/file/my-decrypted-file.txt"));
Map<String, Object> payload = Map.of(FileDecrypt.SOURCE_FILE_PATH, sourceSpec, FileDecrypt.TARGET_FILE_PATH, targetSpec);
FileDecrypt.with(sdk, AlgorithmEnum.OTPv2)
.supplyAsync(Optional.of(payload))
.thenApply((response) -> {
switch (response.status) {
case Ok: {
var decryptFilePath = (Path) response.payload.get(ServerResponse.DATA);
logger.info("Decrypt Filepath: " + decryptFilePath);
return decryptFilePath;
}
default: {
logger.warning(String.format("failed , reason: %s", response.moreInfo()));
return null;
}
}
}).get();
Request a temporary access token (which is cached in the active user profile) for a particular email address. If successful, the user will receive an email containing a PIN number and a validation link.
Map<String, Object> payload =
Map.of( Authorize.USER, "me@email.com",
Authorize.FIRST_NAME, "John",
Authorize.LAST_NAME, "Doe");
String accessToken = Authorize
.with(sdk)
.supplyAsync(Optional.of(payload))
.thenApply(
(ServerResponse response) -> {
switch (response.status) {
case Ok: {
// Success. A pre-authorization token is automatically be cached
// If you like, you can also retrieve it directly
return (String) response.payload.get(ServerResponse.DATA);
}
default: {
logger.warning(String.format("failed , reason: %s", response.moreInfo()));
return null;
}
}
}).get();
This service validates that the PIN received in the email is mapped to same temporary access token that was previously cached. If so, the temporary code will be exchanged for a permanent access token which can be used for subsequent activities. This access token is stored in the user's active profile .
Map<String, Object> payload =
Map.of(CodeValidator.PIN, "the-pin-that-was-mailed-to-you");
CodeValidator
.with(sdk)
.supplyAsync(Optional.of(payload))
.thenApply(
(ServerResponse response) -> {
switch (response.status) {
case Ok: {
// Success. The access token is stored in the user's active profile
// If you like, you can also retrieve it directly
return (String) response.payload.get(ServerResponse.DATA);
}
default: {
//something went wrong
logger.warning(String.format("failed , reason: %s", response.moreInfo()));
return null;
}
}
}).get();
Alternatively, if the user clicks on the link in the email, they can simply exchange their pre-authorization token for a valid access token by using ExchangeForAccessToken.java directly.
Note that the active user in the sdk
should be the same as the one used to make the authorization call:
ExchangeForAccessToken
.with(sdk)
.supplyAsync(Optional.empty())
.thenApply(
(ServerResponse response) -> {
switch (response.status) {
case Ok: {
// Success. A new access token is already stored in the user's active profile
// If you like, you can also retrieve it directly
return (String) response.payload.get(ServerResponse.DATA);
}
default: {
//something went wrong
return null;
}
}
}).get();
Revokes a key using its token. Only the user who sent the message will be able to revoke it. If successful a 204 status is returned.
Warning: This action is not reversible.
RevokeKeyAccess
.with(sdk)
.supplyAsync(Optional.of(Map.of(Decrypt.LOCATOR_TOKEN, "message_locator_token")))
.thenApply(
(ServerResponse response) -> {
switch (response.status) {
case Ok: {
// Success. Key was revoked successfully.
String noContent = (String) response.payload.get(ServerResponse.DATA);
return noContent;
}
default: {
// Something went wrong...
return null;
}
}
}).get();
There may be cases where additional users need to be granted access to a previously sent message, or access needs to be revoked. This can be achieved via GrantUserAccess and RevokeUserAccess respectively:
GrantUserAccess.with(sdk)
.supplyAsync(Optional.of(Map.of(
GrantUserAccess.RECIPIENTS, "john@email.com",
GrantUserAccess.LOCATOR_TOKEN, "message_locator_token"
)))
.thenApply(
(ServerResponse response) -> {
switch (response.status) {
case Ok: {
// Success. John will now be able to read that message.
break;
}
default: {
// Something went wrong...
break;
}
}
return response;
}).get();
// Revoke access from particular users.
RevokeUserAccess.with(sdk)
.supplyAsync(Optional.of(Map.of(
RevokeUserAccess.RECIPIENTS, "jack@email.com",
RevokeUserAccess.LOCATOR_TOKEN, "message_locator_token"
)))
.thenApply(
(ServerResponse response) -> {
switch (response.status) {
case Ok: {
// Success - Jack will no longer be able to read that message.
break;
}
default: {
// Something went wrong...
break;
}
}
return response;
}).get();
After creation, a user can connect to an Alias account by using the AuthorizeAlias
endpoint:
Map<String, Object> payload = Map.of(Authorize.USER, "an-alias-id");
AuthorizeAlias
.with(sdk)
.supplyAsync(Optional.of(payload))
.thenApply(
(ServerResponse response) -> {
switch (response.status) {
case Ok: {
// Success - The alias user was authorized.
// The alias token is automatically stored as the active profile.
// If you like, you can also retrieve it directly
return (String) response.payload.get(ServerResponse.DATA);
}
default: {
return null;
}
}
}).get();
A user has the option of only using XQ for its key management services alone. The necessary steps to do this are detailed below:
XQ provides a quantum source that can be used to generate entropy for seeding their encryption key:
FetchQuantumEntropy.with(sdk)
.supplyAsync(Optional.empty())
.thenApply(
(ServerResponse response) -> {
switch (response.status) {
case Ok: {
String key = (String) response.payload.get(ServerResponse.DATA);
// Do something with the key
return key;
}
default: {
//Something went wrong
logger.warning(String.format("failed , reason: %s", response.moreInfo()));
return null;
}
}
}).get();
The encryption key, authorized recipients, as well as any additional metadata is sent to the XQ subscription server. If successful, a signed and encrypted key packet, a.k.a locator token returned to the user.
Map<String, Object> payload = Map.of(
UploadKey.KEY, "THE_ENCRYPTION_KEY",
UploadKey.RECIPIENTS, List.of("jane@email.com, jack@email.com"),
UploadKey.MESSAGE_EXPIRATION_HOURS, 24,
UploadKey.DELETE_ON_RECEIPT, false
);
UploadKey.with(sdk)
.supplyAsync(Optional.of(payload))
.thenApply((ServerResponse response) -> {
switch (response.status) {
case Ok: {
String locatorToken = (String) response.payload.get(ServerResponse.DATA);
// The packet, a.k.a locator token, is used later on to retrieve the key.
break;
}
default: {
logger.warning(String.format("failed , reason: %s", response.moreInfo()));
break;
}
}
return response;
}).get();
Use the locator token associated with the respective message to retrieve the encryption key. Even with the locator key only users that were previously specified as recipients can fetch this key.
Map<String, Object> payload = Map.of(FetchKey.LOCATOR_TOKEN, "KEY_LOCATOR_TOKEN");
FetchKey.with(this.sdk)
.supplyAsync(Optional.of(payload))
.thenApply((ServerResponse response) -> {
switch (response.status) {
case Ok: {
String encryptionKey = (String) response.payload.get(ServerResponse.DATA);
// The received key can now be used to decrypt the original message.
return encryptionKey;
}
default: {
logger.warning(String.format("failed , reason: %s", response.moreInfo()));
return null;
}
}
}).get();
The SDK provides limited functionality for dashboard administration. In order to use any of the services listed in this section a user must be signed into XQ with an authorized email account associated with the management portal.
DashboardLogin.with(sdk)
.supplyAsync(Optional.empty())
.thenApply((ServerResponse response) -> {
switch (response.status) {
case Ok: {
// Success. New dashboard access token will be stored
// for the current profile.
String dashboardAccessToken = (String) response.payload.get(ServerResponse.DATA);
return dashboardAccessToken;
}
default: {
logger.warning(String.format("failed , reason: %s", response.moreInfo()));
return null;
}
}
}).get();
Users may group a number of emails accounts under a single alias. Doing this makes it possible to add all of the associated email accounts to an outgoing message by adding that alias as a message recipient instead. Note that changing the group members does not affect the access rights of messages that have previously been sent.
AddUserGroup.with(sdk)
.supplyAsync(Optional.of(payload))
.thenApply((ServerResponse response) -> {
switch (response.status) {
case Ok: {
// Success. The new user group was created.
String groupId = (String) response.payload.get(AddUserGroup.ID);
// The new group email format is {groupId}@group.local
return groupId;
}
default: {
logger.warning(String.format("failed , reason: %s", response.moreInfo()));
return null;
}
}
});
In situations where a user may want to associate an external account with an XQ account for the purposes of encryption and tracking , they can choose to create an account with an Alias role.
These type of accounts will allow user authorization using only an account ID. However, these accounts have similar restrictions to anonymous accounts: They will be incapable of account management, and also have no access to the dashboard. However - unlike basic anonymous accounts - they can be fully tracked in a dashboard portal.
Map<String, Object> payload = Map.of(AddContact.EMAIL, "john@email.com",
AddContact.NOTIFICATIONS, Notifications.NONE,
AddContact.ROLE, Roles.Alias.ordinal(),
AddContact.TITLE, "Mr.",
AddContact.FIRST_NAME, "John",
AddContact.LAST_NAME, "Doe");
AddContact.with(sdk)
.supplyAsync(Optional.of(payload))
.thenApply(
(ServerResponse serverResponse) -> {
switch (serverResponse.status) {
case Ok: {
var contactId = serverResponse.payload.get(AddContact.ID);
return contactId;
}
default: {
logger.info(String.format("failed , reason: %s", serverResponse.moreInfo()));
return null;
}
}
}
).get();
A basic disk backed cache implementation utilizing MapDB which is used to store access tokens by email address