Artifact Encryption plug point (#1202)
* added ArtifactEncryption interface, injected it into SM creation UI module, added encryption metadata key generation upon SM creation, used encryptor during file upload Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch.io> * add default artifact encryption implementation based on gcm aes algorithm Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch.io> * changed ArtifactEncryptor interface to manage encryption secrets by itself Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch.io> * cleaned up stale code, fixed sonar Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch.io> * fixed software module encryption within transaction Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch.io> * added artifact encryption secrets store Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch.io> * extended ArtifactEncryption interface to allow decryption, secrets store provides removeSecret, added missing javadocs Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch.io> * intriduced DbArtifact interface, use EncryptionAwareDbArtifact for artifact decryption during download Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch.io> * introduced ArtifactEncryptionService to minimize duplications and unneccessary dependency injections Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch.io> * declared ArtifactEncryptionService as a bean Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch.io> * added persistant encryption flag to software module Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch.io> * further adptations for encryption flag persistence Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch.io> * added ArtifactEncryptionException, fixed encryption check in UI Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch.io> * added encryption error handling Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch.io> * added encrypted flag to DDI/DMF, adapted exception handling Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch.io> * adapted rest docs Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch.io> * Add test to verify artifact encryption is not given by default Signed-off-by: Florian Ruschbaschan <Florian.Ruschbaschan@bosch.io> * Add isEncrypted() to toString() of JpaSoftwareModule, fix typos Signed-off-by: Florian Ruschbaschan <Florian.Ruschbaschan@bosch.io> * Fix sql migration scripts Signed-off-by: Florian Ruschbaschan <Florian.Ruschbaschan@bosch.io> * Calculate encrypted artifact size by subtract encryption size overhead Signed-off-by: Florian Ruschbaschan <Florian.Ruschbaschan@bosch.io> * publish upload failed without waiting for interuption during UI file upload Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch.io> * upgraded cron utils to 9.1.6 Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch.io> Co-authored-by: Florian Ruschbaschan <Florian.Ruschbaschan@bosch.io>
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright (c) 2021 Bosch.IO GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.hawkbit.repository.exception.ArtifactEncryptionFailedException;
|
||||
|
||||
/**
|
||||
* Interface definition for artifact encryption.
|
||||
*
|
||||
*/
|
||||
public interface ArtifactEncryption {
|
||||
|
||||
/**
|
||||
* Defines the required secret keys for particular encryption algorithm.
|
||||
*
|
||||
* @return list of required secret keys
|
||||
*/
|
||||
Set<String> requiredSecretKeys();
|
||||
|
||||
/**
|
||||
* Generates required secrets key/value pairs.
|
||||
*
|
||||
* @return secrets key/value pairs
|
||||
* @throws ArtifactEncryptionFailedException
|
||||
*/
|
||||
Map<String, String> generateSecrets();
|
||||
|
||||
/**
|
||||
* Encrypts artifact stream with provided secrets.
|
||||
*
|
||||
* @param secrets
|
||||
* secrets key/value pairs to be used for encryption
|
||||
* @param stream
|
||||
* artifact stream to encrypt
|
||||
* @return encrypted input stream
|
||||
* @throws ArtifactEncryptionFailedException
|
||||
*/
|
||||
InputStream encryptStream(final Map<String, String> secrets, final InputStream stream);
|
||||
|
||||
/**
|
||||
* Decrypts encrypted artifact stream based on provided secrets.
|
||||
*
|
||||
* @param secrets
|
||||
* secrets key/value pairs to be used for decryption
|
||||
* @param stream
|
||||
* artifact stream to decrypt
|
||||
* @return decrypted input stream
|
||||
* @throws ArtifactEncryptionFailedException
|
||||
*/
|
||||
InputStream decryptStream(final Map<String, String> secrets, final InputStream stream);
|
||||
|
||||
/**
|
||||
* Size of the underlying encryption algorithm overhead in bytes
|
||||
*
|
||||
* @return encryption overhead in byte
|
||||
*/
|
||||
int encryptionSizeOverhead();
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright (c) 2021 Bosch.IO GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModule;
|
||||
|
||||
/**
|
||||
* Interface definition for artifact encryption secrets store.
|
||||
*
|
||||
*/
|
||||
public interface ArtifactEncryptionSecretsStore {
|
||||
|
||||
/**
|
||||
* Adds secret key/value pair associated with particular
|
||||
* {@link SoftwareModule} id to the store.
|
||||
*
|
||||
* @param softwareModuleId
|
||||
* {@link SoftwareModule} id associated with the secret
|
||||
* @param secretKey
|
||||
* key of the secret
|
||||
* @param secretValue
|
||||
* value of the secret
|
||||
*/
|
||||
void addSecret(final long softwareModuleId, final String secretKey, final String secretValue);
|
||||
|
||||
/**
|
||||
* Checks if secret is present for particular {@link SoftwareModule} id and
|
||||
* key in the store.
|
||||
*
|
||||
* @param softwareModuleId
|
||||
* {@link SoftwareModule} id associated with the secret
|
||||
* @param secretKey
|
||||
* key of the secret
|
||||
*/
|
||||
boolean secretExists(final long softwareModuleId, final String secretKey);
|
||||
|
||||
/**
|
||||
* Retrieves secret value associated with particular {@link SoftwareModule}
|
||||
* id and key from the store.
|
||||
*
|
||||
* @param softwareModuleId
|
||||
* {@link SoftwareModule} id associated with the secret
|
||||
* @param secretKey
|
||||
* key of the secret
|
||||
*/
|
||||
Optional<String> getSecret(final long softwareModuleId, final String secretKey);
|
||||
|
||||
/**
|
||||
* Removes secret key/value pair associated with particular
|
||||
* {@link SoftwareModule} id from the store.
|
||||
*
|
||||
* @param softwareModuleId
|
||||
* {@link SoftwareModule} id associated with the secret
|
||||
* @param secretKey
|
||||
* key of the secret
|
||||
*/
|
||||
void removeSecret(final long softwareModuleId, final String secretKey);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Copyright (c) 2021 Bosch.IO GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.hawkbit.repository.exception.ArtifactEncryptionUnsupportedException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
/**
|
||||
* Service responsible for encryption operations.
|
||||
*
|
||||
*/
|
||||
public final class ArtifactEncryptionService {
|
||||
|
||||
private static final ArtifactEncryptionService SINGLETON = new ArtifactEncryptionService();
|
||||
|
||||
@Autowired(required = false)
|
||||
private ArtifactEncryption artifactEncryption;
|
||||
|
||||
@Autowired(required = false)
|
||||
private ArtifactEncryptionSecretsStore artifactEncryptionSecretsStore;
|
||||
|
||||
private ArtifactEncryptionService() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the artifact encryption service singleton instance
|
||||
*/
|
||||
public static ArtifactEncryptionService getInstance() {
|
||||
return SINGLETON;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if required encryption and secrets store beans are present.
|
||||
*
|
||||
* @return if encryption is supported
|
||||
*/
|
||||
public boolean isEncryptionSupported() {
|
||||
return artifactEncryption != null && artifactEncryptionSecretsStore != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates encryption secrets and saves them in secret store by software
|
||||
* module id reference.
|
||||
*
|
||||
* @param smId
|
||||
* software module id
|
||||
*/
|
||||
public void addSoftwareModuleEncryptionSecrets(final long smId) {
|
||||
if (!isEncryptionSupported()) {
|
||||
throw new ArtifactEncryptionUnsupportedException("Encryption secrets generation is not supported.");
|
||||
}
|
||||
|
||||
final Map<String, String> secrets = artifactEncryption.generateSecrets();
|
||||
secrets.forEach((key, value) -> artifactEncryptionSecretsStore.addSecret(smId, key, value));
|
||||
// we want to clear secrets from memory as soon as possible
|
||||
secrets.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts artifact stream using the keys retrieved from secrets store by
|
||||
* software module id reference.
|
||||
*
|
||||
* @param smId
|
||||
* software module id
|
||||
* @param artifactStream
|
||||
* artifact stream to encrypt
|
||||
* @return encrypted input stream
|
||||
*/
|
||||
public InputStream encryptSoftwareModuleArtifact(final long smId, final InputStream artifactStream) {
|
||||
if (!isEncryptionSupported()) {
|
||||
throw new ArtifactEncryptionUnsupportedException("Artifact encryption is not supported.");
|
||||
}
|
||||
|
||||
return artifactEncryption.encryptStream(getSoftwareModuleEncryptionSecrets(smId), artifactStream);
|
||||
}
|
||||
|
||||
private Map<String, String> getSoftwareModuleEncryptionSecrets(final long smId) {
|
||||
final Set<String> requiredSecretsKeys = artifactEncryption.requiredSecretKeys();
|
||||
final Map<String, String> requiredSecrets = new HashMap<>();
|
||||
for (final String requiredSecretsKey : requiredSecretsKeys) {
|
||||
final Optional<String> requiredSecretsValue = artifactEncryptionSecretsStore.getSecret(smId,
|
||||
requiredSecretsKey);
|
||||
requiredSecretsValue.ifPresent(secretValue -> requiredSecrets.put(requiredSecretsKey, secretValue));
|
||||
}
|
||||
|
||||
return requiredSecrets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts artifact stream using the keys retrieved from secrets store by
|
||||
* software module id reference.
|
||||
*
|
||||
* @param smId
|
||||
* software module id
|
||||
* @param encryptedArtifactStream
|
||||
* artifact stream to decrypt
|
||||
* @return decrypted input stream
|
||||
*/
|
||||
public InputStream decryptSoftwareModuleArtifact(final long smId, final InputStream encryptedArtifactStream) {
|
||||
if (!isEncryptionSupported()) {
|
||||
throw new ArtifactEncryptionUnsupportedException("Artifact decryption is not supported.");
|
||||
}
|
||||
|
||||
return artifactEncryption.decryptStream(getSoftwareModuleEncryptionSecrets(smId), encryptedArtifactStream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Size of the underlying encryption algorithm overhead in bytes
|
||||
*
|
||||
* @return encryption overhead in byte
|
||||
*/
|
||||
public int encryptionSizeOverhead() {
|
||||
return artifactEncryption.encryptionSizeOverhead();
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||
import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions;
|
||||
import org.eclipse.hawkbit.repository.exception.ArtifactDeleteFailedException;
|
||||
import org.eclipse.hawkbit.repository.exception.ArtifactUploadFailedException;
|
||||
@@ -69,8 +69,8 @@ public interface ArtifactManagement {
|
||||
|
||||
/**
|
||||
* Garbage collects artifact binaries if only referenced by given
|
||||
* {@link SoftwareModule#getId()} or {@link SoftwareModule}'s that are marked
|
||||
* as deleted.
|
||||
* {@link SoftwareModule#getId()} or {@link SoftwareModule}'s that are
|
||||
* marked as deleted.
|
||||
*
|
||||
*
|
||||
* @param artifactSha1Hash
|
||||
@@ -161,15 +161,20 @@ public interface ArtifactManagement {
|
||||
Page<Artifact> findBySoftwareModule(@NotNull Pageable pageReq, long swId);
|
||||
|
||||
/**
|
||||
* Loads {@link AbstractDbArtifact} from store for given {@link Artifact}.
|
||||
* Loads {@link DbArtifact} from store for given {@link Artifact}.
|
||||
*
|
||||
* @param sha1Hash
|
||||
* to search for
|
||||
* @return loaded {@link AbstractDbArtifact}
|
||||
* @param softwareModuleId
|
||||
* software module id.
|
||||
* @param isEncrypted
|
||||
* flag to indicate if artifact is encrypted.
|
||||
* @return loaded {@link DbArtifact}
|
||||
*
|
||||
*/
|
||||
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_DOWNLOAD_ARTIFACT + SpringEvalExpressions.HAS_AUTH_OR
|
||||
+ SpringEvalExpressions.IS_CONTROLLER)
|
||||
Optional<AbstractDbArtifact> loadArtifactBinary(@NotEmpty String sha1Hash);
|
||||
Optional<DbArtifact> loadArtifactBinary(@NotEmpty String sha1Hash, long softwareModuleId,
|
||||
final boolean isEncrypted);
|
||||
|
||||
}
|
||||
|
||||
@@ -70,6 +70,13 @@ public interface SoftwareModuleCreate {
|
||||
return type(Optional.ofNullable(type).map(SoftwareModuleType::getKey).orElse(null));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param encrypted
|
||||
* if should be encrypted
|
||||
* @return updated builder instance
|
||||
*/
|
||||
SoftwareModuleCreate encrypted(boolean encrypted);
|
||||
|
||||
/**
|
||||
* @return peek on current state of {@link SoftwareModule} in the builder
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2021 Bosch.IO GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.exception;
|
||||
|
||||
import org.eclipse.hawkbit.exception.AbstractServerRtException;
|
||||
import org.eclipse.hawkbit.exception.SpServerError;
|
||||
|
||||
/**
|
||||
* Exception being thrown in case of error while generating encryption secrets,
|
||||
* encrypting or decrypting artifacts.
|
||||
*/
|
||||
public final class ArtifactEncryptionFailedException extends AbstractServerRtException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final EncryptionOperation encryptionOperation;
|
||||
|
||||
public ArtifactEncryptionFailedException(final EncryptionOperation encryptionOperation) {
|
||||
this(encryptionOperation, null, null);
|
||||
}
|
||||
|
||||
public ArtifactEncryptionFailedException(final EncryptionOperation encryptionOperation, final String message) {
|
||||
this(encryptionOperation, message, null);
|
||||
}
|
||||
|
||||
public ArtifactEncryptionFailedException(final EncryptionOperation encryptionOperation, final String message,
|
||||
final Throwable cause) {
|
||||
super(message, SpServerError.SP_ARTIFACT_ENCRYPTION_FAILED, cause);
|
||||
this.encryptionOperation = encryptionOperation;
|
||||
}
|
||||
|
||||
public EncryptionOperation getEncryptionOperation() {
|
||||
return encryptionOperation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encryption operation that caused the exception.
|
||||
*/
|
||||
public enum EncryptionOperation {
|
||||
GENERATE_SECRETS, ENCRYPT, DECRYPT;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2021 Bosch.IO GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.exception;
|
||||
|
||||
import org.eclipse.hawkbit.exception.AbstractServerRtException;
|
||||
import org.eclipse.hawkbit.exception.SpServerError;
|
||||
|
||||
/**
|
||||
* Exception being thrown when artifact encryption is not supported
|
||||
*/
|
||||
public final class ArtifactEncryptionUnsupportedException extends AbstractServerRtException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public ArtifactEncryptionUnsupportedException() {
|
||||
super(SpServerError.SP_ARTIFACT_ENCRYPTION_NOT_SUPPORTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param message
|
||||
* of the error
|
||||
*/
|
||||
public ArtifactEncryptionUnsupportedException(final String message) {
|
||||
super(message, SpServerError.SP_ARTIFACT_ENCRYPTION_NOT_SUPPORTED);
|
||||
}
|
||||
}
|
||||
@@ -67,4 +67,9 @@ public interface SoftwareModule extends NamedVersionedEntity {
|
||||
*/
|
||||
List<DistributionSet> getAssignedTo();
|
||||
|
||||
/**
|
||||
* @return {@code true} if this software module is marked as encrypted
|
||||
* otherwise {@code false}
|
||||
*/
|
||||
boolean isEncrypted();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2021 Bosch.IO GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
import org.eclipse.hawkbit.repository.exception.ArtifactEncryptionUnsupportedException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
|
||||
/**
|
||||
* Test class to verify that no {@link ArtifactEncryptionService} required beans
|
||||
* are loaded and therefore the encryption support is not given.
|
||||
*/
|
||||
@Feature("Unit Tests - Repository")
|
||||
@Story("Artifact Encryption Service")
|
||||
class ArtifactEncryptionServiceTest {
|
||||
|
||||
@Test
|
||||
@Description("Verify that no artifact encryption support is given")
|
||||
void verifyNoArtifactEncryptionSupport() {
|
||||
final ArtifactEncryptionService artifactEncryptionService = ArtifactEncryptionService.getInstance();
|
||||
|
||||
assertThat(artifactEncryptionService.isEncryptionSupported()).isFalse();
|
||||
assertThatExceptionOfType(ArtifactEncryptionUnsupportedException.class)
|
||||
.isThrownBy(() -> artifactEncryptionService.addSoftwareModuleEncryptionSecrets(1L));
|
||||
assertThatExceptionOfType(ArtifactEncryptionUnsupportedException.class)
|
||||
.isThrownBy(() -> artifactEncryptionService.encryptSoftwareModuleArtifact(1L, null));
|
||||
assertThatExceptionOfType(ArtifactEncryptionUnsupportedException.class)
|
||||
.isThrownBy(() -> artifactEncryptionService.decryptSoftwareModuleArtifact(1L, null));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user