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:
Bondar Bogdan
2021-11-18 09:07:05 +01:00
committed by GitHub
parent 7e28fba104
commit 146735012a
74 changed files with 1214 additions and 324 deletions

View File

@@ -0,0 +1,65 @@
/**
* 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.jpa;
import java.io.InputStream;
import java.util.function.UnaryOperator;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
/**
* {@link DbArtifact} implementation that decrypts the underlying artifact
* binary input stream.
*/
public class EncryptionAwareDbArtifact implements DbArtifact {
private final DbArtifact encryptedDbArtifact;
private final UnaryOperator<InputStream> decryptionFunction;
private final int encryptionOverhead;
public EncryptionAwareDbArtifact(final DbArtifact encryptedDbArtifact,
final UnaryOperator<InputStream> decryptionFunction) {
this.encryptedDbArtifact = encryptedDbArtifact;
this.decryptionFunction = decryptionFunction;
this.encryptionOverhead = 0;
}
public EncryptionAwareDbArtifact(final DbArtifact encryptedDbArtifact,
final UnaryOperator<InputStream> decryptionFunction, final int encryptionOverhead) {
this.encryptedDbArtifact = encryptedDbArtifact;
this.decryptionFunction = decryptionFunction;
this.encryptionOverhead = encryptionOverhead;
}
@Override
public String getArtifactId() {
return encryptedDbArtifact.getArtifactId();
}
@Override
public DbArtifactHash getHashes() {
return encryptedDbArtifact.getHashes();
}
@Override
public long getSize() {
return encryptedDbArtifact.getSize() - encryptionOverhead;
}
@Override
public String getContentType() {
return encryptedDbArtifact.getContentType();
}
@Override
public InputStream getFileInputStream() {
return decryptionFunction.apply(encryptedDbArtifact.getFileInputStream());
}
}

View File

