Skip to content

Commit

Permalink
Add backup utility methods to client (#116)
Browse files Browse the repository at this point in the history
- Adds new methods for compression and unpacking backup content to json
- Adds new test cases
- Updates readme

Resolves #115
{minor}
  • Loading branch information
nagyesta authored Apr 9, 2022
1 parent e514d92 commit 57e00b5
Show file tree
Hide file tree
Showing 20 changed files with 435 additions and 11 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ alternative for the cases when using a real Key Vault is not practical or imposs

### Warning!

> Lowkey Vault is NOT intended as a [Azure Key Vault](https://azure.microsoft.com/en-us/services/key-vault/) replacement. Please do not attempt using it instead of the real service in production as it is not using any security measures to keep your secrets safe.
> Lowkey Vault is NOT intended as an [Azure Key Vault](https://azure.microsoft.com/en-us/services/key-vault/) replacement. Please do not attempt using it instead of the real service in production as it is not using any security measures to keep your secrets safe.
### Valid use-cases

Expand Down Expand Up @@ -54,7 +54,7 @@ I have an app using Azure Key Vault and:
### Docker

1. Pull the most recent version from ```nagyesta/lowkey-vault```
2. ```docker run lowkey-vault:<version> -p 8443:8443```
2. ```docker run --rm -p 8443:8443 nagyesta/lowkey-vault:<version>```
3. Use ```https://localhost:8443``` as key vault URI when using
the [Azure Key Vault Key client](https://docs.microsoft.com/en-us/azure/key-vault/keys/quick-create-java)
or the [Azure Key Vault Secret client](https://docs.microsoft.com/en-us/azure/key-vault/secrets/quick-create-java)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import lombok.NonNull;

import java.io.IOException;
import java.net.URI;
import java.util.List;
import java.util.function.Supplier;
Expand All @@ -26,4 +27,8 @@ VaultModel createVault(@NonNull URI baseUri,
boolean purge(@NonNull URI baseUri);

void timeShift(@NonNull TimeShiftContext context);

String unpackBackup(byte[] backup) throws IOException;

byte[] compressBackup(String backup) throws IOException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@
import org.apache.http.HttpHeaders;
import reactor.util.annotation.Nullable;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import static com.azure.core.http.ContentType.APPLICATION_JSON;
import static com.github.nagyesta.lowkeyvault.http.management.impl.ResponseEntity.VAULT_MODEL_LIST_TYPE_REF;
Expand Down Expand Up @@ -129,6 +135,30 @@ public void timeShift(@NonNull final TimeShiftContext context) {
sendRaw(request);
}

@Override
public String unpackBackup(final byte[] backup) throws IOException {
final byte[] nonNullBackup = Optional.ofNullable(backup)
.orElseThrow(() -> new IllegalArgumentException("Backup cannot be null"));
//noinspection LocalCanBeFinal
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(nonNullBackup);
GZIPInputStream gzipInputStream = new GZIPInputStream(byteArrayInputStream)) {
final String json = new String(gzipInputStream.readAllBytes());
return objectReader.readTree(json).toPrettyString();
}
}

@Override
public byte[] compressBackup(@NonNull final String backup) throws IOException {
//noinspection LocalCanBeFinal
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream)) {
gzipOutputStream.write(backup.getBytes(StandardCharsets.UTF_8));
gzipOutputStream.flush();
gzipOutputStream.finish();
return byteArrayOutputStream.toByteArray();
}
}

String vaultModelAsString(final URI baseUri, final RecoveryLevel recoveryLevel, final Integer recoverableDays) {
try {
return objectWriter.writeValueAsString(new VaultModel(baseUri, recoveryLevel, recoverableDays, null, null));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.azure.core.http.HttpRequest;
import com.azure.core.http.HttpResponse;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;
Expand All @@ -21,6 +22,8 @@
import org.mockito.*;
import reactor.core.publisher.Mono;

import java.io.IOException;
import java.math.BigInteger;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
Expand All @@ -35,6 +38,8 @@

class LowkeyVaultManagementClientImplTest {

public static final String SIMPLE_JSON = "{\"property\":42}";
public static final String SIMPLE_JSON_PRETTY = "{\n\t\"property\": 42\n}";
private static final String HTTPS_LOCALHOST = "https://localhost";
private static final String JSON = "{}";
private static final int RECOVERABLE_DAYS = 90;
Expand Down Expand Up @@ -474,5 +479,58 @@ void testSendAndProcessShouldThrowExceptionWhenResponseCodeIsNot2xx() throws Jso
verify(objectReader, never()).forType(eq(VaultModel.class));
verify(objectReader, never()).readValue(anyString());
}

@Test
void testUnpackBackupShouldThrowExceptionWhenCalledWithNull() {
//given

//when
Assertions.assertThrows(IllegalArgumentException.class, () -> underTest.unpackBackup(null));

//then + exception
}

@SuppressWarnings("ConstantConditions")
@Test
void testCompressBackupShouldThrowExceptionWhenCalledWithNull() {
//given

//when
Assertions.assertThrows(IllegalArgumentException.class, () -> underTest.compressBackup(null));

//then + exception
}

@Test
void testUnpackBackupShouldProduceFormattedJsonWhenCalledWithValidInput() throws IOException {
//given
final byte[] input = underTest.compressBackup(SIMPLE_JSON);
final JsonNode node = mock(JsonNode.class);
when(objectReader.readTree(eq(SIMPLE_JSON))).thenReturn(node);
when(node.toPrettyString()).thenReturn(SIMPLE_JSON_PRETTY);

//when
final String actual = underTest.unpackBackup(input);

//then
Assertions.assertEquals(SIMPLE_JSON_PRETTY, actual);
final InOrder inOrder = inOrder(objectReader, node);
inOrder.verify(objectReader).readTree(eq(SIMPLE_JSON));
inOrder.verify(node).toPrettyString();
verifyNoMoreInteractions(objectReader, node);
}

@Test
void testCompressBackupShouldProduceGzipBytesWhenCalledWithValidInput() throws IOException {
//given
final byte[] out = new BigInteger("239366333208093937709170404274988390036218345476049221665072896269330010352532848640")
.toByteArray();

//when
final byte[] actual = underTest.compressBackup(SIMPLE_JSON);

//then
Assertions.assertArrayEquals(out, actual);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@
import com.azure.security.keyvault.keys.cryptography.models.DecryptResult;
import com.azure.security.keyvault.keys.cryptography.models.EncryptResult;
import com.azure.security.keyvault.keys.models.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.nagyesta.lowkeyvault.http.ApacheHttpClientProvider;
import com.github.nagyesta.lowkeyvault.http.management.LowkeyVaultManagementClient;

import javax.crypto.SecretKey;
import java.security.KeyPair;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Optional;

public class KeyTestContext extends CommonTestContext<KeyVaultKey, DeletedKey, KeyProperties, KeyClient> {

public static final OffsetDateTime NOW = OffsetDateTime.now(ZoneOffset.UTC);

private final ObjectMapper objectMapper = new ObjectMapper();
private LowkeyVaultManagementClient lowkeyVaultManagementClient;
private CryptographyClient cryptographyClient;
private CreateRsaKeyOptions createRsaKeyOptions;
private CreateEcKeyOptions createEcKeyOptions;
Expand All @@ -38,6 +38,13 @@ protected KeyClient providerToClient(final ApacheHttpClientProvider provider) {
return provider.getKeyClient();
}

public synchronized LowkeyVaultManagementClient getLowkeyVaultManagementClient() {
if (lowkeyVaultManagementClient == null) {
lowkeyVaultManagementClient = getProvider().getLowkeyVaultManagementClient(objectMapper);
}
return lowkeyVaultManagementClient;
}

public CryptographyClient getCryptographyClient() {
return cryptographyClient;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import org.testng.Assert;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.*;
import java.util.stream.Collectors;

public class CommonAssertions {

Expand Down Expand Up @@ -53,4 +55,13 @@ protected void assertByteArrayLength(final int byteArrayLength, final byte[] byt
assertTrue("Byte array was " + bytes.length + " long, expected " + byteArrayLength + " (+/-1 tolerance)",
byteArrayLength - 1 <= bytes.length && byteArrayLength + 1 >= bytes.length);
}

protected String readResourceContent(final String resource) throws IOException {
//noinspection LocalCanBeFinal
try (InputStream stream = getClass().getResourceAsStream(resource);
InputStreamReader reader = new InputStreamReader(Objects.requireNonNull(stream));
BufferedReader bufferedReader = new BufferedReader(reader)) {
return bufferedReader.lines().collect(Collectors.joining(System.lineSeparator()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.util.Arrays;
Expand Down Expand Up @@ -117,6 +120,9 @@ public void ecKeyImportedWithNameAndParameters(final String name, final KeyCurve
context.setKeyPair(keyPair);
final JsonWebKey key = JsonWebKey.fromEc(keyPair, new BouncyCastleProvider())
.setKeyOps(List.of(KeyOperation.SIGN, KeyOperation.ENCRYPT, KeyOperation.WRAP_KEY));
if (hsm) {
key.setKeyType(KeyType.EC_HSM);
}
final ImportKeyOptions options = new ImportKeyOptions(name, key)
.setHardwareProtected(hsm);
final KeyVaultKey ecKey = context.getClient().importKey(options);
Expand All @@ -129,6 +135,9 @@ public void rsaKeyImportedWithNameAndParameters(final String name, final int siz
context.setKeyPair(keyPair);
final JsonWebKey key = JsonWebKey.fromRsa(keyPair)
.setKeyOps(List.of(KeyOperation.SIGN, KeyOperation.ENCRYPT, KeyOperation.WRAP_KEY));
if (hsm) {
key.setKeyType(KeyType.RSA_HSM);
}
final ImportKeyOptions options = new ImportKeyOptions(name, key)
.setHardwareProtected(hsm);
final KeyVaultKey rsaKey = context.getClient().importKey(options);
Expand Down Expand Up @@ -393,13 +402,32 @@ public void theKeyNamedNameIsBackedUp(final String name) {
context.setBackupBytes(name, bytes);
}

@And("the key named {name} is backed up to resource")
public void theKeyNamedNameIsBackedUpToResource(final String name) throws IOException {
final byte[] bytes = context.getClient().backupKey(name);
final String s = context.getLowkeyVaultManagementClient().unpackBackup(bytes);
final File file = new File("/home/esta/IdeaProjects/github/lowkey-vault/lowkey-vault-docker/src/test/resources"
+ "/json/backups/" + name + ".json");
file.createNewFile();
new FileWriter(file).append(s).close();
context.setBackupBytes(name, bytes);
}

@And("the key named {name} is restored")
public void theKeyNamedNameIsRestored(final String name) {
final byte[] bytes = context.getBackupBytes(name);
final KeyVaultKey key = context.getClient().restoreKeyBackup(bytes);
context.addFetchedKey(name, key);
}

@And("the key named {name} is restored from classpath resource")
public void theKeyIsRestoredFromClasspath(final String name) throws IOException {
final String content = readResourceContent("/json/backups/" + name + ".json");
final byte[] bytes = context.getLowkeyVaultManagementClient().compressBackup(content);
final KeyVaultKey key = context.getClient().restoreKeyBackup(bytes);
context.addFetchedKey(key.getName(), key);
}

private byte[] hash(final byte[] text, final String algorithm) {
try {
final MessageDigest md = MessageDigest.getInstance("SHA-" + algorithm.substring(2, 5));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import io.cucumber.java.en.Then;
import org.springframework.beans.factory.annotation.Autowired;

import java.io.IOException;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.LinkedList;
Expand Down Expand Up @@ -168,4 +169,12 @@ public void theKeyNamedNameMatchesThePreviousBackup(final String name) {
final byte[] bytes = context.getClient().backupKey(name);
assertEquals(context.getBackupBytes(name), bytes);
}

@And("the unpacked backup of {name} matches the content of the classpath resource")
public void theKeyNamedNameMatchesTheResourceContent(final String name) throws IOException {
final byte[] bytes = context.getClient().backupKey(name);
final String backup = context.getLowkeyVaultManagementClient().unpackBackup(bytes);
final String expected = readResourceContent("/json/backups/" + name + ".json");
assertEquals(expected, backup);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public int octKeySize(final String size) {
return Integer.parseInt(size);
}

@ParameterType("(2048|4096)")
@ParameterType("(2048|3072|4096)")
public int rsaKeySize(final String size) {
return Integer.parseInt(size);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,46 @@ Feature: Key backup and restore
| backupOct-4 | 128 | A128CBC | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do. |
| backupOct-5 | 192 | A192CBC | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do. |
| backupOct-6 | 256 | A256CBC | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do. |

@Key @KeyImport @KeyEncrypt @KeyBackup @KeyRestore @RSA
Scenario Outline: RSA_BACKUP_02 An RSA key is restored from json, backed up, then the backup content is compared to the source
Given a vault is created with name keys-backup-<keyName>
And a key client is created with the vault named keys-backup-<keyName>
And the key named <keyName> is restored from classpath resource
When the key named <keyName> is backed up
And the unpacked backup of <keyName> matches the content of the classpath resource

Examples:
| keyName |
| jsonBackupRsa-2048 |
| jsonBackupRsa-3072 |
| jsonBackupRsa-4096 |

@Key @KeyImport @KeyEncrypt @KeyBackup @KeyRestore @EC
Scenario Outline: EC_BACKUP_02 An EC key is restored from json, backed up, then the backup content is compared to the source
Given a vault is created with name keys-backup-<keyName>
And a key client is created with the vault named keys-backup-<keyName>
And the key named <keyName> is restored from classpath resource
When the key named <keyName> is backed up
And the unpacked backup of <keyName> matches the content of the classpath resource

Examples:
| keyName |
| jsonBackupEc-256 |
| jsonBackupEc-256k |
| jsonBackupEc-384 |
| jsonBackupEc-521 |

@Key @KeyImport @KeyEncrypt @KeyBackup @KeyRestore @OCT
Scenario Outline: OCT_BACKUP_02 An OCT key is restored from json, backed up, then the backup content is compared to the source
Given a vault is created with name keys-backup-<keyName>
And a key client is created with the vault named keys-backup-<keyName>
And the key named <keyName> is restored from classpath resource
When the key named <keyName> is backed up
And the unpacked backup of <keyName> matches the content of the classpath resource

Examples:
| keyName |
| jsonBackupOct-128 |
| jsonBackupOct-192 |
| jsonBackupOct-256 |
Loading

0 comments on commit 57e00b5

Please sign in to comment.