SEBSERV-135 finished implementation

This commit is contained in:
anhefti 2021-05-12 09:50:58 +02:00
parent a46e2c0b27
commit a853c02947
25 changed files with 445 additions and 96 deletions

View file

@ -282,4 +282,17 @@ public class APIMessage implements Serializable {
return builder.toString();
}
public static boolean checkError(final Exception error, final ErrorMessage errorMessage) {
if (!(error instanceof APIMessageException)) {
return false;
}
final APIMessageException _error = (APIMessageException) error;
return _error.getAPIMessages()
.stream()
.filter(msg -> errorMessage.messageCode.equals(msg.messageCode))
.findFirst()
.isPresent();
}
}

View file

@ -48,6 +48,7 @@ public final class SEBClientConfig implements GrantEntity, Activatable {
public static final String ATTR_QUIT_PASSWORD_CONFIRM = "hashedQuitPasswordConfirm";
public static final String ATTR_ENCRYPT_SECRET_CONFIRM = "confirm_encrypt_secret";
public static final String ATTR_ENCRYPT_CERTIFICATE_ALIAS = "cert_alias";
public static final String ATTR_ENCRYPT_CERTIFICATE_ASYM = "cert_encryption_asym";
public static final String FILTER_ATTR_CREATION_DATE = "creation_date";
@ -161,6 +162,9 @@ public final class SEBClientConfig implements GrantEntity, Activatable {
@JsonProperty(ATTR_ENCRYPT_CERTIFICATE_ALIAS)
public final String encryptCertificateAlias;
@JsonProperty(ATTR_ENCRYPT_CERTIFICATE_ASYM)
public final Boolean encryptCertificateAsym;
@JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_ACTIVE)
public final Boolean active;
@ -190,6 +194,7 @@ public final class SEBClientConfig implements GrantEntity, Activatable {
@JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_ENCRYPT_SECRET) final CharSequence encryptSecret,
@JsonProperty(ATTR_ENCRYPT_SECRET_CONFIRM) final CharSequence encryptSecretConfirm,
@JsonProperty(ATTR_ENCRYPT_CERTIFICATE_ALIAS) final String encryptCertificateAlias,
@JsonProperty(ATTR_ENCRYPT_CERTIFICATE_ASYM) final Boolean encryptCertificateAsym,
@JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_ACTIVE) final Boolean active) {
this.id = id;
@ -216,6 +221,7 @@ public final class SEBClientConfig implements GrantEntity, Activatable {
this.encryptSecret = encryptSecret;
this.encryptSecretConfirm = encryptSecretConfirm;
this.encryptCertificateAlias = encryptCertificateAlias;
this.encryptCertificateAsym = encryptCertificateAsym;
this.active = active;
}
@ -254,6 +260,7 @@ public final class SEBClientConfig implements GrantEntity, Activatable {
this.encryptSecret = postParams.getCharSequence(Domain.SEB_CLIENT_CONFIGURATION.ATTR_ENCRYPT_SECRET);
this.encryptSecretConfirm = postParams.getCharSequence(ATTR_ENCRYPT_SECRET_CONFIRM);
this.encryptCertificateAlias = postParams.getString(ATTR_ENCRYPT_CERTIFICATE_ALIAS);
this.encryptCertificateAsym = postParams.getBooleanObject(ATTR_ENCRYPT_CERTIFICATE_ASYM);
this.active = false;
}
@ -442,6 +449,7 @@ public final class SEBClientConfig implements GrantEntity, Activatable {
Constants.EMPTY_NOTE,
Constants.EMPTY_NOTE,
Constants.EMPTY_NOTE,
this.encryptCertificateAsym,
this.active);
}
@ -469,6 +477,7 @@ public final class SEBClientConfig implements GrantEntity, Activatable {
null,
null,
null,
false,
false);
}

View file