@@ -16,7 +16,9 @@ import org.eclipse.hawkbit.artifact.repository.ArtifactRepository;
import org.eclipse.hawkbit.artifact.repository.ArtifactStoreException;
import org.eclipse.hawkbit.artifact.repository.HashNotMatchException;
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
import org.eclipse.hawkbit.repository.ArtifactEncryptionService;
import org.eclipse.hawkbit.repository.ArtifactManagement;
import org.eclipse.hawkbit.repository.QuotaManagement;
import org.eclipse.hawkbit.repository.exception.ArtifactDeleteFailedException;
@@ -104,15 +106,24 @@ public class JpaArtifactManagement implements ArtifactManagement {
assertArtifactQuota(moduleId, 1);
final AbstractDbArtifact artifact = storeArtifact(artifactUpload);
final AbstractDbArtifact artifact = storeArtifact(artifactUpload, softwareModule.isEncrypted());
return storeArtifactMetadata(softwareModule, filename, artifact, existing);
}
private AbstractDbArtifact storeArtifact(final ArtifactUpload artifactUpload) {
try (final InputStream quotaStream = wrapInQuotaStream(artifactUpload.getInputStream())) {
return artifactRepository.store(tenantAware.getCurrentTenant(), quotaStream, artifactUpload.getFilename(),
artifactUpload.getContentType(), new DbArtifactHash(artifactUpload.getProvidedSha1Sum(),
artifactUpload.getProvidedMd5Sum(), artifactUpload.getProvidedSha256Sum()));
private AbstractDbArtifact storeArtifact(final ArtifactUpload artifactUpload, final boolean isSmEncrypted) {
final String tenant = tenantAware.getCurrentTenant();
final long smId = artifactUpload.getModuleId();
final InputStream stream = artifactUpload.getInputStream();
final String fileName = artifactUpload.getFilename();
final String contentType = artifactUpload.getContentType();
final String providedSha1 = artifactUpload.getProvidedSha1Sum();
final String providedMd5 = artifactUpload.getProvidedMd5Sum();
final String providedSha256 = artifactUpload.getProvidedSha256Sum();
try (final InputStream wrappedStream = wrapInQuotaStream(
isSmEncrypted ? wrapInEncryptionStream(smId, stream) : stream)) {
return artifactRepository.store(tenant, wrappedStream, fileName, contentType,
new DbArtifactHash(providedSha1, providedMd5, providedSha256));
} catch (final ArtifactStoreException | IOException e) {
throw new ArtifactUploadFailedException(e);
} catch (final HashNotMatchException e) {
@@ -126,6 +137,10 @@ public class JpaArtifactManagement implements ArtifactManagement {
}
}
private InputStream wrapInEncryptionStream(final long smId, final InputStream stream) {
return ArtifactEncryptionService.getInstance().encryptSoftwareModuleArtifact(smId, stream);
}
private void assertArtifactQuota(final long id, final int requested) {
QuotaHelper.assertAssignmentQuota(id, requested, quotaManagement.getMaxArtifactsPerSoftwareModule(),
Artifact.class, SoftwareModule.class, localArtifactRepository::countBySoftwareModuleId);
@@ -212,10 +227,26 @@ public class JpaArtifactManagement implements ArtifactManagement {
}
@Override
public Optional<AbstractDbArtifact> loadArtifactBinary(final String sha1Hash) {
return Optional.ofNullable(artifactRepository.existsByTenantAndSha1(tenantAware.getCurrentTenant(), sha1Hash)
? artifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), sha1Hash)
: null);
public Optional<DbArtifact> loadArtifactBinary(final String sha1Hash, final long softwareModuleId,
final boolean isEncrypted) {
final String tenant = tenantAware.getCurrentTenant();
if (artifactRepository.existsByTenantAndSha1(tenant, sha1Hash)) {
final DbArtifact dbArtifact = artifactRepository.getArtifactBySha1(tenant, sha1Hash);
return Optional.ofNullable(
isEncrypted ? wrapInEncryptionAwareDbArtifact(softwareModuleId, dbArtifact) : dbArtifact);
}
return Optional.empty();
}
private final DbArtifact wrapInEncryptionAwareDbArtifact(final long smId, final DbArtifact dbArtifact) {
if (dbArtifact == null) {
return null;
}
final ArtifactEncryptionService encryptionService = ArtifactEncryptionService.getInstance();
return new EncryptionAwareDbArtifact(dbArtifact,
stream -> encryptionService.decryptSoftwareModuleArtifact(smId, stream),
encryptionService.encryptionSizeOverhead());
}
private Artifact storeArtifactMetadata(final SoftwareModule softwareModule, final String providedFilename,

View File

@@ -31,6 +31,7 @@ import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions;
import org.eclipse.hawkbit.repository.ArtifactEncryptionService;
import org.eclipse.hawkbit.repository.ArtifactManagement;
import org.eclipse.hawkbit.repository.QuotaManagement;
import org.eclipse.hawkbit.repository.RepositoryConstants;
@@ -155,7 +156,14 @@ public class JpaSoftwareModuleManagement implements SoftwareModuleManagement {
public SoftwareModule create(final SoftwareModuleCreate c) {
final JpaSoftwareModuleCreate create = (JpaSoftwareModuleCreate) c;
return softwareModuleRepository.save(create.build());
final JpaSoftwareModule sm = softwareModuleRepository.save(create.build());
if (create.isEncrypted()) {
// flush sm creation in order to get an Id
entityManager.flush();
ArtifactEncryptionService.getInstance().addSoftwareModuleEncryptionSecrets(sm.getId());
}
return sm;
}
@Override
@@ -295,8 +303,8 @@ public class JpaSoftwareModuleManagement implements SoftwareModuleManagement {
@Override
public Page<SoftwareModule> findByRsql(final Pageable pageable, final String rsqlParam) {
final Specification<JpaSoftwareModule> spec = RSQLUtility.buildRsqlSpecification(rsqlParam, SoftwareModuleFields.class,
virtualPropertyReplacer, database);
final Specification<JpaSoftwareModule> spec = RSQLUtility.buildRsqlSpecification(rsqlParam,
SoftwareModuleFields.class, virtualPropertyReplacer, database);
return convertSmPage(softwareModuleRepository.findAll(spec, pageable), pageable);
}

View File

@@ -16,6 +16,9 @@ import javax.persistence.EntityManager;
import javax.sql.DataSource;
import org.eclipse.hawkbit.artifact.repository.ArtifactRepository;
import org.eclipse.hawkbit.repository.ArtifactEncryption;
import org.eclipse.hawkbit.repository.ArtifactEncryptionSecretsStore;
import org.eclipse.hawkbit.repository.ArtifactEncryptionService;
import org.eclipse.hawkbit.repository.ArtifactManagement;
import org.eclipse.hawkbit.repository.BaseRepositoryTypeProvider;
import org.eclipse.hawkbit.repository.ControllerManagement;
@@ -245,7 +248,8 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
/**
* @param dsTypeManagement
* for loading {@link TargetType#getCompatibleDistributionSetTypes()}
* for loading
* {@link TargetType#getCompatibleDistributionSetTypes()}
* @return TargetTypeBuilder bean
*/
@Bean
@@ -261,8 +265,9 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
/**
* @param softwareManagement
* for loading {@link DistributionSetType#getMandatoryModuleTypes()}
* and {@link DistributionSetType#getOptionalModuleTypes()}
* for loading
* {@link DistributionSetType#getMandatoryModuleTypes()} and
* {@link DistributionSetType#getOptionalModuleTypes()}
* @return DistributionSetTypeBuilder bean
*/
@Bean
@@ -304,8 +309,8 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
/**
* @return the {@link SystemSecurityContext} singleton bean which make it
* accessible in beans which cannot access the service directly, e.g.
* JPA entities.
* accessible in beans which cannot access the service directly,
* e.g. JPA entities.
*/
@Bean
SystemSecurityContextHolder systemSecurityContextHolder() {
@@ -313,9 +318,9 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
}
/**
* @return the {@link TenantConfigurationManagement} singleton bean which make
* it accessible in beans which cannot access the service directly, e.g.
* JPA entities.
* @return the {@link TenantConfigurationManagement} singleton bean which
* make it accessible in beans which cannot access the service
* directly, e.g. JPA entities.
*/
@Bean
TenantConfigurationManagementHolder tenantConfigurationManagementHolder() {
@@ -324,8 +329,9 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
/**
* @return the {@link SystemManagementHolder} singleton bean which holds the
* current {@link SystemManagement} service and make it accessible in
* beans which cannot access the service directly, e.g. JPA entities.
* current {@link SystemManagement} service and make it accessible
* in beans which cannot access the service directly, e.g. JPA
* entities.
*/
@Bean
SystemManagementHolder systemManagementHolder() {
@@ -333,9 +339,10 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
}
/**
* @return the {@link TenantAwareHolder} singleton bean which holds the current
* {@link TenantAware} service and make it accessible in beans which
* cannot access the service directly, e.g. JPA entities.
* @return the {@link TenantAwareHolder} singleton bean which holds the
* current {@link TenantAware} service and make it accessible in
* beans which cannot access the service directly, e.g. JPA
* entities.
*/
@Bean
TenantAwareHolder tenantAwareHolder() {
@@ -343,9 +350,10 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
}
/**
* @return the {@link SecurityTokenGeneratorHolder} singleton bean which holds
* the current {@link SecurityTokenGenerator} service and make it
* accessible in beans which cannot access the service via injection
* @return the {@link SecurityTokenGeneratorHolder} singleton bean which
* holds the current {@link SecurityTokenGenerator} service and make
* it accessible in beans which cannot access the service via
* injection
*/
@Bean
SecurityTokenGeneratorHolder securityTokenGeneratorHolder() {
@@ -940,7 +948,8 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
}
/**
* Our default {@link BaseRepositoryTypeProvider} bean always provides the NoCountBaseRepository
* Our default {@link BaseRepositoryTypeProvider} bean always provides the
* NoCountBaseRepository
*
* @return a {@link BaseRepositoryTypeProvider} bean
*/
@@ -950,4 +959,17 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
return new NoCountBaseRepositoryTypeProvider();
}
/**
* Default artifact encryption service bean that internally uses
* {@link ArtifactEncryption} and {@link ArtifactEncryptionSecretsStore}
* beans for {@link SoftwareModule} artifacts encryption/decryption
*
* @return a {@link ArtifactEncryptionService} bean
*/
@Bean
@ConditionalOnMissingBean
ArtifactEncryptionService artifactEncryptionService() {
return ArtifactEncryptionService.getInstance();
}
}

View File

@@ -26,13 +26,26 @@ public class JpaSoftwareModuleCreate extends AbstractSoftwareModuleUpdateCreate<
private final SoftwareModuleTypeManagement softwareModuleTypeManagement;
private boolean encrypted;
JpaSoftwareModuleCreate(final SoftwareModuleTypeManagement softwareModuleTypeManagement) {
this.softwareModuleTypeManagement = softwareModuleTypeManagement;
}
@Override
public SoftwareModuleCreate encrypted(final boolean encrypted) {
this.encrypted = encrypted;
return this;
}
public boolean isEncrypted() {
return encrypted;
}
@Override
public JpaSoftwareModule build() {
return new JpaSoftwareModule(getSoftwareModuleTypeFromKeyString(type), name, version, description, vendor);
return new JpaSoftwareModule(getSoftwareModuleTypeFromKeyString(type), name, version, description, vendor,
encrypted);
}
private SoftwareModuleType getSoftwareModuleTypeFromKeyString(final String type) {

View File

@@ -90,6 +90,9 @@ public class JpaSoftwareModule extends AbstractJpaNamedVersionedEntity implement
@OneToMany(mappedBy = "softwareModule", fetch = FetchType.LAZY, targetEntity = JpaSoftwareModuleMetadata.class)
private List<JpaSoftwareModuleMetadata> metadata;
@Column(name = "encrypted")
private boolean encrypted;
/**
* Default constructor.
*/
@@ -97,6 +100,20 @@ public class JpaSoftwareModule extends AbstractJpaNamedVersionedEntity implement
// Default constructor for JPA
}
/**
* parameterized constructor.
*
* @param type
* of the {@link SoftwareModule}
* @param name
* abstract name of the {@link SoftwareModule}
* @param version
* of the {@link SoftwareModule}
*/
public JpaSoftwareModule(final SoftwareModuleType type, final String name, final String version) {
this(type, name, version, null, null, false);
}
/**
* parameterized constructor.
*
@@ -110,12 +127,15 @@ public class JpaSoftwareModule extends AbstractJpaNamedVersionedEntity implement
* of the {@link SoftwareModule}
* @param vendor
* of the {@link SoftwareModule}
* @param encrypted
* encryption flag of the {@link SoftwareModule}
*/
public JpaSoftwareModule(final SoftwareModuleType type, final String name, final String version,
final String description, final String vendor) {
final String description, final String vendor, final boolean encrypted) {
super(name, version, description);
this.vendor = vendor;
this.type = (JpaSoftwareModuleType) type;
this.encrypted = encrypted;
}
public void addArtifact(final Artifact artifact) {
@@ -175,8 +195,8 @@ public class JpaSoftwareModule extends AbstractJpaNamedVersionedEntity implement
* Marks or un-marks this software module as deleted.
*
* @param deleted
* {@code true} if the software module should be marked as
* deleted otherwise {@code false}
* {@code true} if the software module should be marked as deleted
* otherwise {@code false}
*/
public void setDeleted(final boolean deleted) {
this.deleted = deleted;
@@ -186,10 +206,27 @@ public class JpaSoftwareModule extends AbstractJpaNamedVersionedEntity implement
this.type = type;
}
@Override
public boolean isEncrypted() {
return encrypted;
}
/**
* Marks this software module as encrypted.
*
* @param encrypted
* {@code true} if the software module should be marked as encrypted
* otherwise {@code false}
*/
public void setEncrypted(final boolean encrypted) {
this.encrypted = encrypted;
}
@Override
public String toString() {
return "SoftwareModule [deleted=" + deleted + ", name=" + getName() + ", version=" + getVersion()
+ ", revision=" + getOptLockRevision() + ", Id=" + getId() + ", type=" + getType().getName() + "]";
return "SoftwareModule [deleted=" + isDeleted() + ", encrypted=" + isEncrypted() + ", name=" + getName()
+ ", version=" + getVersion() + ", revision=" + getOptLockRevision() + ", Id=" + getId() + ", type="
+ getType().getName() + "]";
}
@Override

View File

@@ -0,0 +1,3 @@
ALTER TABLE sp_base_software_module ADD COLUMN encrypted BOOLEAN;
UPDATE sp_base_software_module SET encrypted = 0;

View File

@@ -0,0 +1,3 @@
ALTER TABLE sp_base_software_module ADD COLUMN encrypted BOOLEAN;
UPDATE sp_base_software_module SET encrypted = 0;

View File

@@ -0,0 +1,3 @@
ALTER TABLE sp_base_software_module ADD COLUMN encrypted BOOLEAN;
UPDATE sp_base_software_module SET encrypted = 0;

View File

@@ -0,0 +1,3 @@
ALTER TABLE sp_base_software_module ADD COLUMN encrypted BOOLEAN;
UPDATE sp_base_software_module SET encrypted = 0;

View File

@@ -1,3 +1 @@
ALTER TABLE sp_distribution_set ADD COLUMN valid BOOLEAN;
UPDATE sp_distribution_set SET valid = 1;
ALTER TABLE sp_distribution_set ADD valid BIT DEFAULT 1;

View File

@@ -0,0 +1 @@
ALTER TABLE sp_base_software_module ADD encrypted BIT DEFAULT 0;