@ -169,6 +169,11 @@ public final class Utils {
: Collections.emptyList();
}
public static <T extends Collection<V>, V> T addAll(final T target, final T source) {
target.addAll(source);
return target;
}
public static <K, V> Map<K, V> immutableMapOf(final Map<K, V> params) {
return (params != null)
? Collections.unmodifiableMap(params)

View file

@ -18,6 +18,7 @@ import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.CertificateInfo;
@ -72,6 +73,8 @@ public class CertificateList implements TemplateComposer {
new LocTextKey("sebserver.certificate.message.error.file");
static final LocTextKey FORM_IMPORT_CONFIRM_TEXT_KEY =
new LocTextKey("sebserver.certificate.action.import-config.confirm");
static final LocTextKey FORM_ACTION_MESSAGE_IN_USE_TEXT_KEY =
new LocTextKey("sebserver.certificate.action.remove.in-use");
private final TableFilterAttribute aliasFilter = new TableFilterAttribute(
CriteriaType.TEXT,
@ -169,7 +172,13 @@ public class CertificateList implements TemplateComposer {
this.restService.getBuilder(RemoveCertificate.class)
.withFormParam(API.CERTIFICATE_ALIAS, ids)
.call()
.onError(erorr -> action.pageContext().notifyRemoveError(EntityType.CERTIFICATE, erorr));
.onError(error -> {
if (APIMessage.checkError(error, APIMessage.ErrorMessage.INTEGRITY_VALIDATION)) {
action.pageContext().publishInfo(FORM_ACTION_MESSAGE_IN_USE_TEXT_KEY);
} else {
action.pageContext().notifyRemoveError(EntityType.CERTIFICATE, error);
}
});
return action;
}

View file

@ -106,6 +106,8 @@ public class SEBClientConfigForm implements TemplateComposer {
private static final LocTextKey FORM_ENCRYPT_CERT_KEY =
new LocTextKey("sebserver.clientconfig.form.certificate");
private static final LocTextKey FORM_ENCRYPT_CERTIFICATE_ASYM =
new LocTextKey("sebserver.clientconfig.form.type.async");
private static final LocTextKey FORM_ENCRYPT_SECRET_TEXT_KEY =
new LocTextKey("sebserver.clientconfig.form.encryptSecret");
private static final LocTextKey FORM_CONFIRM_ENCRYPT_SECRET_TEXT_KEY =
@ -285,15 +287,6 @@ public class SEBClientConfigForm implements TemplateComposer {
.mandatory(!isReadonly))
.withDefaultSpanEmptyCell(3)
.withDefaultSpanInput(3)
.addField(FormBuilder.singleSelection(
SEBClientConfig.ATTR_ENCRYPT_CERTIFICATE_ALIAS,
FORM_ENCRYPT_CERT_KEY,
clientConfig.encryptCertificateAlias,
() -> this.pageService.getResourceService().identityCertificatesResources()))
.withDefaultSpanEmptyCell(3)
.withDefaultSpanInput(3)
.addField(FormBuilder.password(
Domain.SEB_CLIENT_CONFIGURATION.ATTR_ENCRYPT_SECRET,
FORM_ENCRYPT_SECRET_TEXT_KEY,
@ -307,6 +300,24 @@ public class SEBClientConfigForm implements TemplateComposer {
FORM_CONFIRM_ENCRYPT_SECRET_TEXT_KEY,
pwd))
.withDefaultSpanInput(2)
.addField(FormBuilder.singleSelection(
SEBClientConfig.ATTR_ENCRYPT_CERTIFICATE_ALIAS,
FORM_ENCRYPT_CERT_KEY,
clientConfig.encryptCertificateAlias,
() -> this.pageService.getResourceService().identityCertificatesResources()))
.withDefaultSpanInput(3)
.withDefaultSpanLabel(1)
.addField(FormBuilder.checkbox(
SEBClientConfig.ATTR_ENCRYPT_CERTIFICATE_ASYM,
FORM_ENCRYPT_CERTIFICATE_ASYM,
(BooleanUtils.isTrue(clientConfig.encryptCertificateAsym))
? Constants.TRUE_STRING
: Constants.FALSE_STRING)
.withRightLabel()
.withEmptyCellSeparation(false))
.withDefaultSpanEmptyCell(1)
.withDefaultSpanLabel(2)
.withDefaultSpanInput(2)
.addField(FormBuilder.text(
SEBClientConfig.ATTR_PING_INTERVAL,

View file

@ -8,8 +8,6 @@
package ch.ethz.seb.sebserver.gui.form;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import org.apache.commons.lang3.BooleanUtils;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
@ -17,6 +15,9 @@ import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
public class CheckboxFieldBuilder extends FieldBuilder<String> {
protected CheckboxFieldBuilder(
@ -27,15 +28,31 @@ public class CheckboxFieldBuilder extends FieldBuilder<String> {
super(name, label, value);
}
public FieldBuilder<?> withRightLabel() {
this.rightLabel = true;
return this;
}
@Override
void build(final FormBuilder builder) {
final boolean readonly = builder.readonly || this.readonly;
final Control titleLabel = createTitleLabel(builder.formParent, builder, this);
final Composite fieldGrid = createFieldGrid(builder.formParent, this.spanInput);
final Button checkbox = builder.widgetFactory.buttonLocalized(
fieldGrid,
SWT.CHECK,
null, null);
Control titleLabel = null;
Composite fieldGrid;
Button checkbox;
if (this.rightLabel) {
fieldGrid = createFieldGrid(builder.formParent, this.spanInput, false);
checkbox = builder.widgetFactory.buttonLocalized(
fieldGrid,
SWT.CHECK,
this.label, this.tooltip);
} else {
titleLabel = createTitleLabel(builder.formParent, builder, this);
fieldGrid = createFieldGrid(builder.formParent, this.spanInput);
checkbox = builder.widgetFactory.buttonLocalized(
fieldGrid,
SWT.CHECK,
null, null);
}
final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, true);
checkbox.setLayoutData(gridData);

View file

@ -39,6 +39,7 @@ public abstract class FieldBuilder<T> {
boolean visible = true;
String defaultLabel = null;
boolean isMandatory = false;
boolean rightLabel = false;
final String name;
final LocTextKey label;
@ -195,8 +196,12 @@ public abstract class FieldBuilder<T> {
}
public static Composite createFieldGrid(final Composite parent, final int hspan) {
return createFieldGrid(parent, hspan, false);
}
public static Composite createFieldGrid(final Composite parent, final int hspan, final boolean rightLabel) {
final Composite fieldGrid = new Composite(parent, SWT.NONE);
final GridLayout gridLayout = new GridLayout();
final GridLayout gridLayout = new GridLayout((rightLabel) ? 2 : 1, true);
gridLayout.verticalSpacing = 0;
gridLayout.marginHeight = 0;
gridLayout.marginWidth = 0;

View file

@ -222,7 +222,7 @@ public class CertificateDAOImpl implements CertificateDAO {
}
// dataEncipherment
if (keyUsage[3]) {
if (keyUsage[2] || keyUsage[3]) {
final String alias = certificates.keyStore.engineGetCertificateAlias(cert);
if (this.cryptor.getPrivateKey(certificates.keyStore, alias).hasValue()) {
result.add(CertificateType.DATA_ENCIPHERMENT_PRIVATE_KEY);

View file

@ -28,6 +28,7 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage;
@ -431,6 +432,7 @@ public class SEBClientConfigDAOImpl implements SEBClientConfigDAO {
additionalAttributes.containsKey(SEBClientConfig.ATTR_ENCRYPT_CERTIFICATE_ALIAS)
? additionalAttributes.get(SEBClientConfig.ATTR_ENCRYPT_CERTIFICATE_ALIAS).getValue()
: null,
additionalAttributes.containsKey(SEBClientConfig.ATTR_ENCRYPT_CERTIFICATE_ASYM),
BooleanUtils.toBooleanObject(record.getActive())));
}
@ -608,6 +610,19 @@ public class SEBClientConfigDAOImpl implements SEBClientConfigDAO {
configId,
SEBClientConfig.ATTR_ENCRYPT_CERTIFICATE_ALIAS);
}
if (BooleanUtils.isTrue(sebClientConfig.encryptCertificateAsym)) {
this.additionalAttributesDAO.saveAdditionalAttribute(
EntityType.SEB_CLIENT_CONFIGURATION,
configId,
SEBClientConfig.ATTR_ENCRYPT_CERTIFICATE_ASYM,
Constants.TRUE_STRING);
} else {
this.additionalAttributesDAO.delete(
EntityType.SEB_CLIENT_CONFIGURATION,
configId,
SEBClientConfig.ATTR_ENCRYPT_CERTIFICATE_ASYM);
}
}
}

View file

@ -16,6 +16,9 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SEBConfigEncrypti
* within a concrete strategy. */
public interface SEBConfigEncryptionContext {
/** Get the institution identifier */
Long institutionId();
/** Get the current encryption/decryption strategy
*
* @return the current encryption/decryption strategy */

View file

@ -40,7 +40,6 @@ public interface SEBConfigEncryptionService {
PASSWORD_PWCC(Type.PASSWORD, "pwcc"),
/** Encryption with public/private asymmetric keys and symmetric key */
PUBLIC_KEY_HASH_SYMMETRIC_KEY(Type.CERTIFICATE, "phsk"),
// NOTE not supported yet but eventually needed for SEB config import.
/** Encryption with public/private key */
PUBLIC_KEY_HASH(Type.CERTIFICATE, "pkhs");

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2021 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl;
import java.io.IOException;
import java.io.InputStream;
import java.security.cert.Certificate;
import javax.crypto.Cipher;
import org.apache.commons.codec.digest.DigestUtils;
public abstract class AbstractCertificateCryptor {
protected static final int PUBLIC_KEY_HASH_SIZE = 20;
protected static final int ENCRYPTION_KEY_BITS = 256;
protected static final int KEY_LENGTH_SIZE = 4;
protected byte[] generatePublicKeyHash(final Certificate cert) {
try {
final org.bouncycastle.asn1.x509.Certificate bcCert =
org.bouncycastle.asn1.x509.Certificate.getInstance(cert.getEncoded());
final byte[] bytes = bcCert.getSubjectPublicKeyInfo().getPublicKeyData().getBytes();
return DigestUtils.sha1(bytes);
} catch (final Exception e) {
throw new RuntimeException("Failed to generate public key hash:" + e.getMessage(), e);
}
}
protected byte[] encryptWithCert(final Certificate cert, final byte[] data) throws Exception {
return encryptWithCert(cert, data, data.length);
}
protected byte[] encryptWithCert(final Certificate cert, final byte[] data, final int length) throws Exception {
final String algorithm = cert.getPublicKey().getAlgorithm();
final Cipher encryptCipher = Cipher.getInstance(algorithm);
encryptCipher.init(Cipher.ENCRYPT_MODE, cert);
return encryptCipher.doFinal(data, 0, length);
}
protected byte[] decryptWithCert(final Certificate cert, final byte[] encryptedData) throws Exception {
final String algorithm = cert.getPublicKey().getAlgorithm();
final Cipher encryptCipher = Cipher.getInstance(algorithm);
encryptCipher.init(Cipher.DECRYPT_MODE, cert);
return encryptCipher.doFinal(encryptedData);
}
protected byte[] parsePublicKeyHash(final InputStream input) throws IOException {
final byte[] publicKeyHash = new byte[PUBLIC_KEY_HASH_SIZE];
input.read(publicKeyHash, 0, publicKeyHash.length);
return publicKeyHash;
}
}

View file

@ -0,0 +1,95 @@
/*
* Copyright (c) 2021 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.cert.Certificate;
import java.util.Set;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SEBConfigCryptor;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SEBConfigEncryptionContext;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SEBConfigEncryptionService.Strategy;
@Lazy
@Component
@WebServiceProfile
public class CertificateAsymetricKeyCryptor extends AbstractCertificateCryptor implements SEBConfigCryptor {
private static final Logger log = LoggerFactory.getLogger(CertificateAsymetricKeyCryptor.class);
private static final Set<Strategy> STRATEGIES = Utils.immutableSetOf(
Strategy.PUBLIC_KEY_HASH);
@Override
public Set<Strategy> strategies() {
return STRATEGIES;
}
@Override
public void encrypt(
final OutputStream output,
final InputStream input,
final SEBConfigEncryptionContext context) {
if (log.isDebugEnabled()) {
log.debug("*** Start streaming asynchronous certificate asymmetric-key encryption");
}
try {
final Certificate certificate = context.getCertificate();
final byte[] publicKeyHash = generatePublicKeyHash(certificate);
output.write(publicKeyHash, 0, publicKeyHash.length);
final byte[] buffer = new byte[128];
int readBytes = input.read(buffer, 0, buffer.length);
while (readBytes > 0) {
final byte[] encryptedBlock = encryptWithCert(certificate, buffer, readBytes);
output.write(encryptedBlock, 0, encryptedBlock.length);
readBytes = input.read(buffer, 0, buffer.length);
}
} catch (final Exception e) {
log.error("Error while trying to stream and encrypt data: ", e);
} finally {
IOUtils.closeQuietly(input);
try {
output.flush();
output.close();
} catch (final Exception e) {
log.error("Failed to close output: ", e);
}
}
}
@Override
public void decrypt(
final OutputStream output,
final InputStream input,
final SEBConfigEncryptionContext context) {
// TODO Auto-generated method stub
if (log.isDebugEnabled()) {
log.debug("*** Start streaming asynchronous certificate asymmetric-key decryption");
}
}
}

View file

@ -11,12 +11,14 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@ -25,6 +27,8 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.CertificateInfo;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.CertificateInfo.CertificateFileType;
@ -35,6 +39,7 @@ import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.CertificateDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SEBClientConfigDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.CertificateService;
@Lazy
@ -43,9 +48,15 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.CertificateServic
public class CertificateServiceImpl implements CertificateService {
private final CertificateDAO certificateDAO;
private final SEBClientConfigDAO sebClientConfigDAO;
public CertificateServiceImpl(
final CertificateDAO certificateDAO,
final SEBClientConfigDAO sebClientConfigDAO) {
public CertificateServiceImpl(final CertificateDAO certificateDAO) {
this.certificateDAO = certificateDAO;
this.sebClientConfigDAO = sebClientConfigDAO;
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
}
@Override
@ -103,6 +114,18 @@ public class CertificateServiceImpl implements CertificateService {
@Override
public Result<EntityKey> removeCertificate(final Long institutionId, final String alias) {
// TODO check if certificate is in use
if (this.sebClientConfigDAO.all(institutionId, true)
.getOr(Collections.emptyList())
.stream()
.filter(config -> alias.equals(config.encryptCertificateAlias))
.findFirst()
.isPresent()) {
throw new APIMessageException(APIMessage.ErrorMessage.INTEGRITY_VALIDATION);
}
return this.certificateDAO.removeCertificate(institutionId, alias);
}

View file

@ -12,20 +12,17 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.security.KeyFactory;
import java.nio.ByteOrder;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Iterator;
import java.util.Objects;
import java.util.Set;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.tomcat.util.http.fileupload.ByteArrayOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -33,8 +30,10 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.Certificates;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.CertificateService;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SEBConfigCryptor;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SEBConfigEncryptionContext;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SEBConfigEncryptionService.Strategy;
@ -42,26 +41,34 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SEBConfigEncrypti
@Lazy
@Component
@WebServiceProfile
public class CertificateSymetricKeyCryptor implements SEBConfigCryptor {
/** Uses symetric key for encryption of the data and a certificate asymetric key to encrypt
* and decrypt the symetric key. All is put together as described here:
* https://www.safeexambrowser.org/developer/seb-file-format.html
*
* <pre>
* | phsk | public key hash | key length | encrypted key | ... encrypted data ... |
* | 0 - 3 | 4 - 23 | 24 - 27 | 28 - (28+kl) | (29+kl) - n |
* </pre>
*/
public class CertificateSymetricKeyCryptor extends AbstractCertificateCryptor implements SEBConfigCryptor {
private static final Logger log = LoggerFactory.getLogger(CertificateSymetricKeyCryptor.class);
private static final Set<Strategy> STRATEGIES = Utils.immutableSetOf(
Strategy.PUBLIC_KEY_HASH_SYMMETRIC_KEY);
private static final int PUBLIC_KEY_HASH_SIZE = 20;
private static final int ENCRYPTION_KEY_BITS = 256;
private static final int ENCRYPTION_KEY_LENGTH = 32;
private static final int KEY_LENGTH_SIZE = 4;
private final PasswordEncryptor passwordEncryptor;
private final PasswordDecryptor passwordDecryptor;
private final CertificateService certificateService;
public CertificateSymetricKeyCryptor(
final PasswordEncryptor passwordEncryptor,
final PasswordDecryptor passwordDecryptor) {
final PasswordDecryptor passwordDecryptor,
final CertificateService certificateService) {
this.passwordEncryptor = passwordEncryptor;
this.passwordDecryptor = passwordDecryptor;
this.certificateService = certificateService;
}
@Override
@ -76,7 +83,7 @@ public class CertificateSymetricKeyCryptor implements SEBConfigCryptor {
final SEBConfigEncryptionContext context) {
if (log.isDebugEnabled()) {
log.debug("*** Start streaming asynchronous certificate encryption");
log.debug("*** Start streaming asynchronous certificate symmetric-key encryption");
}
try {
@ -93,6 +100,14 @@ public class CertificateSymetricKeyCryptor implements SEBConfigCryptor {
} catch (final Exception e) {
log.error("Error while trying to stream and encrypt data: ", e);
} finally {
IOUtils.closeQuietly(input);
try {
output.flush();
output.close();
} catch (final Exception e) {
log.error("Failed to close output: ", e);
}
}
}
@ -102,31 +117,75 @@ public class CertificateSymetricKeyCryptor implements SEBConfigCryptor {
final InputStream input,
final SEBConfigEncryptionContext context) {
// TODO Auto-generated method stub
if (log.isDebugEnabled()) {
log.debug("*** Start streaming asynchronous certificate symmetric-key decryption");
}
try {
final Certificate certificate = context.getCertificate();
final byte[] publicKeyHash = parsePublicKeyHash(input);
final Certificate cert = getCertificateByPublicKeyHash(
context.institutionId(),
publicKeyHash);
final byte[] encryptedKey = getEncryptedKey(input);
final byte[] symetricKey = decryptWithCert(cert, encryptedKey);
final CharSequence symetricKeyBase64 = Base64.getEncoder().encodeToString(symetricKey);
this.passwordDecryptor.decrypt(output, input, symetricKeyBase64);
} catch (final Exception e) {
log.error("Error while trying to stream and decrypt data: ", e);
} finally {
try {
output.flush();
output.close();
} catch (final Exception e) {
log.error("Failed to close output: ", e);
}
}
}
private byte[] generatePublicKeyHash(final Certificate cert)
throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] publicKey = cert.getPublicKey().getEncoded();
final PublicKey publicKey2 = cert.getPublicKey();
final KeyFactory keyFactory = KeyFactory.getInstance("RSA");
final X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKey);
final PublicKey _publicKey = keyFactory.generatePublic(keySpec);
publicKey = _publicKey.getEncoded();
private byte[] getEncryptedKey(final InputStream input) throws IOException {
final String algorithm = publicKey2.getAlgorithm();
final String encodeHex = String.valueOf(Hex.encodeHex(publicKey));
final String sha1Hex = DigestUtils.sha1Hex(publicKey);
final String sha256Hex = DigestUtils.sha256Hex(publicKey);
return DigestUtils.sha1(publicKey);
// first get the length of the encrypted key
final byte[] keyLength = new byte[KEY_LENGTH_SIZE];
input.read(keyLength, 0, keyLength.length);
final int keyLengthInt = ByteBuffer
.wrap(keyLength)
.order(ByteOrder.LITTLE_ENDIAN)
.getInt();
// then get the encrypted symmetric key
final byte[] encryptedKey = new byte[keyLengthInt];
input.read(encryptedKey, 0, encryptedKey.length);
return encryptedKey;
}
private Certificate getCertificateByPublicKeyHash(final Long institutionId, final byte[] publicKeyHash) {
try {
final Certificates certs = this.certificateService
.getCertificates(institutionId)
.getOrThrow();
@SuppressWarnings("unchecked")
final Iterator<String> asIterator = certs.keyStore
.engineAliases()
.asIterator();
while (asIterator.hasNext()) {
final Certificate certificate = certs.keyStore.engineGetCertificate(asIterator.next());
final byte[] otherPublicKeyHash = generatePublicKeyHash(certificate);
if (Objects.equals(otherPublicKeyHash, publicKeyHash)) {
return certificate;
}
}
return null;
} catch (final Exception e) {
log.error("Unexpected error while trying to get certificate by public key hash: ", e);
return null;
}
}
private byte[] generateSymetricKey() throws NoSuchAlgorithmException {
@ -143,9 +202,10 @@ public class CertificateSymetricKeyCryptor implements SEBConfigCryptor {
final byte[] symetricKey) throws Exception {
final ByteArrayOutputStream data = new ByteArrayOutputStream();
final byte[] encryptedKey = generateEncryptedKey(cert, symetricKey);
final byte[] encryptedKey = encryptWithCert(cert, symetricKey);
final byte[] encryptedKeyLength = ByteBuffer
.allocate(KEY_LENGTH_SIZE)
.order(ByteOrder.LITTLE_ENDIAN)
.putInt(encryptedKey.length)
.array();
@ -157,18 +217,4 @@ public class CertificateSymetricKeyCryptor implements SEBConfigCryptor {
return byteArray;
}
private byte[] generateEncryptedKey(final Certificate cert, final byte[] symetricKey) throws Exception {
final String algorithm = cert.getPublicKey().getAlgorithm();
final Cipher encryptCipher = Cipher.getInstance(algorithm);
encryptCipher.init(Cipher.ENCRYPT_MODE, cert);
final byte[] cipherText = encryptCipher.doFinal(symetricKey);
return cipherText;
}
private byte[] parsePublicKeyHash(final InputStream input) throws IOException {
final byte[] publicKeyHash = new byte[PUBLIC_KEY_HASH_SIZE];
input.read(publicKeyHash, 0, publicKeyHash.length);
return publicKeyHash;
}
}

View file

@ -8,7 +8,6 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
@ -273,7 +272,7 @@ public class ClientConfigServiceImpl implements ClientConfigService {
this.sebConfigEncryptionService.streamEncrypted(
zipOut,
pIn,
EncryptionContext.contextOfPlainText());
EncryptionContext.contextOfPlainText(config.institutionId));
}
// ZIP again to finish up
@ -300,7 +299,10 @@ public class ClientConfigServiceImpl implements ClientConfigService {
.getOrThrow();
return EncryptionContext.contextOf(
SEBConfigEncryptionService.Strategy.PUBLIC_KEY_HASH_SYMMETRIC_KEY,
config.institutionId,
(config.encryptCertificateAsym)
? SEBConfigEncryptionService.Strategy.PUBLIC_KEY_HASH
: SEBConfigEncryptionService.Strategy.PUBLIC_KEY_HASH_SYMMETRIC_KEY,
certificate);
}
@ -314,6 +316,7 @@ public class ClientConfigServiceImpl implements ClientConfigService {
config.configPurpose)
: null;
return EncryptionContext.contextOf(
config.institutionId,
(config.configPurpose == ConfigPurpose.CONFIGURE_CLIENT)
? SEBConfigEncryptionService.Strategy.PASSWORD_PWCC
: SEBConfigEncryptionService.Strategy.PASSWORD_PSWD,
@ -503,7 +506,7 @@ public class ClientConfigServiceImpl implements ClientConfigService {
private void certificateEncryption(
final OutputStream out,
final SEBClientConfig config,
final InputStream in) throws IOException {
final InputStream in) {
if (log.isDebugEnabled()) {
log.debug("*** SEB client configuration with certificate based encryption");
@ -511,20 +514,33 @@ public class ClientConfigServiceImpl implements ClientConfigService {
final boolean withPasswordEncryption = config.hasEncryptionSecret();
PipedOutputStream passEncryptionOut = null;
PipedInputStream passEncryptionIn = null;
PipedOutputStream streamOut = null;
PipedInputStream streamIn = null;
try {
streamOut = new PipedOutputStream();
streamIn = new PipedInputStream(streamOut);
if (withPasswordEncryption) {
// encrypt with password first
passEncryptionOut = new PipedOutputStream();
passEncryptionIn = new PipedInputStream(passEncryptionOut);
passwordEncryption(passEncryptionOut, config, in);
if (withPasswordEncryption) {
// encrypt with password first
passwordEncryption(streamOut, config, in);
} else {
// just add plaintext header
this.sebConfigEncryptionService.streamEncrypted(
streamOut,
in,
EncryptionContext.contextOfPlainText(config.institutionId));
}
this.sebConfigEncryptionService.streamEncrypted(
out,
streamIn,
buildCertificateEncryptionContext(config));
} catch (final Exception e) {
log.error("Unexpected error while tying to stream certificate encrypted config: ", e);
IOUtils.closeQuietly(streamOut);
IOUtils.closeQuietly(streamIn);
}
this.sebConfigEncryptionService.streamEncrypted(
out,
(withPasswordEncryption) ? passEncryptionIn : in,
buildCertificateEncryptionContext(config));
}
private void passwordEncryption(

View file

@ -225,6 +225,7 @@ public class ExamConfigServiceImpl implements ExamConfigService {
cryptOut,
cryptIn,
EncryptionContext.contextOf(
institutionId,
Strategy.PASSWORD_PSWD,
encryptionPasswordPlaintext));
@ -342,7 +343,7 @@ public class ExamConfigServiceImpl implements ExamConfigService {
streamDecrypted = this.sebConfigEncryptionService.streamDecrypted(
cryptOut,
cryptIn,
EncryptionContext.contextOf(password));
EncryptionContext.contextOf(config.institutionId, password));
// if zipped, unzip attach unzip stream first
unzippedIn = this.examConfigIO.unzip(plainIn);

View file

@ -190,20 +190,28 @@ public final class SEBConfigEncryptionServiceImpl implements SEBConfigEncryption
static class EncryptionContext implements SEBConfigEncryptionContext {
public final Long institutionId;
public final Strategy strategy;
public final CharSequence password;
public final Certificate certificate;
private EncryptionContext(
final Long institutionId,
final Strategy strategy,
final CharSequence password,
final Certificate certificate) {
this.institutionId = institutionId;
this.strategy = strategy;
this.password = password;
this.certificate = certificate;
}
@Override
public Long institutionId() {
return this.institutionId;
}
@Override
public Strategy getStrategy() {
return this.strategy;
@ -220,19 +228,21 @@ public final class SEBConfigEncryptionServiceImpl implements SEBConfigEncryption
}
static SEBConfigEncryptionContext contextOf(
final Long institutionId,
final Strategy strategy,
final CharSequence password) {
checkPasswordBased(strategy);
return new EncryptionContext(strategy, password, null);
return new EncryptionContext(institutionId, strategy, password, null);
}
static SEBConfigEncryptionContext contextOf(
final Long institutionId,
final Strategy strategy,
final Certificate certificate) {
checkCertificateBased(strategy);
return new EncryptionContext(strategy, null, certificate);
return new EncryptionContext(institutionId, strategy, null, certificate);
}
static void checkPasswordBased(final Strategy strategy) {
@ -247,12 +257,16 @@ public final class SEBConfigEncryptionServiceImpl implements SEBConfigEncryption
}
}
public static SEBConfigEncryptionContext contextOfPlainText() {
return new EncryptionContext(Strategy.PLAIN_TEXT, null, null);
public static SEBConfigEncryptionContext contextOfPlainText(final Long institutionId) {
return new EncryptionContext(institutionId, Strategy.PLAIN_TEXT, null, null);
}
public static SEBConfigEncryptionContext contextOf(final CharSequence password) {
return new EncryptionContext(null, password, null);
public static SEBConfigEncryptionContext contextOf(
final Long institutionId,
final CharSequence password) {
return new EncryptionContext(institutionId, null, password, null);
}
}

View file

@ -47,7 +47,7 @@ public class ZipServiceImpl implements ZipService {
log.error("Error while streaming data to zipped output: ", e);
} finally {
try {
in.close();
IOUtils.closeQuietly(in);
if (zipOutputStream != null) {
zipOutputStream.flush();
zipOutputStream.close();

View file

@ -193,7 +193,7 @@ public class CertificateController {
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public CertificateInfo loadCertificate(
public CertificateInfo importCertificate(
@RequestParam(
name = API.PARAM_INSTITUTION_ID,
required = true,

View file

@ -731,7 +731,8 @@ sebserver.clientconfig.form.sebConfigPurpose=Configuration Purpose
sebserver.clientconfig.form.sebConfigPurpose.tooltip=This indicates whether this connection configuration shall be used to configure the SEB Client or to start an exam
sebserver.clientconfig.form.certificate=Encrypt with Certificate
sebserver.clientconfig.form.certificate.tooltip=Choose identity certificate to be used for encrypting the connection configuration
sebserver.clientconfig.form.type.async=Use asymmetric-only encryption
sebserver.clientconfig.form.type.async.tooltip=Use old asymmetric-only encryption (for SEB < 2.2)
sebserver.clientconfig.config.purpose.START_EXAM=Starting an Exam
sebserver.clientconfig.config.purpose.START_EXAM.tooltip=If the connection configuration is loaded via a SEB-Link, the local configuration will not be overwritten.
@ -1562,6 +1563,7 @@ sebserver.certificate.action.import.missing-password=The certificate file needs
sebserver.certificate.action.import-config.confirm=Certificate(s) successfully imported
sebserver.certificate.message.error.file=Unsupported file type
sebserver.certificate.action.import-file-select.no=Please select a valid file
sebserver.certificate.action.remove.in-use=This certificate is in use and cannot be removed.

View file

@ -125,6 +125,7 @@ public class ModelObjectJSONGenerator {
"encryptSecret",
"encryptSecretConfirm",
"certAlias",
false,
true);
System.out.println(domainObject.getClass().getSimpleName() + ":");
System.out.println(writerWithDefaultPrettyPrinter.writeValueAsString(domainObject));

View file

@ -125,6 +125,7 @@ public class ClientConfigTest extends GuiIntegrationTest {
"password",
null,
"certAlias",
false,
null))
.call();
@ -155,6 +156,7 @@ public class ClientConfigTest extends GuiIntegrationTest {
"password",
"password",
"certAlias",
false,
null))
.call()
.getOrThrow();

View file

@ -74,6 +74,7 @@ public class PasswordEncryptorTest {
final ByteArrayOutputStream out = new ByteArrayOutputStream(512);
final SEBConfigEncryptionContext context = EncryptionContext.contextOf(
1L,
Strategy.PASSWORD_PWCC,
pwd);

View file

@ -40,7 +40,7 @@ public class SebConfigEncryptionServiceImplTest {
.streamEncrypted(
out,
IOUtils.toInputStream(config, "UTF-8"),
EncryptionContext.contextOfPlainText());
EncryptionContext.contextOfPlainText(1L));
final byte[] plainWithHeader = out.toByteArray();
assertNotNull(plainWithHeader);
@ -50,7 +50,7 @@ public class SebConfigEncryptionServiceImplTest {
sebConfigEncryptionServiceImpl.streamDecrypted(
out2,
new ByteArrayInputStream(plainWithHeader),
EncryptionContext.contextOf(Strategy.PASSWORD_PSWD, (CharSequence) null));
EncryptionContext.contextOf(1L, Strategy.PASSWORD_PSWD, (CharSequence) null));
out2.close();
@ -74,6 +74,7 @@ public class SebConfigEncryptionServiceImplTest {
out,
IOUtils.toInputStream(config, "UTF-8"),
EncryptionContext.contextOf(
1L,
Strategy.PASSWORD_PWCC,
pwd));
@ -87,7 +88,7 @@ public class SebConfigEncryptionServiceImplTest {
sebConfigEncryptionServiceImpl.streamDecrypted(
out2,
new ByteArrayInputStream(byteArray),
EncryptionContext.contextOf(Strategy.PASSWORD_PSWD, pwd));
EncryptionContext.contextOf(1L, Strategy.PASSWORD_PSWD, pwd));
final byte[] byteArray2 = out2.toByteArray();
assertNotNull(byteArray2);