diff --git a/hopsworks-admin/pom.xml b/hopsworks-admin/pom.xml index 0593bc7137..4060ae256a 100644 --- a/hopsworks-admin/pom.xml +++ b/hopsworks-admin/pom.xml @@ -72,6 +72,7 @@ + io.hops.hopsworks hopsworks-rest-utils @@ -79,6 +80,13 @@ provided + + io.hops.hopsworks + hopsworks-security + ${project.version} + provided + + io.hops.hopsworks hopsworks-common diff --git a/hopsworks-admin/src/main/java/io/hops/hopsworks/admin/maintenance/LoggedMaintenanceHelper.java b/hopsworks-admin/src/main/java/io/hops/hopsworks/admin/maintenance/LoggedMaintenanceHelper.java index cefca1fff2..2b95e23991 100644 --- a/hopsworks-admin/src/main/java/io/hops/hopsworks/admin/maintenance/LoggedMaintenanceHelper.java +++ b/hopsworks-admin/src/main/java/io/hops/hopsworks/admin/maintenance/LoggedMaintenanceHelper.java @@ -15,10 +15,10 @@ */ package io.hops.hopsworks.admin.maintenance; -import io.hops.hopsworks.common.security.CertificatesMgmService; import io.hops.hopsworks.common.util.Settings; import io.hops.hopsworks.exceptions.EncryptionMasterPasswordException; import io.hops.hopsworks.persistence.entity.util.VariablesVisibility; +import io.hops.hopsworks.security.password.MasterPasswordService; import javax.ejb.EJB; import javax.ejb.Stateless; @@ -36,7 +36,7 @@ public class LoggedMaintenanceHelper { @EJB private Settings settings; @EJB - private CertificatesMgmService certificatesMgmService; + private MasterPasswordService masterPasswordService; public void updateVariable(String varName, String varValue, @@ -47,8 +47,8 @@ public void updateVariable(String varName, String varValue, public void changeMasterEncryptionPassword(String currentPassword, String newPassword, HttpServletRequest request) throws IOException, EncryptionMasterPasswordException { String userEmail = request.getUserPrincipal().getName(); - certificatesMgmService.checkPassword(currentPassword, userEmail); - Integer opId = certificatesMgmService.initUpdateOperation(); - certificatesMgmService.resetMasterEncryptionPassword(opId, newPassword, userEmail); + masterPasswordService.checkPassword(currentPassword, userEmail); + Integer opId = masterPasswordService.initUpdateOperation(); + masterPasswordService.resetMasterEncryptionPassword(opId, newPassword, userEmail); } } diff --git a/hopsworks-api/pom.xml b/hopsworks-api/pom.xml index 7e6b5cff25..0b48e898e0 100644 --- a/hopsworks-api/pom.xml +++ b/hopsworks-api/pom.xml @@ -173,6 +173,13 @@ + + io.hops.hopsworks + hopsworks-security + ${project.version} + provided + + org.glassfish.jersey.core diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/admin/SystemAdminService.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/admin/SystemAdminService.java index c0db174202..fce6a35d52 100644 --- a/hopsworks-api/src/main/java/io/hops/hopsworks/api/admin/SystemAdminService.java +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/admin/SystemAdminService.java @@ -65,6 +65,7 @@ import io.hops.hopsworks.persistence.entity.user.Users; import io.hops.hopsworks.persistence.entity.util.Variables; import io.hops.hopsworks.restutils.RESTCodes; +import io.hops.hopsworks.security.password.MasterPasswordService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; @@ -103,6 +104,8 @@ public class SystemAdminService { @EJB private CertificatesMgmService certificatesMgmService; @EJB + private MasterPasswordService masterPasswordService; + @EJB private NoCacheResponse noCacheResponse; @EJB private Settings settings; @@ -134,9 +137,9 @@ public Response changeMasterEncryptionPassword(@Context SecurityContext sc, LOGGER.log(Level.FINE, "Requested master encryption password change"); try { Users user = jWTHelper.getUserPrincipal(sc); - certificatesMgmService.checkPassword(oldPassword, user.getEmail()); - Integer operationId = certificatesMgmService.initUpdateOperation(); - certificatesMgmService.resetMasterEncryptionPassword(operationId, newPassword, user.getEmail()); + masterPasswordService.checkPassword(oldPassword, user.getEmail()); + Integer operationId = masterPasswordService.initUpdateOperation(); + masterPasswordService.resetMasterEncryptionPassword(operationId, newPassword, user.getEmail()); RESTApiJsonResponse response = noCacheResponse.buildJsonResponse(Response.Status.CREATED, String.valueOf(operationId)); @@ -154,7 +157,7 @@ public Response changeMasterEncryptionPassword(@Context SecurityContext sc, @GET @Path("/encryptionPass/{opId}") public Response getUpdatePasswordStatus(@PathParam("opId") Integer operationId, @Context SecurityContext sc) { - CertificatesMgmService.UPDATE_STATUS status = certificatesMgmService.getOperationStatus(operationId); + MasterPasswordService.UPDATE_STATUS status = masterPasswordService.getOperationStatus(operationId); switch (status) { case OK: return noCacheResponse.getNoCacheCORSResponseBuilder(Response.Status.OK).build(); diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/filter/AuthFilter.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/filter/AuthFilter.java index 1c0efef899..15821537e0 100644 --- a/hopsworks-api/src/main/java/io/hops/hopsworks/api/filter/AuthFilter.java +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/filter/AuthFilter.java @@ -25,6 +25,7 @@ import io.hops.hopsworks.jwt.AlgorithmFactory; import io.hops.hopsworks.jwt.JWTController; import io.hops.hopsworks.jwt.annotation.JWTRequired; +import io.hops.hopsworks.jwt.exception.SigningKeyEncryptionException; import io.hops.hopsworks.jwt.exception.SigningKeyNotFoundException; import io.hops.hopsworks.jwt.filter.JWTFilter; import io.hops.hopsworks.restutils.JsonResponse; @@ -65,7 +66,7 @@ public class AuthFilter extends JWTFilter { private ResourceInfo resourceInfo; @Override - public Algorithm getAlgorithm(DecodedJWT jwt) throws SigningKeyNotFoundException { + public Algorithm getAlgorithm(DecodedJWT jwt) throws SigningKeyNotFoundException, SigningKeyEncryptionException { return algorithmFactory.getAlgorithm(jwt); } diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/jwt/JWTHelper.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/jwt/JWTHelper.java index 86f7ede3e1..e102e736e4 100644 --- a/hopsworks-api/src/main/java/io/hops/hopsworks/api/jwt/JWTHelper.java +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/jwt/JWTHelper.java @@ -33,6 +33,7 @@ import io.hops.hopsworks.jwt.exception.InvalidationException; import io.hops.hopsworks.jwt.exception.JWTException; import io.hops.hopsworks.jwt.exception.NotRenewableException; +import io.hops.hopsworks.jwt.exception.SigningKeyEncryptionException; import io.hops.hopsworks.jwt.exception.SigningKeyNotFoundException; import io.hops.hopsworks.jwt.exception.VerificationException; import io.hops.hopsworks.persistence.entity.project.Project; @@ -163,8 +164,7 @@ public String getAuthToken(ContainerRequestContext req) { * @throws DuplicateSigningKeyException */ public String createToken(Users user, String issuer, Map claims) throws NoSuchAlgorithmException, - SigningKeyNotFoundException, - DuplicateSigningKeyException { + SigningKeyNotFoundException, DuplicateSigningKeyException, SigningKeyEncryptionException { String[] audience = null; Date expiresAt = null; @@ -204,7 +204,8 @@ public String createOneTimeToken(Users user, String issuer, Map try { token = createOneTimeToken(user, roles, issuer, audience, now, expiresAt, Constants.ONE_TIME_JWT_SIGNING_KEY_NAME, claims, false); - } catch (NoSuchAlgorithmException | SigningKeyNotFoundException | DuplicateSigningKeyException ex) { + } catch (NoSuchAlgorithmException | SigningKeyNotFoundException | DuplicateSigningKeyException | + SigningKeyEncryptionException ex) { Logger.getLogger(JWTHelper.class.getName()).log(Level.SEVERE, null, ex); } return token; @@ -212,7 +213,8 @@ public String createOneTimeToken(Users user, String issuer, Map public String createOneTimeToken(Users user, String[] roles, String issuer, String[] audience, Date notBefore, Date expiresAt, String keyName, Map claims, boolean createNewKey) - throws NoSuchAlgorithmException, SigningKeyNotFoundException, DuplicateSigningKeyException { + throws NoSuchAlgorithmException, SigningKeyNotFoundException, DuplicateSigningKeyException, + SigningKeyEncryptionException { SignatureAlgorithm algorithm = SignatureAlgorithm.valueOf(Constants.ONE_TIME_JWT_SIGNATURE_ALGORITHM); claims = jwtController.addDefaultClaimsIfMissing(claims, false, 0, roles); @@ -233,7 +235,8 @@ public String createOneTimeToken(Users user, String[] roles, String issuer, Stri * @throws DuplicateSigningKeyException */ public String createToken(Users user, String[] audience, String issuer, Date expiresAt, Map claims) - throws NoSuchAlgorithmException, SigningKeyNotFoundException, DuplicateSigningKeyException { + throws NoSuchAlgorithmException, SigningKeyNotFoundException, DuplicateSigningKeyException, + SigningKeyEncryptionException { SignatureAlgorithm alg = SignatureAlgorithm.valueOf(settings.getJWTSignatureAlg()); String[] roles = userController.getUserRoles(user).toArray(new String[0]); @@ -253,7 +256,7 @@ public String createToken(Users user, String[] audience, String issuer, Date exp * @throws DuplicateSigningKeyException */ public JWTResponseDTO createToken(JWTRequestDTO jWTRequestDTO, String issuer) throws NoSuchAlgorithmException, - SigningKeyNotFoundException, DuplicateSigningKeyException { + SigningKeyNotFoundException, DuplicateSigningKeyException, SigningKeyEncryptionException { if (jWTRequestDTO == null || jWTRequestDTO.getKeyName() == null || jWTRequestDTO.getKeyName().isEmpty() || jWTRequestDTO.getAudiences() == null || jWTRequestDTO.getAudiences().length == 0 || jWTRequestDTO.getSubject() == null || jWTRequestDTO.getSubject().isEmpty()) { @@ -302,9 +305,8 @@ public boolean validToken(HttpServletRequest req, String issuer) { * @throws NotRenewableException * @throws InvalidationException */ - public JWTResponseDTO renewToken(JsonWebTokenDTO jsonWebTokenDTO, boolean invalidate, - Map claims) - throws SigningKeyNotFoundException, NotRenewableException, InvalidationException { + public JWTResponseDTO renewToken(JsonWebTokenDTO jsonWebTokenDTO, boolean invalidate, Map claims) + throws SigningKeyNotFoundException, NotRenewableException, InvalidationException, SigningKeyEncryptionException { if (jsonWebTokenDTO == null || jsonWebTokenDTO.getToken() == null || jsonWebTokenDTO.getToken().isEmpty()) { throw new IllegalArgumentException("No token provided."); } @@ -414,8 +416,8 @@ public void deleteSigningKeyByName(String keyName) { * @throws SigningKeyNotFoundException * @throws VerificationException */ - public DecodedJWT verifyOneTimeToken(String token, String issuer) throws SigningKeyNotFoundException, - VerificationException { + public DecodedJWT verifyOneTimeToken(String token, String issuer) throws SigningKeyNotFoundException, + VerificationException, SigningKeyEncryptionException { DecodedJWT jwt = null; if (token == null || token.trim().isEmpty()) { throw new VerificationException("Token not provided."); diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/jwt/JWTResource.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/jwt/JWTResource.java index 06e94ce218..7c42d49bdf 100644 --- a/hopsworks-api/src/main/java/io/hops/hopsworks/api/jwt/JWTResource.java +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/jwt/JWTResource.java @@ -27,6 +27,7 @@ import io.hops.hopsworks.jwt.exception.InvalidationException; import io.hops.hopsworks.jwt.exception.JWTException; import io.hops.hopsworks.jwt.exception.NotRenewableException; +import io.hops.hopsworks.jwt.exception.SigningKeyEncryptionException; import io.hops.hopsworks.jwt.exception.SigningKeyNotFoundException; import io.hops.hopsworks.persistence.entity.user.Users; import io.hops.hopsworks.restutils.RESTCodes; @@ -75,7 +76,7 @@ public class JWTResource { @POST @ApiOperation(value = "Create application token", response = JWTResponseDTO.class) public Response createToken(JWTRequestDTO jWTRequestDTO, @Context SecurityContext sc) throws NoSuchAlgorithmException, - SigningKeyNotFoundException, DuplicateSigningKeyException { + SigningKeyNotFoundException, DuplicateSigningKeyException, SigningKeyEncryptionException { JWTResponseDTO jWTResponseDTO = jWTHelper.createToken(jWTRequestDTO, settings.getJWTIssuer()); return Response.ok().entity(jWTResponseDTO).build(); } @@ -83,7 +84,7 @@ public Response createToken(JWTRequestDTO jWTRequestDTO, @Context SecurityContex @PUT @ApiOperation(value = "Renew application token", response = JWTResponseDTO.class) public Response renewToken(JsonWebTokenDTO jsonWebTokenDTO, @Context SecurityContext sc) - throws SigningKeyNotFoundException, NotRenewableException, InvalidationException { + throws SigningKeyNotFoundException, NotRenewableException, InvalidationException, SigningKeyEncryptionException { JWTResponseDTO jWTResponseDTO = jWTHelper.renewToken(jsonWebTokenDTO, true, new HashMap<>(3)); return Response.ok().entity(jWTResponseDTO).build(); } diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/user/AuthService.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/user/AuthService.java index 9eef312b43..026931fb87 100644 --- a/hopsworks-api/src/main/java/io/hops/hopsworks/api/user/AuthService.java +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/user/AuthService.java @@ -60,6 +60,7 @@ import io.hops.hopsworks.jwt.annotation.JWTRequired; import io.hops.hopsworks.jwt.exception.DuplicateSigningKeyException; import io.hops.hopsworks.jwt.exception.InvalidationException; +import io.hops.hopsworks.jwt.exception.SigningKeyEncryptionException; import io.hops.hopsworks.jwt.exception.SigningKeyNotFoundException; import io.hops.hopsworks.persistence.entity.user.Users; import io.hops.hopsworks.persistence.entity.util.FormatUtils; @@ -156,8 +157,7 @@ public Response jwtSession(@Context SecurityContext sc) { @JWTNotRequired public Response login(@FormParam("email") String email, @FormParam("password") String password, @FormParam("otp") String otp, @Context HttpServletRequest req) throws UserException, SigningKeyNotFoundException, - NoSuchAlgorithmException, - LoginException, DuplicateSigningKeyException { + NoSuchAlgorithmException, LoginException, DuplicateSigningKeyException, SigningKeyEncryptionException { if (email == null || email.isEmpty()) { throw new IllegalArgumentException("Email was not provided"); @@ -204,7 +204,7 @@ public Response logout(@Context HttpServletRequest req) throws UserException, In @JWTNotRequired public Response serviceLogin(@FormParam("email") String email, @FormParam("password") String password, @Context HttpServletRequest request) throws UserException, GeneralSecurityException, SigningKeyNotFoundException, - DuplicateSigningKeyException, HopsSecurityException { + DuplicateSigningKeyException, HopsSecurityException, SigningKeyEncryptionException { if (Strings.isNullOrEmpty(email)) { throw new IllegalArgumentException("Email cannot be null or empty"); } @@ -418,7 +418,7 @@ private void logoutSession(HttpServletRequest req) throws UserException { } private Response login(Users user, String password, HttpServletRequest req) throws UserException, - SigningKeyNotFoundException, NoSuchAlgorithmException, DuplicateSigningKeyException { + SigningKeyNotFoundException, NoSuchAlgorithmException, DuplicateSigningKeyException, SigningKeyEncryptionException { RESTApiJsonResponse json = new RESTApiJsonResponse(); if (user.getBbcGroupCollection() == null || user.getBbcGroupCollection().isEmpty()) { throw new UserException(RESTCodes.UserErrorCode.NO_ROLE_FOUND, Level.FINE, diff --git a/hopsworks-api/src/main/java/io/hops/hopsworks/api/util/DownloadService.java b/hopsworks-api/src/main/java/io/hops/hopsworks/api/util/DownloadService.java index b4c229f74a..32758a1b26 100644 --- a/hopsworks-api/src/main/java/io/hops/hopsworks/api/util/DownloadService.java +++ b/hopsworks-api/src/main/java/io/hops/hopsworks/api/util/DownloadService.java @@ -57,6 +57,7 @@ import io.hops.hopsworks.exceptions.DatasetException; import io.hops.hopsworks.exceptions.ProjectException; import io.hops.hopsworks.jwt.annotation.JWTRequired; +import io.hops.hopsworks.jwt.exception.SigningKeyEncryptionException; import io.hops.hopsworks.jwt.exception.SigningKeyNotFoundException; import io.hops.hopsworks.jwt.exception.VerificationException; import io.hops.hopsworks.persistence.entity.dataset.Dataset; @@ -171,7 +172,7 @@ public Response getDownloadToken(@PathParam("path") String path, @QueryParam("ty @ApiOperation(value = "Download file.", response = StreamingOutput.class) public Response downloadFromHDFS(@PathParam("path") String path, @QueryParam("token") String token, @QueryParam("type") DatasetType datasetType, @Context SecurityContext sc) throws DatasetException, - SigningKeyNotFoundException, VerificationException, ProjectException { + SigningKeyNotFoundException, VerificationException, ProjectException, SigningKeyEncryptionException { if(!settings.isDownloadAllowed()){ throw new DatasetException(RESTCodes.DatasetErrorCode.DOWNLOAD_NOT_ALLOWED, Level.FINEST); } diff --git a/hopsworks-ca/pom.xml b/hopsworks-ca/pom.xml index cbfd5ac656..50a21a5648 100644 --- a/hopsworks-ca/pom.xml +++ b/hopsworks-ca/pom.xml @@ -72,6 +72,12 @@ hopsworks-rest-utils ${project.version} + + io.hops.hopsworks + hopsworks-security + ${project.version} + provided + io.hops.hopsworks hopsworks-jwt diff --git a/hopsworks-ca/src/main/java/io/hops/hopsworks/ca/api/filter/AuthFilter.java b/hopsworks-ca/src/main/java/io/hops/hopsworks/ca/api/filter/AuthFilter.java index 5426b0ddea..854e4b5761 100644 --- a/hopsworks-ca/src/main/java/io/hops/hopsworks/ca/api/filter/AuthFilter.java +++ b/hopsworks-ca/src/main/java/io/hops/hopsworks/ca/api/filter/AuthFilter.java @@ -20,6 +20,7 @@ import com.auth0.jwt.interfaces.DecodedJWT; import io.hops.hopsworks.ca.api.exception.mapper.CAJsonResponse; import io.hops.hopsworks.ca.controllers.CAConf; +import io.hops.hopsworks.jwt.exception.SigningKeyEncryptionException; import io.hops.hopsworks.restutils.JsonResponse; import io.hops.hopsworks.restutils.RESTCodes; import io.hops.hopsworks.jwt.AlgorithmFactory; @@ -62,7 +63,7 @@ public class AuthFilter extends JWTFilter { private UriInfo uriInfo; @Override - public Algorithm getAlgorithm(DecodedJWT jwt) throws SigningKeyNotFoundException { + public Algorithm getAlgorithm(DecodedJWT jwt) throws SigningKeyNotFoundException, SigningKeyEncryptionException { return algorithmFactory.getAlgorithm(jwt); } diff --git a/hopsworks-common/pom.xml b/hopsworks-common/pom.xml index 16f64ded62..496a913b03 100644 --- a/hopsworks-common/pom.xml +++ b/hopsworks-common/pom.xml @@ -69,6 +69,13 @@ provided + + io.hops.hopsworks + hopsworks-security + ${project.version} + provided + + io.hops.hopsworks hopsworks-jwt diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/elastic/ElasticJWTController.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/elastic/ElasticJWTController.java index 968310a603..0a8b74234f 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/elastic/ElasticJWTController.java +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/elastic/ElasticJWTController.java @@ -15,6 +15,7 @@ */ package io.hops.hopsworks.common.elastic; +import io.hops.hopsworks.jwt.exception.SigningKeyEncryptionException; import io.hops.hopsworks.persistence.entity.project.Project; import io.hops.hopsworks.persistence.entity.project.team.ProjectRoleTypes; import io.hops.hopsworks.common.dao.project.team.ProjectTeamFacade; @@ -54,7 +55,7 @@ public String getSigningKeyForELK() throws ElasticException { SignatureAlgorithm alg = SignatureAlgorithm.valueOf(settings.getJWTSignatureAlg()); try { return jwtController.getSigningKeyForELK(alg); - } catch (NoSuchAlgorithmException e) { + } catch (NoSuchAlgorithmException | SigningKeyEncryptionException e) { throw new ElasticException(RESTCodes.ElasticErrorCode.SIGNING_KEY_ERROR, Level.SEVERE, "Failed to get elk signing key", e.getMessage(), e); @@ -98,7 +99,8 @@ private String createTokenForELK(String project, Optional projectInodeId, } return jwtController.createTokenForELK(project, settings.getJWTIssuer() , claims, expiresAt, alg); - } catch (DuplicateSigningKeyException | NoSuchAlgorithmException | SigningKeyNotFoundException e) { + } catch (DuplicateSigningKeyException | NoSuchAlgorithmException | SigningKeyNotFoundException | + SigningKeyEncryptionException e) { throw new ElasticException(RESTCodes.ElasticErrorCode.JWT_NOT_CREATED, Level.SEVERE, "Failed to create jwt token for elk", e.getMessage(), e); } diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/CertificateMaterializer.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/CertificateMaterializer.java index c90c1a32be..d3b7d65ab8 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/CertificateMaterializer.java +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/CertificateMaterializer.java @@ -51,6 +51,7 @@ import io.hops.hopsworks.common.util.HopsUtils; import io.hops.hopsworks.common.util.Settings; import io.hops.hopsworks.exceptions.CryptoPasswordNotFoundException; +import io.hops.hopsworks.security.password.MasterPasswordService; import org.apache.commons.collections.Bag; import org.apache.commons.collections.MapUtils; import org.apache.commons.collections.bag.HashBag; @@ -125,7 +126,7 @@ public class CertificateMaterializer { @EJB private UserFacade userFacade; @EJB - private CertificatesMgmService certificatesMgmService; + private MasterPasswordService masterPasswordService; @EJB private RemoteMaterialReferencesFacade remoteMaterialReferencesFacade; @EJB @@ -1206,7 +1207,7 @@ private char[] decryptMaterialPassword(String certificateIdentifier, String encr String userPassword = user.getPassword(); try { - String decryptedPassword = HopsUtils.decrypt(userPassword, encryptedPassword, certificatesMgmService + String decryptedPassword = HopsUtils.decrypt(userPassword, encryptedPassword, masterPasswordService .getMasterEncryptionPassword()); return decryptedPassword.toCharArray(); } catch (Exception ex) { diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/CertificatesController.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/CertificatesController.java index 5e493c6137..13c7914563 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/CertificatesController.java +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/CertificatesController.java @@ -49,6 +49,7 @@ import io.hops.hopsworks.exceptions.HopsSecurityException; import io.hops.hopsworks.restutils.RESTCodes; import io.hops.hopsworks.common.util.HopsUtils; +import io.hops.hopsworks.security.password.MasterPasswordService; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x500.X500NameBuilder; import org.bouncycastle.asn1.x500.style.BCStyle; @@ -114,6 +115,10 @@ public class CertificatesController { private CertsFacade certsFacade; @EJB private CertificatesMgmService certificatesMgmService; + @EJB + private MasterPasswordService masterPasswordService; + @EJB + private Settings settings; @Inject @Any private Instance certificateHandlers; @@ -163,7 +168,7 @@ public void init() { public Future generateCertificates(Project project, Users user) throws Exception { String userKeyPwd = HopsUtils.randomString(64); String encryptedKey = HopsUtils.encrypt(user.getPassword(), userKeyPwd, - certificatesMgmService.getMasterEncryptionPassword()); + masterPasswordService.getMasterEncryptionPassword()); Pair userKeystores = generateStores(project.getName() + Settings.HOPS_USERNAME_SEPARATOR + user.getUsername(), diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/CertificatesMgmService.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/CertificatesMgmService.java index a17ab84ab5..03af150e46 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/CertificatesMgmService.java +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/CertificatesMgmService.java @@ -38,227 +38,29 @@ */ package io.hops.hopsworks.common.security; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; -import io.hops.hopsworks.common.dao.certificates.CertsFacade; +import io.hops.hopsworks.common.dao.command.SystemCommandFacade; +import io.hops.hopsworks.common.dao.host.HostsFacade; import io.hops.hopsworks.persistence.entity.command.Operation; import io.hops.hopsworks.persistence.entity.command.SystemCommand; -import io.hops.hopsworks.common.dao.command.SystemCommandFacade; -import io.hops.hopsworks.common.dao.dela.certs.ClusterCertificateFacade; import io.hops.hopsworks.persistence.entity.host.Hosts; -import io.hops.hopsworks.common.dao.host.HostsFacade; -import io.hops.hopsworks.common.dao.user.UserFacade; -import io.hops.hopsworks.persistence.entity.user.Users; -import io.hops.hopsworks.common.message.MessageController; -import io.hops.hopsworks.common.util.Settings; -import io.hops.hopsworks.exceptions.EncryptionMasterPasswordException; -import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.io.FileUtils; -import javax.annotation.PostConstruct; -import javax.ejb.AccessTimeout; -import javax.ejb.Asynchronous; -import javax.ejb.ConcurrencyManagement; -import javax.ejb.ConcurrencyManagementType; import javax.ejb.EJB; -import javax.ejb.Lock; -import javax.ejb.LockType; import javax.ejb.Singleton; import javax.ejb.TransactionAttribute; import javax.ejb.TransactionAttributeType; -import javax.enterprise.inject.Any; -import javax.enterprise.inject.Instance; -import javax.inject.Inject; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.LinkOption; -import java.nio.file.attribute.PosixFileAttributeView; -import java.nio.file.attribute.PosixFilePermission; -import java.nio.file.attribute.PosixFilePermissions; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; import java.util.logging.Logger; @Singleton -@ConcurrencyManagement(ConcurrencyManagementType.CONTAINER) public class CertificatesMgmService { private final Logger LOG = Logger.getLogger(CertificatesMgmService.class.getName()); - @EJB - private Settings settings; - @EJB - private UserFacade userFacade; - @EJB - private CertsFacade certsFacade; - @EJB - private ClusterCertificateFacade clusterCertificateFacade; - @EJB - private MessageController messageController; @EJB private SystemCommandFacade systemCommandFacade; @EJB private HostsFacade hostsFacade; - @Inject - @Any - private Instance handlers; - public enum UPDATE_STATUS { - OK, - WORKING, - FAILED, - NOT_FOUND - } - - private File masterPasswordFile; - private final Map handlersResult = new HashMap<>(); - private Cache updateStatus; - private Random rand; - public CertificatesMgmService() { - - } - - @PostConstruct - public void init() { - masterPasswordFile = new File(settings.getHopsworksMasterEncPasswordFile()); - if (!masterPasswordFile.exists()) { - throw new IllegalStateException("Master encryption file does not exist"); - } - - try { - PosixFileAttributeView fileView = Files.getFileAttributeView(masterPasswordFile.toPath(), - PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS); - Set filePermissions = fileView.readAttributes().permissions(); - boolean ownerRead = filePermissions.contains(PosixFilePermission.OWNER_READ); - boolean ownerWrite = filePermissions.contains(PosixFilePermission - .OWNER_WRITE); - boolean ownerExecute = filePermissions.contains(PosixFilePermission - .OWNER_EXECUTE); - - boolean groupRead = filePermissions.contains(PosixFilePermission.GROUP_READ); - boolean groupWrite = filePermissions.contains(PosixFilePermission - .GROUP_WRITE); - boolean groupExecute = filePermissions.contains(PosixFilePermission - .GROUP_EXECUTE); - - boolean othersRead = filePermissions.contains(PosixFilePermission - .OTHERS_READ); - boolean othersWrite = filePermissions.contains(PosixFilePermission - .OTHERS_WRITE); - boolean othersExecute = filePermissions.contains(PosixFilePermission - .OTHERS_EXECUTE); - - // Permissions should be 700 - if ((ownerRead && ownerWrite && ownerExecute) - && (!groupRead && !groupWrite && !groupExecute) - && (!othersRead && !othersWrite && !othersExecute)) { - String owner = fileView.readAttributes().owner().getName(); - String group = fileView.readAttributes().group().getName(); - String permStr = PosixFilePermissions.toString(filePermissions); - LOG.log(Level.INFO, "Passed permissions check for file " + masterPasswordFile.getAbsolutePath() - + ". Owner: " + owner + " Group: " + group + " Permissions: " + permStr); - } else { - throw new IllegalStateException("Wrong permissions for file " + masterPasswordFile.getAbsolutePath() - + ", it should be 700"); - } - - updateStatus = CacheBuilder.newBuilder() - .maximumSize(100) - .expireAfterWrite(12L, TimeUnit.HOURS) - .build(); - rand = new Random(); - } catch (UnsupportedOperationException ex) { - LOG.log(Level.WARNING, "Associated filesystem is not POSIX compliant. " + - "Continue without checking the permissions of " + masterPasswordFile.getAbsolutePath() - + " This might be a security problem."); - } catch (IOException ex) { - throw new IllegalStateException("Error while getting POSIX permissions of " + masterPasswordFile - .getAbsolutePath()); - } - } - - @Lock(LockType.READ) - @AccessTimeout(value = 3, unit = TimeUnit.SECONDS) - public String getMasterEncryptionPassword() throws IOException { - return FileUtils.readFileToString(masterPasswordFile).trim(); - } - - /** - * Validates the provided password against the configured one - * @param providedPassword Password to validate - * @param userRequestedEmail User requested the password check - * @throws IOException - * @throws EncryptionMasterPasswordException - */ - @Lock(LockType.READ) - @AccessTimeout(value = 3, unit = TimeUnit.SECONDS) - public void checkPassword(String providedPassword, String userRequestedEmail) - throws IOException, EncryptionMasterPasswordException { - String sha = DigestUtils.sha256Hex(providedPassword); - if (!getMasterEncryptionPassword().equals(sha)) { - Users user = userFacade.findByEmail(userRequestedEmail); - String logMsg = "*** Attempt to change master encryption password with wrong credentials"; - if (user != null) { - LOG.log(Level.INFO, logMsg + " by user <" + user.getUsername() + ">"); - } else { - LOG.log(Level.INFO, logMsg); - } - throw new EncryptionMasterPasswordException("Provided password is incorrect"); - } - } - - public Integer initUpdateOperation() { - Integer operationId = rand.nextInt(); - updateStatus.put(operationId, UPDATE_STATUS.WORKING); - return operationId; - } - - public UPDATE_STATUS getOperationStatus(Integer operationId) { - UPDATE_STATUS status = updateStatus.getIfPresent(operationId); - return status != null ? status : UPDATE_STATUS.NOT_FOUND; - } - - /** - * Decrypt secrets using the old master password and encrypt them with the new - * Both for project specific and project generic certificates - * @param newMasterPasswd new master encryption password - * @param userRequested User requested password change - */ - @SuppressWarnings("unchecked") - @Asynchronous - @Lock(LockType.WRITE) - @AccessTimeout(value = 500) - public void resetMasterEncryptionPassword(Integer operationId, String newMasterPasswd, String userRequested) { - try { - String newDigest = DigestUtils.sha256Hex(newMasterPasswd); - callUpdateHandlers(newDigest); - updateMasterEncryptionPassword(newDigest); - StringBuilder successLog = gatherLogs(); - sendSuccessfulMessage(successLog, userRequested); - updateStatus.put(operationId, UPDATE_STATUS.OK); - LOG.log(Level.INFO, "Master encryption password changed!"); - } catch (EncryptionMasterPasswordException ex) { - String errorMsg = "*** Master encryption password update failed!!! Rolling back..."; - LOG.log(Level.SEVERE, errorMsg, ex); - updateStatus.put(operationId, UPDATE_STATUS.FAILED); - callRollbackHandlers(); - sendUnsuccessfulMessage(errorMsg + "\n" + ex.getMessage(), userRequested); - } catch (IOException ex) { - String errorMsg = "*** Failed to write new encryption password to file: " + masterPasswordFile.getAbsolutePath() - + ". Rolling back..."; - LOG.log(Level.SEVERE, errorMsg, ex); - updateStatus.put(operationId, UPDATE_STATUS.FAILED); - callRollbackHandlers(); - sendUnsuccessfulMessage(errorMsg + "\n" + ex.getMessage(), userRequested); - } finally { - handlersResult.clear(); - } } @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) @@ -269,52 +71,4 @@ public void issueServiceKeyRotationCommand() { systemCommandFacade.persist(rotateCommand); } } - - private void callUpdateHandlers(String newDigest) throws EncryptionMasterPasswordException, IOException { - for (MasterPasswordHandler handler : handlers) { - MasterPasswordChangeResult result = handler.perform(getMasterEncryptionPassword(), newDigest); - handlersResult.put(handler.getClass(), result); - if (result.getCause() != null) { - throw result.getCause(); - } - } - } - - private void callRollbackHandlers() { - for (MasterPasswordHandler handler : handlers) { - MasterPasswordChangeResult result = handlersResult.get(handler.getClass()); - if (result != null) { - handler.rollback(result); - } - } - } - - private StringBuilder gatherLogs() { - StringBuilder successLog = new StringBuilder(); - for (MasterPasswordChangeResult result : handlersResult.values()) { - if (result.getSuccessLog() != null) { - successLog.append(result.getSuccessLog()); - successLog.append("\n\n"); - } - } - return successLog; - } - - private void updateMasterEncryptionPassword(String newPassword) throws IOException { - FileUtils.writeStringToFile(masterPasswordFile, newPassword); - } - - private void sendSuccessfulMessage(StringBuilder successLog, String userRequested) { - sendInbox(successLog.toString(), "Changed successfully", userRequested); - } - - private void sendUnsuccessfulMessage(String message, String userRequested) { - sendInbox(message, "Change failed!", userRequested); - } - - private void sendInbox(String message, String preview, String userRequested) { - Users to = userFacade.findByEmail(userRequested); - Users from = userFacade.findByEmail(settings.getAdminEmail()); - messageController.send(to, from, "Master encryption password changed", preview, message, ""); - } } diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/DelaCertsMasterPasswordHandler.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/DelaCertsMasterPasswordHandler.java index fab2f1c7e2..c1a6b7fbae 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/DelaCertsMasterPasswordHandler.java +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/DelaCertsMasterPasswordHandler.java @@ -42,6 +42,8 @@ import io.hops.hopsworks.common.dao.dela.certs.ClusterCertificateFacade; import io.hops.hopsworks.common.util.Settings; import io.hops.hopsworks.exceptions.EncryptionMasterPasswordException; +import io.hops.hopsworks.security.password.MasterPasswordChangeResult; +import io.hops.hopsworks.security.password.MasterPasswordHandler; import javax.ejb.EJB; import javax.ejb.Stateless; diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/PSUserCertsMasterPasswordHandler.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/PSUserCertsMasterPasswordHandler.java index 539d6e0546..4371cff82a 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/PSUserCertsMasterPasswordHandler.java +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/PSUserCertsMasterPasswordHandler.java @@ -44,6 +44,8 @@ import io.hops.hopsworks.persistence.entity.user.Users; import io.hops.hopsworks.common.hdfs.HdfsUsersController; import io.hops.hopsworks.exceptions.EncryptionMasterPasswordException; +import io.hops.hopsworks.security.password.MasterPasswordChangeResult; +import io.hops.hopsworks.security.password.MasterPasswordHandler; import javax.ejb.EJB; import javax.ejb.Stateless; diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/secrets/SecretsController.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/secrets/SecretsController.java index dc06d5268f..2add2a2ead 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/secrets/SecretsController.java +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/secrets/SecretsController.java @@ -27,15 +27,14 @@ import io.hops.hopsworks.common.dao.user.security.secrets.SecretPlaintext; import io.hops.hopsworks.common.dao.user.security.secrets.SecretsFacade; import io.hops.hopsworks.persistence.entity.user.security.secrets.VisibilityType; -import io.hops.hopsworks.common.project.ProjectController; -import io.hops.hopsworks.common.security.CertificatesMgmService; -import io.hops.hopsworks.common.security.SymmetricEncryptionDescriptor; -import io.hops.hopsworks.common.security.SymmetricEncryptionService; import io.hops.hopsworks.common.util.DateUtils; import io.hops.hopsworks.exceptions.ProjectException; import io.hops.hopsworks.exceptions.ServiceException; import io.hops.hopsworks.exceptions.UserException; import io.hops.hopsworks.restutils.RESTCodes; +import io.hops.hopsworks.security.encryption.SymmetricEncryptionDescriptor; +import io.hops.hopsworks.security.encryption.SymmetricEncryptionService; +import io.hops.hopsworks.security.password.MasterPasswordService; import javax.ejb.EJB; import javax.ejb.Stateless; @@ -64,12 +63,10 @@ public class SecretsController { @EJB private SymmetricEncryptionService symmetricEncryptionService; @EJB - private CertificatesMgmService certificatesMgmService; + private MasterPasswordService masterPasswordService; @EJB private UserFacade userFacade; @EJB - private ProjectController projectController; - @EJB private ProjectFacade projectFacade; /** @@ -299,7 +296,7 @@ private SecretPlaintext constructSecretView(Users user, Secret ciphered) { */ private SecretPlaintext decrypt(Users user, Secret ciphered) throws IOException, GeneralSecurityException { - String password = certificatesMgmService.getMasterEncryptionPassword(); + String password = masterPasswordService.getMasterEncryptionPassword(); // [salt(64),iv(12),payload)] byte[][] split = symmetricEncryptionService.splitPayloadFromCryptoPrimitives(ciphered.getSecret()); @@ -329,7 +326,7 @@ private SecretPlaintext decrypt(Users user, Secret ciphered) * @throws GeneralSecurityException */ private byte[] encryptSecret(String secret) throws IOException, GeneralSecurityException { - String password = certificatesMgmService.getMasterEncryptionPassword(); + String password = masterPasswordService.getMasterEncryptionPassword(); SymmetricEncryptionDescriptor descriptor = new SymmetricEncryptionDescriptor.Builder() .setInput(string2bytes(secret)) .setPassword(password) diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/secrets/SecretsPasswordHandler.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/secrets/SecretsPasswordHandler.java index 175771b22b..de50f4d02e 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/secrets/SecretsPasswordHandler.java +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/secrets/SecretsPasswordHandler.java @@ -16,14 +16,14 @@ package io.hops.hopsworks.common.security.secrets; -import io.hops.hopsworks.persistence.entity.user.security.secrets.Secret; -import io.hops.hopsworks.persistence.entity.user.security.secrets.SecretId; import io.hops.hopsworks.common.dao.user.security.secrets.SecretsFacade; -import io.hops.hopsworks.common.security.MasterPasswordChangeResult; -import io.hops.hopsworks.common.security.MasterPasswordHandler; -import io.hops.hopsworks.common.security.SymmetricEncryptionDescriptor; -import io.hops.hopsworks.common.security.SymmetricEncryptionService; import io.hops.hopsworks.exceptions.EncryptionMasterPasswordException; +import io.hops.hopsworks.persistence.entity.user.security.secrets.Secret; +import io.hops.hopsworks.persistence.entity.user.security.secrets.SecretId; +import io.hops.hopsworks.security.encryption.SymmetricEncryptionDescriptor; +import io.hops.hopsworks.security.encryption.SymmetricEncryptionService; +import io.hops.hopsworks.security.password.MasterPasswordChangeResult; +import io.hops.hopsworks.security.password.MasterPasswordHandler; import javax.ejb.EJB; import javax.ejb.Stateless; diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/user/AuthController.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/user/AuthController.java index 9c575d0d86..bc5c2857a3 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/user/AuthController.java +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/user/AuthController.java @@ -39,28 +39,28 @@ package io.hops.hopsworks.common.user; import io.hops.hopsworks.common.dao.certificates.CertsFacade; -import io.hops.hopsworks.persistence.entity.certificates.UserCerts; -import io.hops.hopsworks.persistence.entity.project.Project; import io.hops.hopsworks.common.dao.project.ProjectFacade; -import io.hops.hopsworks.persistence.entity.user.BbcGroup; import io.hops.hopsworks.common.dao.user.BbcGroupFacade; import io.hops.hopsworks.common.dao.user.UserFacade; -import io.hops.hopsworks.persistence.entity.user.Users; import io.hops.hopsworks.common.dao.user.security.audit.AccountAuditFacade; -import io.hops.hopsworks.persistence.entity.user.security.ua.SecurityQuestion; -import io.hops.hopsworks.persistence.entity.user.security.ua.UserAccountStatus; -import io.hops.hopsworks.persistence.entity.user.security.ua.UserAccountType; import io.hops.hopsworks.common.dao.user.security.ua.UserAccountsEmailMessages; import io.hops.hopsworks.common.security.utils.Secret; import io.hops.hopsworks.common.security.utils.SecurityUtils; -import io.hops.hopsworks.common.util.HttpUtil; -import io.hops.hopsworks.persistence.entity.user.security.ua.ValidationKeyType; -import io.hops.hopsworks.restutils.RESTCodes; -import io.hops.hopsworks.exceptions.UserException; -import io.hops.hopsworks.common.security.CertificatesMgmService; import io.hops.hopsworks.common.util.EmailBean; import io.hops.hopsworks.common.util.HopsUtils; +import io.hops.hopsworks.common.util.HttpUtil; import io.hops.hopsworks.common.util.Settings; +import io.hops.hopsworks.exceptions.UserException; +import io.hops.hopsworks.persistence.entity.certificates.UserCerts; +import io.hops.hopsworks.persistence.entity.project.Project; +import io.hops.hopsworks.persistence.entity.user.BbcGroup; +import io.hops.hopsworks.persistence.entity.user.Users; +import io.hops.hopsworks.persistence.entity.user.security.ua.SecurityQuestion; +import io.hops.hopsworks.persistence.entity.user.security.ua.UserAccountStatus; +import io.hops.hopsworks.persistence.entity.user.security.ua.UserAccountType; +import io.hops.hopsworks.persistence.entity.user.security.ua.ValidationKeyType; +import io.hops.hopsworks.restutils.RESTCodes; +import io.hops.hopsworks.security.password.MasterPasswordService; import javax.ejb.EJB; import javax.ejb.EJBException; @@ -98,7 +98,7 @@ public class AuthController { @EJB private ProjectFacade projectFacade; @EJB - private CertificatesMgmService certificatesMgmService; + private MasterPasswordService masterPasswordService; @EJB private SecurityUtils securityUtils; @EJB @@ -510,7 +510,7 @@ private void resetProjectCertPassword(Users p, String oldPass) { try { for (Project project : projects) { UserCerts userCert = userCertsFacade.findUserCert(project.getName(), p.getUsername()); - String masterEncryptionPassword = certificatesMgmService.getMasterEncryptionPassword(); + String masterEncryptionPassword = masterPasswordService.getMasterEncryptionPassword(); String certPassword = HopsUtils.decrypt(oldPass, userCert.getUserKeyPwd(), masterEncryptionPassword); //Encrypt it with new password and store it in the db String newSecret = HopsUtils.encrypt(p.getPassword(), certPassword, masterEncryptionPassword); diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/util/Settings.java b/hopsworks-common/src/main/java/io/hops/hopsworks/common/util/Settings.java index afdd1364db..c05bef5c28 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/util/Settings.java +++ b/hopsworks-common/src/main/java/io/hops/hopsworks/common/util/Settings.java @@ -54,6 +54,7 @@ import io.hops.hopsworks.exceptions.ProvenanceException; import io.hops.hopsworks.persistence.entity.util.VariablesVisibility; import io.hops.hopsworks.restutils.RESTLogLevel; +import io.hops.hopsworks.security.util.SecuritySettings; import org.apache.commons.codec.digest.DigestUtils; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; @@ -104,6 +105,8 @@ public class Settings implements Serializable { private ProjectUtils projectUtils; @EJB private OSProcessExecutor osProcessExecutor; + @EJB + private SecuritySettings securitySettings; @PersistenceContext(unitName = "kthfsPU") private EntityManager em; @@ -728,6 +731,7 @@ private void checkCache() { public synchronized void refreshCache() { cached = false; populateCache(); + securitySettings.refreshCache(); } public synchronized void updateVariable(String variableName, String variableValue, VariablesVisibility visibility) { diff --git a/hopsworks-dela/pom.xml b/hopsworks-dela/pom.xml index 084a465dc7..4c1a2f1c4c 100644 --- a/hopsworks-dela/pom.xml +++ b/hopsworks-dela/pom.xml @@ -73,6 +73,20 @@ io.hops.hopsworks hopsworks-persistence ${project.version} + + + + * + * + + + + + + io.hops.hopsworks + hopsworks-security + ${project.version} + provided diff --git a/hopsworks-dela/src/main/java/io/hops/hopsworks/dela/DelaSetupWorker.java b/hopsworks-dela/src/main/java/io/hops/hopsworks/dela/DelaSetupWorker.java index e7a2fd5788..e3c19fd7e6 100644 --- a/hopsworks-dela/src/main/java/io/hops/hopsworks/dela/DelaSetupWorker.java +++ b/hopsworks-dela/src/main/java/io/hops/hopsworks/dela/DelaSetupWorker.java @@ -41,23 +41,19 @@ import com.google.gson.Gson; import io.hops.hopsworks.common.dao.dela.certs.ClusterCertificateFacade; import io.hops.hopsworks.common.dela.AddressJSON; -import io.hops.hopsworks.restutils.RESTCodes; -import io.hops.hopsworks.common.security.CertificatesMgmService; import io.hops.hopsworks.common.util.OSProcessExecutor; import io.hops.hopsworks.common.util.Settings; import io.hops.hopsworks.dela.dto.hopssite.ClusterServiceDTO; -import io.hops.hopsworks.exceptions.DelaException; import io.hops.hopsworks.dela.hopssite.HopssiteController; +import io.hops.hopsworks.exceptions.DelaException; +import io.hops.hopsworks.restutils.RESTCodes; +import io.hops.hopsworks.security.password.MasterPasswordService; import io.hops.hopsworks.util.CertificateHelper; import io.hops.hopsworks.util.SettingsHelper; -import java.net.MalformedURLException; -import java.net.URISyntaxException; -import java.net.URL; -import java.security.KeyStore; -import java.util.List; -import java.util.Optional; -import java.util.logging.Level; -import java.util.logging.Logger; +import org.apache.commons.codec.digest.DigestUtils; +import org.javatuples.Pair; +import org.javatuples.Triplet; + import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.annotation.Resource; @@ -69,9 +65,14 @@ import javax.ejb.TimerService; import javax.ejb.TransactionAttribute; import javax.ejb.TransactionAttributeType; -import org.apache.commons.codec.digest.DigestUtils; -import org.javatuples.Pair; -import org.javatuples.Triplet; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.security.KeyStore; +import java.util.List; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; @Startup @Singleton @@ -92,7 +93,7 @@ public class DelaSetupWorker { @EJB private TransferDelaController delaCtrl; @EJB - private CertificatesMgmService certificatesMgmService; + private MasterPasswordService masterPasswordService; @EJB private OSProcessExecutor osProcessExecutor; @@ -174,7 +175,7 @@ private void setup(Timer timer) { if (clusterName.isPresent()) { Optional> keystoreAux = CertificateHelper.loadKeystoreFromDB(masterPswd.get(), clusterName.get(), clusterCertFacade, - certificatesMgmService); + masterPasswordService); if (keystoreAux.isPresent()) { setupComplete(keystoreAux.get(), timer); return; @@ -182,7 +183,7 @@ private void setup(Timer timer) { } Optional> keystoreAux - = CertificateHelper.loadKeystoreFromFile(masterPswd.get(), settings, clusterCertFacade, certificatesMgmService, + = CertificateHelper.loadKeystoreFromFile(masterPswd.get(), settings, clusterCertFacade, masterPasswordService, osProcessExecutor); if (keystoreAux.isPresent()) { setupComplete(keystoreAux.get(), timer); diff --git a/hopsworks-dela/src/main/java/io/hops/hopsworks/util/CertificateHelper.java b/hopsworks-dela/src/main/java/io/hops/hopsworks/util/CertificateHelper.java index 1f1afbcafe..297d3b0db4 100644 --- a/hopsworks-dela/src/main/java/io/hops/hopsworks/util/CertificateHelper.java +++ b/hopsworks-dela/src/main/java/io/hops/hopsworks/util/CertificateHelper.java @@ -41,12 +41,12 @@ import com.google.common.io.ByteStreams; import io.hops.hopsworks.common.dao.dela.certs.ClusterCertificateFacade; -import io.hops.hopsworks.common.security.CertificatesMgmService; import io.hops.hopsworks.common.util.HopsUtils; import io.hops.hopsworks.common.util.LocalhostServices; import io.hops.hopsworks.common.util.OSProcessExecutor; import io.hops.hopsworks.common.util.Settings; import io.hops.hopsworks.persistence.entity.dela.certs.ClusterCertificate; +import io.hops.hopsworks.security.password.MasterPasswordService; import org.apache.commons.io.FileUtils; import org.javatuples.Triplet; @@ -71,7 +71,7 @@ public class CertificateHelper { private final static Logger LOG = Logger.getLogger(CertificateHelper.class.getName()); public static Optional> loadKeystoreFromFile(String masterPswd, Settings settings, - ClusterCertificateFacade certFacade, CertificatesMgmService certificatesMgmService, + ClusterCertificateFacade certFacade, MasterPasswordService masterPasswordService, OSProcessExecutor osProcessExecutor) { String certPath = settings.getHopsSiteCert(); String intermediateCertPath = settings.getHopsSiteIntermediateCert(); @@ -80,7 +80,7 @@ public static Optional> loadKeystoreFromFile try { String certPswd = HopsUtils.randomString(64); String encryptedCertPswd = HopsUtils.encrypt(masterPswd, certPswd, - certificatesMgmService.getMasterEncryptionPassword()); + masterPasswordService.getMasterEncryptionPassword()); File certFile = readFile(certPath); File intermediateCertFile = readFile(intermediateCertPath); String clusterName = getClusterName(certFile); @@ -111,14 +111,14 @@ public static Optional> loadKeystoreFromFile } public static Optional> loadKeystoreFromDB(String masterPswd, String clusterName, - ClusterCertificateFacade certFacade, CertificatesMgmService certificatesMgmService) { + ClusterCertificateFacade certFacade, MasterPasswordService masterPasswordService) { try { Optional cert = certFacade.getClusterCert(clusterName); if (!cert.isPresent()) { return Optional.empty(); } String certPswd = HopsUtils.decrypt(masterPswd, cert.get().getCertificatePassword(), - certificatesMgmService.getMasterEncryptionPassword()); + masterPasswordService.getMasterEncryptionPassword()); KeyStore keystore, truststore; try (ByteArrayInputStream keystoreIS = new ByteArrayInputStream(cert.get().getClusterKey()); ByteArrayInputStream truststoreIS = new ByteArrayInputStream(cert.get().getClusterCert())) { diff --git a/hopsworks-ear/pom.xml b/hopsworks-ear/pom.xml index f6812bffec..57b8b836d8 100644 --- a/hopsworks-ear/pom.xml +++ b/hopsworks-ear/pom.xml @@ -100,6 +100,12 @@ ${project.version} ejb + + io.hops.hopsworks + hopsworks-security + ${project.version} + ejb + io.hops.hopsworks hopsworks-kmon diff --git a/hopsworks-jwt/pom.xml b/hopsworks-jwt/pom.xml index 60b722ab9f..dcf5eac5bb 100644 --- a/hopsworks-jwt/pom.xml +++ b/hopsworks-jwt/pom.xml @@ -46,6 +46,18 @@ hopsworks-persistence ${project.version} + + io.hops.hopsworks + hopsworks-rest-utils + ${project.version} + provided + + + io.hops.hopsworks + hopsworks-security + ${project.version} + provided + com.auth0 java-jwt @@ -62,6 +74,13 @@ commons-lang3 3.8.1 + + + org.mockito + mockito-all + 1.10.19 + test + diff --git a/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/AlgorithmFactory.java b/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/AlgorithmFactory.java index 3ef5fada67..fa2b54ece0 100644 --- a/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/AlgorithmFactory.java +++ b/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/AlgorithmFactory.java @@ -19,38 +19,57 @@ import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.interfaces.ECDSAKeyProvider; import com.auth0.jwt.interfaces.RSAKeyProvider; -import io.hops.hopsworks.persistence.entity.jwt.JwtSigningKey; -import io.hops.hopsworks.jwt.dao.JwtSigningKeyFacade; +import io.hops.hopsworks.jwt.exception.SigningKeyEncryptionException; import io.hops.hopsworks.jwt.exception.SigningKeyNotFoundException; + +import javax.ejb.EJB; +import javax.ejb.Stateless; +import javax.ws.rs.NotSupportedException; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; -import java.util.Base64; -import javax.ejb.EJB; -import javax.ejb.Stateless; -import javax.ws.rs.NotSupportedException; @Stateless public class AlgorithmFactory { - + @EJB - private JwtSigningKeyFacade jwtSigningKeyFacade; + private SigningKeyEncryptionService signingKeyEncryptionService; - public Algorithm getAlgorithm(DecodedJWT jwt) throws SigningKeyNotFoundException { + public Algorithm getAlgorithm(DecodedJWT jwt) throws SigningKeyNotFoundException, SigningKeyEncryptionException { return getAlgorithm(jwt.getAlgorithm(), jwt.getKeyId()); } - public Algorithm getAlgorithm(JsonWebToken jwt) throws SigningKeyNotFoundException { + public Algorithm getAlgorithm(JsonWebToken jwt) throws SigningKeyNotFoundException, SigningKeyEncryptionException { return getAlgorithm(jwt.getAlgorithm(), jwt.getKeyId()); } - public Algorithm getAlgorithm(String algorithm, String keyId) throws SigningKeyNotFoundException { + public Algorithm getAlgorithm(String algorithm, String keyId) throws SigningKeyNotFoundException, + SigningKeyEncryptionException { SignatureAlgorithm alg = SignatureAlgorithm.valueOf(algorithm); return getAlgorithm(alg, keyId); } - - public Algorithm getAlgorithm(SignatureAlgorithm algorithm, String keyId) throws SigningKeyNotFoundException { + + public Algorithm getAlgorithm(String algorithm, byte[] key) { + SignatureAlgorithm alg = SignatureAlgorithm.valueOf(algorithm); + return getHSAlgorithm(alg, key); + } + + public Algorithm getHSAlgorithm(SignatureAlgorithm algorithm, byte[] key) { + switch (algorithm) { + case HS256: + return Algorithm.HMAC256(key); + case HS384: + return Algorithm.HMAC384(key); + case HS512: + return Algorithm.HMAC512(key); + default: + throw new NotSupportedException("Algorithm not supported."); + } + } + + public Algorithm getAlgorithm(SignatureAlgorithm algorithm, String keyId) throws SigningKeyNotFoundException, + SigningKeyEncryptionException { switch (algorithm) { case ES256: return getES256Algorithm(keyId); @@ -90,29 +109,29 @@ private Algorithm getES512Algorithm(String keyId) { return Algorithm.ECDSA512(keyProvider); } - private byte[] getSigningKey(String keyId) throws SigningKeyNotFoundException { + private byte[] getSigningKey(String keyId) throws SigningKeyNotFoundException, SigningKeyEncryptionException { Integer id; try { id = Integer.parseInt(keyId); } catch (NumberFormatException e) { throw new SigningKeyNotFoundException("Signing key not found. The key id should be integer."); } - JwtSigningKey signingKey = jwtSigningKeyFacade.find(id); + DecryptedSigningKey signingKey = signingKeyEncryptionService.getSigningKey(id); if (signingKey == null) { throw new SigningKeyNotFoundException("Signing key not found."); } - return Base64.getDecoder().decode(signingKey.getSecret()); + return signingKey.getDecryptedSecret(); } - private Algorithm getHS256Algorithm(String keyId) throws SigningKeyNotFoundException { + private Algorithm getHS256Algorithm(String keyId) throws SigningKeyNotFoundException, SigningKeyEncryptionException { return Algorithm.HMAC256(getSigningKey(keyId)); } - private Algorithm getHS384Algorithm(String keyId) throws SigningKeyNotFoundException { + private Algorithm getHS384Algorithm(String keyId) throws SigningKeyNotFoundException, SigningKeyEncryptionException { return Algorithm.HMAC384(getSigningKey(keyId)); } - private Algorithm getHS512Algorithm(String keyId) throws SigningKeyNotFoundException { + private Algorithm getHS512Algorithm(String keyId) throws SigningKeyNotFoundException, SigningKeyEncryptionException { return Algorithm.HMAC512(getSigningKey(keyId)); } diff --git a/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/DecryptedSigningKey.java b/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/DecryptedSigningKey.java new file mode 100644 index 0000000000..2325aaf99c --- /dev/null +++ b/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/DecryptedSigningKey.java @@ -0,0 +1,71 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2020, Logical Clocks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.jwt; + +import io.hops.hopsworks.persistence.entity.jwt.JwtSigningKey; + +import java.util.Base64; +import java.util.Date; + +public class DecryptedSigningKey { + private Integer id; + private String name; + private Date createdOn; + private byte[] decryptedSecret; + + public DecryptedSigningKey(JwtSigningKey jwtSigningKey, byte[] decryptedSecret) { + this.id = jwtSigningKey.getId(); + this.name = jwtSigningKey.getName(); + this.createdOn = jwtSigningKey.getCreatedOn(); + this.decryptedSecret = decryptedSecret; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public byte[] getDecryptedSecret() { + return decryptedSecret; + } + + public void setDecryptedSecret(byte[] decryptedSecret) { + this.decryptedSecret = decryptedSecret; + } + + public String getDecryptedSecretBase64Encoded() { + return Base64.getEncoder().encodeToString(decryptedSecret); + } +} diff --git a/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/JWTController.java b/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/JWTController.java index 3167633125..bdb62242d7 100644 --- a/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/JWTController.java +++ b/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/JWTController.java @@ -49,6 +49,7 @@ import io.hops.hopsworks.jwt.exception.InvalidationException; import io.hops.hopsworks.jwt.exception.JWTException; import io.hops.hopsworks.jwt.exception.NotRenewableException; +import io.hops.hopsworks.jwt.exception.SigningKeyEncryptionException; import io.hops.hopsworks.jwt.exception.SigningKeyNotFoundException; import io.hops.hopsworks.jwt.exception.VerificationException; @@ -72,6 +73,8 @@ public class JWTController { private AlgorithmFactory algorithmFactory; @EJB private JwtSigningKeyFacade jwtSigningKeyFacade; + @EJB + private SigningKeyEncryptionService signingKeyEncryptionService; /** * Create a jwt. @@ -80,7 +83,8 @@ public class JWTController { * @return three Base64-URL strings separated by dots * @throws io.hops.hopsworks.jwt.exception.SigningKeyNotFoundException */ - public String createToken(JsonWebToken jwt, Map claims) throws SigningKeyNotFoundException { + public String createToken(JsonWebToken jwt, Map claims) throws SigningKeyNotFoundException, + SigningKeyEncryptionException { return createToken(jwt.getKeyId(), jwt.getIssuer(), jwt.getAudience().toArray(new String[0]), jwt. getExpiresAt(), jwt.getNotBefore(), jwt.getSubject(), claims, jwt.getAlgorithm()); } @@ -100,8 +104,8 @@ public String createToken(JsonWebToken jwt, Map claims) throws S * @throws io.hops.hopsworks.jwt.exception.SigningKeyNotFoundException */ public String createToken(String keyId, String issuer, String[] audience, Date expiresAt, Date notBefore, - String subject, Map claims, SignatureAlgorithm algorithm) throws - SigningKeyNotFoundException { + String subject, Map claims, SignatureAlgorithm algorithm) throws SigningKeyNotFoundException, + SigningKeyEncryptionException { JWTCreator.Builder jwtBuilder = JWT.create() .withKeyId(keyId) .withIssuer(issuer) @@ -170,16 +174,17 @@ private JWTCreator.Builder addClaims(JWTCreator.Builder jwtCreator, Map claims, SignatureAlgorithm algorithm) - throws NoSuchAlgorithmException, SigningKeyNotFoundException, DuplicateSigningKeyException { - JwtSigningKey signingKey; + Date notBefore, String subject, Map claims, SignatureAlgorithm algorithm) + throws NoSuchAlgorithmException, SigningKeyNotFoundException, DuplicateSigningKeyException, + SigningKeyEncryptionException { + Integer id; if (createNewKey) { - signingKey = createNewSigningKey(keyName, algorithm); + id = createNewSigningKey(keyName, algorithm).getId(); } else { - signingKey = getOrCreateSigningKey(keyName, algorithm); + id = getOrCreateSigningKey(keyName, algorithm).getId(); } - return createToken(signingKey.getId().toString(), issuer, audience, expiresAt, notBefore, subject, + return createToken(id.toString(), issuer, audience, expiresAt, notBefore, subject, claims, algorithm); } @@ -265,7 +270,8 @@ public DecodedJWT decodeToken(String token) { * @throws SigningKeyNotFoundException * @throws VerificationException */ - public DecodedJWT verifyToken(String token, String issuer) throws SigningKeyNotFoundException, VerificationException { + public DecodedJWT verifyToken(String token, String issuer) throws SigningKeyNotFoundException, VerificationException, + SigningKeyEncryptionException { DecodedJWT jwt = JWT.decode(token); issuer = issuer == null || issuer.isEmpty() ? jwt.getIssuer() : issuer; int expLeeway = getExpLeewayClaim(jwt); @@ -287,7 +293,7 @@ public DecodedJWT verifyToken(String token, String issuer) throws SigningKeyNotF * @throws InvalidationException */ public DecodedJWT verifyOneTimeToken(String token, String issuer) throws SigningKeyNotFoundException, - VerificationException, InvalidationException { + VerificationException, InvalidationException, SigningKeyEncryptionException { DecodedJWT jwt = verifyToken(token, issuer); invalidateJWT(jwt.getId(), jwt.getExpiresAt(), getExpLeewayClaim(jwt)); return jwt; @@ -305,7 +311,7 @@ public DecodedJWT verifyOneTimeToken(String token, String issuer) throws Signing * @throws VerificationException */ public DecodedJWT verifyToken(String token, String issuer, Set audiences, Set roles) throws - SigningKeyNotFoundException, VerificationException { + SigningKeyNotFoundException, VerificationException, SigningKeyEncryptionException { JsonWebToken jwt = new JsonWebToken(JWT.decode(token)); issuer = issuer == null || issuer.isEmpty() ? jwt.getIssuer() : issuer; DecodedJWT djwt = verifyToken(token, issuer, jwt.getExpLeeway(), algorithmFactory.getAlgorithm(jwt)); @@ -380,7 +386,7 @@ private boolean isTokenInvalidated(String id) { * @throws InvalidationException */ public String autoRenewToken(String token) throws SigningKeyNotFoundException, - NotRenewableException, InvalidationException { + NotRenewableException, InvalidationException, SigningKeyEncryptionException { DecodedJWT jwt = verifyTokenForRenewal(token); boolean isRenewable = getRenewableClaim(jwt); if (!isRenewable) { @@ -407,9 +413,8 @@ public String autoRenewToken(String token) throws SigningKeyNotFoundException, return renewedToken; } - public String renewToken(String token, Date newExp, Date notBefore, boolean invalidate, - Map claims) - throws SigningKeyNotFoundException, NotRenewableException, InvalidationException { + public String renewToken(String token, Date newExp, Date notBefore, boolean invalidate, Map claims) + throws SigningKeyNotFoundException, NotRenewableException, InvalidationException, SigningKeyEncryptionException { return renewToken(token, newExp, notBefore, invalidate, claims, false); } @@ -427,8 +432,8 @@ public String renewToken(String token, Date newExp, Date notBefore, boolean inva * @throws InvalidationException */ public String renewToken(String token, Date newExp, Date notBefore, boolean invalidate, - Map claims, boolean force) - throws SigningKeyNotFoundException, NotRenewableException, InvalidationException { + Map claims, boolean force) throws SigningKeyNotFoundException, NotRenewableException, + InvalidationException, SigningKeyEncryptionException { DecodedJWT jwt = verifyTokenForRenewal(token); if (!force) { Date currentTime = new Date(); @@ -502,7 +507,7 @@ public void invalidateServiceToken(String serviceToken2invalidate, String defaul Claim signingKeyID = serviceJWT2invalidate.getClaim(Constants.SERVICE_JWT_RENEWAL_KEY_ID); if (signingKeyID != null && !signingKeyID.isNull()) { // Do not use Claim.asInt, it returns null - JwtSigningKey signingKey = findSigningKeyById(Integer.parseInt(signingKeyID.asString())); + JwtSigningKey signingKey = jwtSigningKeyFacade.find(Integer.parseInt(signingKeyID.asString())); if (signingKey != null && defaultJWTSigningKeyName != null) { if (!defaultJWTSigningKeyName.equals(signingKey.getName()) && !ONE_TIME_JWT_SIGNING_KEY_NAME.equals(signingKey.getName())) { @@ -518,8 +523,8 @@ public String getSignKeyID(String token) { } public String[] generateOneTimeTokens4ServiceJWTRenewal(JsonWebToken jwtSpecs, Map claims, - String defaultJWTSigningKeyName) - throws NoSuchAlgorithmException, SigningKeyNotFoundException { + String defaultJWTSigningKeyName) throws NoSuchAlgorithmException, SigningKeyNotFoundException, + SigningKeyEncryptionException { String[] renewalTokens = new String[5]; SignatureAlgorithm algorithm = SignatureAlgorithm.valueOf(Constants.ONE_TIME_JWT_SIGNATURE_ALGORITHM); String[] audienceArray = jwtSpecs.getAudience().toArray(new String[1]); @@ -591,7 +596,8 @@ public Map addDefaultClaimsIfMissing(Map userCla return userClaims; } - private DecodedJWT verifyTokenForRenewal(String token) throws SigningKeyNotFoundException, NotRenewableException { + private DecodedJWT verifyTokenForRenewal(String token) throws SigningKeyNotFoundException, NotRenewableException, + SigningKeyEncryptionException { DecodedJWT jwt; try { jwt = verifyToken(token, null); @@ -659,8 +665,9 @@ public String generateJti() { * @return * @throws NoSuchAlgorithmException */ - public JwtSigningKey getOrCreateSigningKey(String keyName, SignatureAlgorithm alg) throws NoSuchAlgorithmException { - return jwtSigningKeyFacade.getOrCreateSigningKey(keyName, alg); + public DecryptedSigningKey getOrCreateSigningKey(String keyName, SignatureAlgorithm alg) + throws NoSuchAlgorithmException, SigningKeyEncryptionException { + return signingKeyEncryptionService.getOrCreateSigningKey(keyName, alg); } /** @@ -672,9 +679,9 @@ public JwtSigningKey getOrCreateSigningKey(String keyName, SignatureAlgorithm al * @throws NoSuchAlgorithmException * @throws io.hops.hopsworks.jwt.exception.DuplicateSigningKeyException */ - public JwtSigningKey createNewSigningKey(String keyName, SignatureAlgorithm alg) throws NoSuchAlgorithmException, - DuplicateSigningKeyException { - return jwtSigningKeyFacade.createNewSigningKey(keyName, alg); + public DecryptedSigningKey createNewSigningKey(String keyName, SignatureAlgorithm alg) + throws NoSuchAlgorithmException, DuplicateSigningKeyException, SigningKeyEncryptionException { + return signingKeyEncryptionService.createSigningKey(keyName, alg); } /** @@ -683,11 +690,9 @@ public JwtSigningKey createNewSigningKey(String keyName, SignatureAlgorithm alg) * @param keyName a unique name given to signing key when created. */ public void deleteSigningKey(String keyName) { - jwtSigningKeyFacade.remove(keyName); - } - - public JwtSigningKey findSigningKeyById(Integer id) { - return jwtSigningKeyFacade.find(id); + JwtSigningKey jsk = jwtSigningKeyFacade.findByName(keyName); + signingKeyEncryptionService.removeFromCache(jsk); + jwtSigningKeyFacade.remove(jsk); } /** @@ -716,8 +721,9 @@ public boolean markOldSigningKeys() { removeMarkedKeys();//remove if there is an old marked but not deleted. jwtSigningKeyFacade.renameSigningKey(jwtSigningKey, Constants.OLD_ONE_TIME_JWT_SIGNING_KEY_NAME); try { - jwtSigningKeyFacade.getOrCreateSigningKey(Constants.ONE_TIME_JWT_SIGNING_KEY_NAME, SignatureAlgorithm.HS256); - } catch (NoSuchAlgorithmException ex) { + signingKeyEncryptionService.getOrCreateSigningKey(Constants.ONE_TIME_JWT_SIGNING_KEY_NAME, + SignatureAlgorithm.HS256); + } catch (NoSuchAlgorithmException | SigningKeyEncryptionException ex) { LOGGER.log(Level.SEVERE, null, ex); } return true; @@ -728,6 +734,7 @@ public boolean markOldSigningKeys() { public void removeMarkedKeys() { JwtSigningKey jwtSigningKey = jwtSigningKeyFacade.findByName(Constants.OLD_ONE_TIME_JWT_SIGNING_KEY_NAME); if (jwtSigningKey != null) { + signingKeyEncryptionService.removeFromCache(jwtSigningKey); jwtSigningKeyFacade.remove(jwtSigningKey); } } @@ -739,8 +746,9 @@ public void removeMarkedKeys() { * @return * @throws NoSuchAlgorithmException */ - public String getSigningKeyForELK(SignatureAlgorithm alg) throws NoSuchAlgorithmException { - return getOrCreateSigningKey(Constants.ELK_SIGNING_KEY_NAME, alg).getSecret(); + public String getSigningKeyForELK(SignatureAlgorithm alg) throws NoSuchAlgorithmException, + SigningKeyEncryptionException { + return getOrCreateSigningKey(Constants.ELK_SIGNING_KEY_NAME, alg).getDecryptedSecretBase64Encoded(); } /** @@ -750,10 +758,9 @@ public String getSigningKeyForELK(SignatureAlgorithm alg) throws NoSuchAlgorithm * @throws NoSuchAlgorithmException * @throws SigningKeyNotFoundException */ - public String createTokenForELK(String subjectName, String issuer, - Map claims, Date expiresAt, SignatureAlgorithm alg) - throws DuplicateSigningKeyException, NoSuchAlgorithmException, - SigningKeyNotFoundException { + public String createTokenForELK(String subjectName, String issuer, Map claims, Date expiresAt, + SignatureAlgorithm alg) throws DuplicateSigningKeyException, NoSuchAlgorithmException, SigningKeyNotFoundException, + SigningKeyEncryptionException { return createToken(Constants.ELK_SIGNING_KEY_NAME, false, issuer, null, expiresAt, null, subjectName.toLowerCase(), diff --git a/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/SigningKeyEncryptionService.java b/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/SigningKeyEncryptionService.java new file mode 100644 index 0000000000..5fb76d5cf5 --- /dev/null +++ b/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/SigningKeyEncryptionService.java @@ -0,0 +1,224 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2020, Logical Clocks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.jwt; + +import io.hops.hopsworks.persistence.entity.jwt.JwtSigningKey; +import io.hops.hopsworks.security.encryption.SymmetricEncryptionDescriptor; +import io.hops.hopsworks.security.encryption.SymmetricEncryptionService; +import io.hops.hopsworks.jwt.dao.JwtSigningKeyFacade; +import io.hops.hopsworks.jwt.exception.DuplicateSigningKeyException; +import io.hops.hopsworks.jwt.exception.SigningKeyEncryptionException; +import io.hops.hopsworks.jwt.exception.SigningKeyNotFoundException; +import io.hops.hopsworks.security.password.MasterPasswordService; + +import javax.ejb.EJB; +import javax.ejb.Singleton; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +@Singleton +public class SigningKeyEncryptionService { + private final static Logger LOGGER = Logger.getLogger(SigningKeyEncryptionService.class.getName()); + + private ConcurrentHashMap signingKeys = new ConcurrentHashMap<>(); + @EJB + private JwtSigningKeyFacade jwtSigningKeyFacade; + @EJB + private SigningKeyGenerator signingKeyGenerator; + @EJB + private SymmetricEncryptionService symmetricEncryptionService; + @EJB + private MasterPasswordService masterPasswordService; + + public SigningKeyEncryptionService() { + } + + //for test + public SigningKeyEncryptionService(JwtSigningKeyFacade jwtSigningKeyFacade, SigningKeyGenerator signingKeyGenerator + , SymmetricEncryptionService symmetricEncryptionService, MasterPasswordService masterPasswordService) { + this.jwtSigningKeyFacade = jwtSigningKeyFacade; + this.signingKeyGenerator = signingKeyGenerator; + this.symmetricEncryptionService = symmetricEncryptionService; + this.masterPasswordService = masterPasswordService; + } + + /** + * Clears decrypted signing keys from the cache. + */ + public void invalidateCache() { + if (signingKeys != null && !signingKeys.isEmpty()) { + signingKeys.clear(); + LOGGER.log(Level.INFO, "Decrypted signing key cache cleared."); + } + } + + public DecryptedSigningKey removeFromCache(JwtSigningKey jwtSigningKey) { + return signingKeys.remove(jwtSigningKey.getId()); + } + + private DecryptedSigningKey decryptAndSave(JwtSigningKey jwtSigningKey) throws SigningKeyEncryptionException { + byte[] decryptedSecret = decrypt(jwtSigningKey.getSecret()); + return save(decryptedSecret, jwtSigningKey); + } + + private DecryptedSigningKey save(byte[] decryptedSecret, JwtSigningKey jwtSigningKey) { + DecryptedSigningKey decryptedSigningKey = new DecryptedSigningKey(jwtSigningKey, decryptedSecret); + signingKeys.put(jwtSigningKey.getId(), decryptedSigningKey); + return decryptedSigningKey; + } + + /** + * Get DecryptedSigningKey from cache if it exists else gets it from database + * @param id + * @return + */ + public DecryptedSigningKey getSigningKey(Integer id) throws SigningKeyEncryptionException, + SigningKeyNotFoundException { + DecryptedSigningKey decryptedSigningKey = signingKeys.get(id); + if (decryptedSigningKey != null) { + return decryptedSigningKey; + } + return getSigningKeyFromDb(id); + } + + public DecryptedSigningKey getSigningKey(JwtSigningKey jwtSigningKey) throws SigningKeyEncryptionException { + DecryptedSigningKey decryptedSigningKey = signingKeys.get(jwtSigningKey.getId()); + if (decryptedSigningKey != null) { + return decryptedSigningKey; + } + return decryptAndSave(jwtSigningKey); + } + + /** + * Get SigningKey from database and decrypt. + * @param id + * @return + */ + public DecryptedSigningKey getSigningKeyFromDb(Integer id) throws SigningKeyEncryptionException, + SigningKeyNotFoundException { + JwtSigningKey jwtSigningKey = jwtSigningKeyFacade.find(id); + if (jwtSigningKey == null) { + throw new SigningKeyNotFoundException("Signing key not found."); + } + return decryptAndSave(jwtSigningKey); + } + + /** + * Create new signing key and saves it encrypted with the master password. + * @param keyName + * @param alg + * @return + * @throws DuplicateSigningKeyException + * @throws NoSuchAlgorithmException + */ + public DecryptedSigningKey createSigningKey(String keyName, SignatureAlgorithm alg) + throws DuplicateSigningKeyException, NoSuchAlgorithmException, SigningKeyEncryptionException { + JwtSigningKey signingKey = jwtSigningKeyFacade.findByName(keyName); + if (signingKey != null) { + throw new DuplicateSigningKeyException("A signing key with the same name already exists."); + } + return createNewSigningKey(keyName, alg); + } + + /** + * Gets signing key by name if it exists else creates a new and saves it encrypted. + * @param keyName + * @param alg + * @return + * @throws NoSuchAlgorithmException + */ + public DecryptedSigningKey getOrCreateSigningKey(String keyName, SignatureAlgorithm alg) + throws NoSuchAlgorithmException, SigningKeyEncryptionException { + JwtSigningKey signingKey = jwtSigningKeyFacade.findByName(keyName); + if (signingKey == null) { + return createNewSigningKey(keyName, alg); + } + return getSigningKey(signingKey); + } + + private DecryptedSigningKey createNewSigningKey(String keyName, SignatureAlgorithm alg) + throws NoSuchAlgorithmException, SigningKeyEncryptionException { + byte[] signingKey = signingKeyGenerator.getSigningKey(alg.getJcaName()); + String encryptedSecret = encrypt(signingKey); + JwtSigningKey jwtSigningKey = new JwtSigningKey(encryptedSecret, keyName); + jwtSigningKeyFacade.persist(jwtSigningKey); + jwtSigningKey = jwtSigningKeyFacade.findByName(keyName); + return save(signingKey, jwtSigningKey); + } + + private byte[] decrypt(String secret) throws SigningKeyEncryptionException { + try { + //get master password and decrypt + String password = masterPasswordService.getMasterEncryptionPassword(); + return decrypt(secret, password); + } catch (IOException e) { + throw new SigningKeyEncryptionException("Failed to decrypt signing key. ", e); + } + } + + public byte[] decrypt(String secret, String masterPassword) throws SigningKeyEncryptionException { + try { + // [salt(64),iv(12),payload)] + byte[][] split = symmetricEncryptionService.splitPayloadFromCryptoPrimitives(Base64.getDecoder().decode(secret)); + SymmetricEncryptionDescriptor descriptor = new SymmetricEncryptionDescriptor.Builder() + .setPassword(masterPassword) + .setSalt(split[0]) + .setIV(split[1]) + .setInput(split[2]) + .build(); + descriptor = symmetricEncryptionService.decrypt(descriptor); + return descriptor.getOutput(); + } catch (GeneralSecurityException e) { + throw new SigningKeyEncryptionException("Failed to decrypt signing key. ", e); + } + } + + private String encrypt(byte[] secret) throws SigningKeyEncryptionException { + try { + String password = masterPasswordService.getMasterEncryptionPassword(); + return encrypt(secret, password); + } catch (IOException e) { + throw new SigningKeyEncryptionException("Failed to encrypt signing key. ", e); + } + } + + public String encrypt(byte[] secret, String masterPassword) throws SigningKeyEncryptionException { + try { + SymmetricEncryptionDescriptor descriptor = new SymmetricEncryptionDescriptor.Builder() + .setInput(secret) + .setPassword(masterPassword) + .build(); + descriptor = symmetricEncryptionService.encrypt(descriptor); + byte[] encrypted = + symmetricEncryptionService.mergePayloadWithCryptoPrimitives(descriptor.getSalt(), descriptor.getIv(), + descriptor.getOutput()); + return Base64.getEncoder().encodeToString(encrypted); + } catch (GeneralSecurityException e) { + throw new SigningKeyEncryptionException("Failed to encrypt signing key. ", e); + } + } + + public String getNewEncryptedSecret(String secret, String oldMasterPassword, String newMasterPassword) + throws SigningKeyEncryptionException { + return encrypt(decrypt(secret, oldMasterPassword), newMasterPassword); + } + +} diff --git a/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/SigningKeyGenerator.java b/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/SigningKeyGenerator.java index 7e34043ae4..b750a8edce 100644 --- a/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/SigningKeyGenerator.java +++ b/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/SigningKeyGenerator.java @@ -16,7 +16,6 @@ package io.hops.hopsworks.jwt; import java.security.NoSuchAlgorithmException; -import java.util.Base64; import javax.crypto.KeyGenerator; import javax.ejb.Singleton; @@ -31,12 +30,10 @@ public class SigningKeyGenerator { * @return base64Encoded string * @throws NoSuchAlgorithmException */ - public String getSigningKey(String algorithm) throws NoSuchAlgorithmException { + public byte[] getSigningKey(String algorithm) throws NoSuchAlgorithmException { if (keyGenerator == null || !keyGenerator.getAlgorithm().equals(algorithm)) { keyGenerator = KeyGenerator.getInstance(algorithm); } - byte[] keyBytes = keyGenerator.generateKey().getEncoded(); - String base64Encoded = Base64.getEncoder().encodeToString(keyBytes); - return base64Encoded; + return keyGenerator.generateKey().getEncoded(); } } diff --git a/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/SigningKeyMasterPasswordHandler.java b/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/SigningKeyMasterPasswordHandler.java new file mode 100644 index 0000000000..e7b11b82c5 --- /dev/null +++ b/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/SigningKeyMasterPasswordHandler.java @@ -0,0 +1,78 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2020, Logical Clocks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.jwt; + +import io.hops.hopsworks.exceptions.EncryptionMasterPasswordException; +import io.hops.hopsworks.jwt.dao.JwtSigningKeyFacade; +import io.hops.hopsworks.persistence.entity.jwt.JwtSigningKey; +import io.hops.hopsworks.security.password.MasterPasswordChangeResult; +import io.hops.hopsworks.security.password.MasterPasswordHandler; + +import javax.ejb.EJB; +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +@Stateless +@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED) +public class SigningKeyMasterPasswordHandler implements MasterPasswordHandler { + private final Logger LOGGER = Logger.getLogger(SigningKeyMasterPasswordHandler.class.getName()); + @EJB + private JwtSigningKeyFacade jwtSigningKeyFacade; + @EJB + private SigningKeyEncryptionService signingKeyEncryptionService; + + @Override + public void pre() { + + } + + @Override + public MasterPasswordChangeResult perform(String oldPassword, String newPassword) { + StringBuilder successLog = new StringBuilder(); + //signingKeyEncryptionService.invalidateCache(); + Map items2rollback = new HashMap<>(); + for (JwtSigningKey key : jwtSigningKeyFacade.findAll()) { + try { + String newSecret = signingKeyEncryptionService.getNewEncryptedSecret(key.getSecret(), oldPassword, newPassword); + key.setSecret(newSecret); + jwtSigningKeyFacade.merge(key); + items2rollback.putIfAbsent(key.getId(), key.getSecret()); + successLog.append("Updated jwt signing key: ").append(key.getName()).append("\n"); + } catch (Exception ex) { + String errorMsg = "Something went wrong while updating master encryption password for jwt signing key. Update" + + " failed on key: " + key.getName(); + LOGGER.log(Level.SEVERE, errorMsg + " rolling back...", ex); + return new MasterPasswordChangeResult<>(items2rollback, new EncryptionMasterPasswordException(errorMsg)); + } + } + return new MasterPasswordChangeResult<>(successLog, items2rollback, null); + } + + @Override + public void rollback(MasterPasswordChangeResult result) { + + } + + @Override + public void post() { + + } +} diff --git a/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/dao/JwtSigningKeyFacade.java b/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/dao/JwtSigningKeyFacade.java index 1d5824b92b..be6e3dfeb5 100644 --- a/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/dao/JwtSigningKeyFacade.java +++ b/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/dao/JwtSigningKeyFacade.java @@ -15,28 +15,20 @@ */ package io.hops.hopsworks.jwt.dao; -import io.hops.hopsworks.jwt.SignatureAlgorithm; -import io.hops.hopsworks.jwt.SigningKeyGenerator; -import io.hops.hopsworks.jwt.exception.DuplicateSigningKeyException; import io.hops.hopsworks.persistence.entity.jwt.JwtSigningKey; -import java.security.NoSuchAlgorithmException; -import java.util.List; -import javax.ejb.EJB; import javax.ejb.Stateless; import javax.persistence.EntityManager; import javax.persistence.NoResultException; import javax.persistence.PersistenceContext; import javax.persistence.TypedQuery; +import java.util.List; @Stateless public class JwtSigningKeyFacade { @PersistenceContext(unitName = "kthfsPU") private EntityManager em; - - @EJB - private SigningKeyGenerator signingKeyGenerator; public JwtSigningKey find(Integer id) { return em.find(JwtSigningKey.class, id); @@ -66,40 +58,13 @@ public void renameSigningKey (JwtSigningKey signingKey, String newName) { signingKey.setName(newName); em.merge(signingKey); } - - public JwtSigningKey getOrCreateSigningKey(String keyName, SignatureAlgorithm alg) throws NoSuchAlgorithmException { - JwtSigningKey signingKey = this.findByName(keyName); - if (signingKey == null) { - signingKey = this.createSigningKey(keyName, alg); - } - return signingKey; - } - - public JwtSigningKey createNewSigningKey(String keyName, SignatureAlgorithm alg) throws NoSuchAlgorithmException, - DuplicateSigningKeyException { - JwtSigningKey signingKey = this.findByName(keyName); - if (signingKey != null) { - // throwing DuplicateSigningKeyException to catch parent exception (JWTException) and - throw new DuplicateSigningKeyException("A signing key with the same name already exists."); - } - return this.createSigningKey(keyName, alg); - } - - private JwtSigningKey createSigningKey(String keyName, SignatureAlgorithm alg) throws NoSuchAlgorithmException { - JwtSigningKey signingKey; - String base64Encoded = signingKeyGenerator.getSigningKey(alg.getJcaName()); - signingKey = new JwtSigningKey(base64Encoded, keyName); - persist(signingKey); - JwtSigningKey newSigningKey = findByName(keyName); - return newSigningKey; - } - public void persist(JwtSigningKey invalidJwt) { - em.persist(invalidJwt); + public void persist(JwtSigningKey jwtSigningKey) { + em.persist(jwtSigningKey); } - public JwtSigningKey merge(JwtSigningKey invalidJwt) { - return em.merge(invalidJwt); + public JwtSigningKey merge(JwtSigningKey jwtSigningKey) { + return em.merge(jwtSigningKey); } public void remove(JwtSigningKey jwtSigningKey) { diff --git a/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/exception/SigningKeyEncryptionException.java b/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/exception/SigningKeyEncryptionException.java new file mode 100644 index 0000000000..c7cc7d21b5 --- /dev/null +++ b/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/exception/SigningKeyEncryptionException.java @@ -0,0 +1,38 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2020, Logical Clocks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.jwt.exception; + +public class SigningKeyEncryptionException extends JWTException { + public SigningKeyEncryptionException() { + } + + public SigningKeyEncryptionException(String message) { + super(message); + } + + public SigningKeyEncryptionException(String message, Throwable cause) { + super(message, cause); + } + + public SigningKeyEncryptionException(Throwable cause) { + super(cause); + } + + public SigningKeyEncryptionException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/filter/JWTFilter.java b/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/filter/JWTFilter.java index a2f872f9fa..0401a3a376 100644 --- a/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/filter/JWTFilter.java +++ b/hopsworks-jwt/src/main/java/io/hops/hopsworks/jwt/filter/JWTFilter.java @@ -37,6 +37,8 @@ import static io.hops.hopsworks.jwt.Constants.EXPIRY_LEEWAY; import static io.hops.hopsworks.jwt.Constants.ROLES; import static io.hops.hopsworks.jwt.Constants.WWW_AUTHENTICATE_VALUE; + +import io.hops.hopsworks.jwt.exception.SigningKeyEncryptionException; import io.hops.hopsworks.jwt.exception.SigningKeyNotFoundException; public abstract class JWTFilter implements ContainerRequestFilter { @@ -133,7 +135,8 @@ private boolean intersect(Collection list1, Collection list2) { return !set1.isEmpty(); } - public abstract Algorithm getAlgorithm(DecodedJWT jwt) throws SigningKeyNotFoundException; + public abstract Algorithm getAlgorithm(DecodedJWT jwt) throws SigningKeyNotFoundException, + SigningKeyEncryptionException; public abstract Set allowedRoles(); diff --git a/hopsworks-jwt/src/test/java/io/hops/hopsworks/jwt/TestSigningKeyEncryptionService.java b/hopsworks-jwt/src/test/java/io/hops/hopsworks/jwt/TestSigningKeyEncryptionService.java new file mode 100644 index 0000000000..69f42832f9 --- /dev/null +++ b/hopsworks-jwt/src/test/java/io/hops/hopsworks/jwt/TestSigningKeyEncryptionService.java @@ -0,0 +1,127 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2020, Logical Clocks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.jwt; + +import io.hops.hopsworks.jwt.dao.JwtSigningKeyFacade; +import io.hops.hopsworks.jwt.exception.SigningKeyEncryptionException; +import io.hops.hopsworks.persistence.entity.jwt.JwtSigningKey; +import io.hops.hopsworks.security.encryption.SymmetricEncryptionService; +import io.hops.hopsworks.security.password.MasterPasswordService; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Random; + +import static junit.framework.Assert.assertTrue; +import static org.mockito.Mockito.doAnswer; + +public class TestSigningKeyEncryptionService { + + private SigningKeyEncryptionService signingKeyEncryptionService; + private JwtSigningKeyFacade jwtSigningKeyFacade; + private SymmetricEncryptionService symmetricEncryptionService; + private SigningKeyGenerator signingKeyGenerator; + private MasterPasswordService masterPasswordService; + + byte[] signingKey1; + byte[] signingKey2; + private String masterPassword; + private Random random = new Random(); + + @Before + public void beforeTest() throws IOException, SigningKeyEncryptionException, NoSuchAlgorithmException { + + jwtSigningKeyFacade = Mockito.mock(JwtSigningKeyFacade.class); + masterPasswordService = Mockito.mock(MasterPasswordService.class); + signingKeyGenerator = new SigningKeyGenerator(); + symmetricEncryptionService = new SymmetricEncryptionService(); + symmetricEncryptionService.init(); + Mockito.when(masterPasswordService.getMasterEncryptionPassword()).thenReturn("master password"); + + signingKeyEncryptionService = new SigningKeyEncryptionService(jwtSigningKeyFacade, signingKeyGenerator, + symmetricEncryptionService, masterPasswordService); + + masterPassword = masterPasswordService.getMasterEncryptionPassword(); + signingKey1 = signingKeyGenerator.getSigningKey(SignatureAlgorithm.HS512.getJcaName()); + + createJwtSigningKey(1, "api", signingKey1); + + signingKey2 = signingKeyGenerator.getSigningKey(SignatureAlgorithm.HS512.getJcaName()); + + createJwtSigningKey(2, "service", signingKey2); + + } + + @Test + public void testGetDecryptedSigningKey() throws Exception { + DecryptedSigningKey decryptedSigningKey = signingKeyEncryptionService.getSigningKey(1); + assertTrue (Arrays.equals(decryptedSigningKey.getDecryptedSecret(), signingKey1)); + + decryptedSigningKey = signingKeyEncryptionService.getSigningKey(2); + assertTrue (Arrays.equals(decryptedSigningKey.getDecryptedSecret(), signingKey2)); + } + + @Test + public void testGetDecryptedSigningKeyByName() throws Exception { + DecryptedSigningKey decryptedSigningKey = + signingKeyEncryptionService.getOrCreateSigningKey("api", SignatureAlgorithm.HS512); + assertTrue(Arrays.equals(decryptedSigningKey.getDecryptedSecret(), signingKey1)); + + decryptedSigningKey = signingKeyEncryptionService.getOrCreateSigningKey("service", SignatureAlgorithm.HS512); + assertTrue(Arrays.equals(decryptedSigningKey.getDecryptedSecret(), signingKey2)); + } + + @Test + public void testCreateSigningKey() throws Exception { + Integer id = whenPersist(); + DecryptedSigningKey decryptedSigningKey = signingKeyEncryptionService.getOrCreateSigningKey("oneTime", + SignatureAlgorithm.HS512); + DecryptedSigningKey decryptedSigningKey1 = signingKeyEncryptionService.getSigningKey(id); + assertTrue (Arrays.equals(decryptedSigningKey.getDecryptedSecret(), decryptedSigningKey1.getDecryptedSecret())); + } + + private Integer whenPersist() { + Integer id = random.ints(3,10000).findFirst().getAsInt(); + doAnswer(new Answer(){ + @Override + public Object answer(InvocationOnMock invocation){ + JwtSigningKey jwtSigningKey = (JwtSigningKey) invocation.getArguments()[0]; + persist(jwtSigningKey, id); + return id; + } + }).when(jwtSigningKeyFacade).persist(Mockito.any(JwtSigningKey.class)); + return id; + } + + private void createJwtSigningKey(Integer id, String name, byte[] signingKey) throws SigningKeyEncryptionException { + String encryptedSecret = signingKeyEncryptionService.encrypt(signingKey, masterPassword); + JwtSigningKey jwtSigningKey = new JwtSigningKey(encryptedSecret, name); + persist(jwtSigningKey, id); + } + + private void persist(JwtSigningKey jwtSigningKey, Integer id) { + jwtSigningKey.setId(id); + Mockito.when(jwtSigningKeyFacade.find(id)).thenReturn(jwtSigningKey); + Mockito.when(jwtSigningKeyFacade.findByName(jwtSigningKey.getName())).thenReturn(jwtSigningKey); + } + +} \ No newline at end of file diff --git a/hopsworks-security/pom.xml b/hopsworks-security/pom.xml new file mode 100644 index 0000000000..1a4b459962 --- /dev/null +++ b/hopsworks-security/pom.xml @@ -0,0 +1,84 @@ + + + 4.0.0 + + io.hops + hopsworks + 1.4.0-SNAPSHOT + .. + + + io.hops.hopsworks + hopsworks-security + 1.4.0-SNAPSHOT + ejb + Hopsworks Security + hopsworks-security + + + + org.apache.commons + commons-lang3 + 3.9 + + + commons-net + commons-net + 3.6 + + + commons-io + commons-io + 2.7 + + + commons-codec + commons-codec + 1.14 + + + io.hops.hopsworks + hopsworks-persistence + ${project.version} + + + io.hops.hopsworks + hopsworks-rest-utils + ${project.version} + provided + + + io.hops.hopsworks + hopsworks-persistence + ${project.version} + + + + * + * + + + + + javax + javaee-api + 7.0 + provided + + + + hopsworks-security + + + org.apache.maven.plugins + maven-ejb-plugin + 2.3 + + 3.1 + + + + + \ No newline at end of file diff --git a/hopsworks-security/src/main/java/io/hops/hopsworks/security/dao/AbstractFacade.java b/hopsworks-security/src/main/java/io/hops/hopsworks/security/dao/AbstractFacade.java new file mode 100755 index 0000000000..f57b5ff009 --- /dev/null +++ b/hopsworks-security/src/main/java/io/hops/hopsworks/security/dao/AbstractFacade.java @@ -0,0 +1,75 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2020, Logical Clocks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ + +package io.hops.hopsworks.security.dao; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.List; + +public abstract class AbstractFacade { + + private final Class entityClass; + + public AbstractFacade(Class entityClass) { + this.entityClass = entityClass; + } + + protected abstract EntityManager getEntityManager(); + + public void save(T entity) { + getEntityManager().persist(entity); + } + + public T update(T entity) { + return getEntityManager().merge(entity); + } + + public void remove(T entity) { + if (entity == null) { + return; + } + getEntityManager().remove(getEntityManager().merge(entity)); + getEntityManager().flush(); + } + + public T find(Object id) { + return getEntityManager().find(entityClass, id); + } + + public List findAll() { + javax.persistence.criteria.CriteriaQuery cq = getEntityManager().getCriteriaBuilder().createQuery(); + cq.select(cq.from(entityClass)); + return getEntityManager().createQuery(cq).getResultList(); + } + + public List findRange(int[] range) { + javax.persistence.criteria.CriteriaQuery cq = getEntityManager().getCriteriaBuilder().createQuery(); + cq.select(cq.from(entityClass)); + Query q = getEntityManager().createQuery(cq); + q.setMaxResults(range[1] - range[0]); + q.setFirstResult(range[0]); + return q.getResultList(); + } + + public long count() { + javax.persistence.criteria.CriteriaQuery cq = getEntityManager().getCriteriaBuilder().createQuery(); + javax.persistence.criteria.Root rt = cq.from(entityClass); + cq.select(getEntityManager().getCriteriaBuilder().count(rt)).where(); + Query q = getEntityManager().createQuery(cq); + return (Long) q.getSingleResult(); + } +} diff --git a/hopsworks-security/src/main/java/io/hops/hopsworks/security/dao/MessageFacade.java b/hopsworks-security/src/main/java/io/hops/hopsworks/security/dao/MessageFacade.java new file mode 100644 index 0000000000..4415ab26e5 --- /dev/null +++ b/hopsworks-security/src/main/java/io/hops/hopsworks/security/dao/MessageFacade.java @@ -0,0 +1,38 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2020, Logical Clocks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.security.dao; + +import io.hops.hopsworks.persistence.entity.message.Message; + +import javax.ejb.Stateless; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +@Stateless +public class MessageFacade extends AbstractFacade { + + @PersistenceContext(unitName = "kthfsPU") + private EntityManager em; + + @Override + protected EntityManager getEntityManager() { + return em; + } + + public MessageFacade() { + super(Message.class); + } +} diff --git a/hopsworks-security/src/main/java/io/hops/hopsworks/security/dao/UsersFacade.java b/hopsworks-security/src/main/java/io/hops/hopsworks/security/dao/UsersFacade.java new file mode 100644 index 0000000000..b60af8c265 --- /dev/null +++ b/hopsworks-security/src/main/java/io/hops/hopsworks/security/dao/UsersFacade.java @@ -0,0 +1,47 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2020, Logical Clocks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.security.dao; + +import io.hops.hopsworks.persistence.entity.user.Users; + +import javax.ejb.Stateless; +import javax.persistence.EntityManager; +import javax.persistence.NoResultException; +import javax.persistence.PersistenceContext; + +@Stateless +public class UsersFacade extends AbstractFacade { + + @PersistenceContext(unitName = "kthfsPU") + private EntityManager em; + + @Override + protected EntityManager getEntityManager() { + return em; + } + + public UsersFacade() { + super(Users.class); + } + + public Users findByEmail(String email) { + try { + return em.createNamedQuery("Users.findByEmail", Users.class).setParameter("email", email).getSingleResult(); + } catch (NoResultException e) { + return null; + } + } +} diff --git a/hopsworks-security/src/main/java/io/hops/hopsworks/security/dao/VariablesFacade.java b/hopsworks-security/src/main/java/io/hops/hopsworks/security/dao/VariablesFacade.java new file mode 100644 index 0000000000..d659f25f4d --- /dev/null +++ b/hopsworks-security/src/main/java/io/hops/hopsworks/security/dao/VariablesFacade.java @@ -0,0 +1,38 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2020, Logical Clocks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.security.dao; + +import io.hops.hopsworks.persistence.entity.util.Variables; + +import javax.ejb.Stateless; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +@Stateless +public class VariablesFacade extends AbstractFacade { + + @PersistenceContext(unitName = "kthfsPU") + private EntityManager em; + + @Override + protected EntityManager getEntityManager() { + return em; + } + + public VariablesFacade() { + super(Variables.class); + } +} diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/SymmetricEncryptionDescriptor.java b/hopsworks-security/src/main/java/io/hops/hopsworks/security/encryption/SymmetricEncryptionDescriptor.java similarity index 98% rename from hopsworks-common/src/main/java/io/hops/hopsworks/common/security/SymmetricEncryptionDescriptor.java rename to hopsworks-security/src/main/java/io/hops/hopsworks/security/encryption/SymmetricEncryptionDescriptor.java index 107b87cf16..cfab1c0b60 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/SymmetricEncryptionDescriptor.java +++ b/hopsworks-security/src/main/java/io/hops/hopsworks/security/encryption/SymmetricEncryptionDescriptor.java @@ -14,7 +14,7 @@ * If not, see . */ -package io.hops.hopsworks.common.security; +package io.hops.hopsworks.security.encryption; import java.nio.charset.Charset; diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/SymmetricEncryptionService.java b/hopsworks-security/src/main/java/io/hops/hopsworks/security/encryption/SymmetricEncryptionService.java similarity index 99% rename from hopsworks-common/src/main/java/io/hops/hopsworks/common/security/SymmetricEncryptionService.java rename to hopsworks-security/src/main/java/io/hops/hopsworks/security/encryption/SymmetricEncryptionService.java index e184ab9881..2d5d92d58d 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/SymmetricEncryptionService.java +++ b/hopsworks-security/src/main/java/io/hops/hopsworks/security/encryption/SymmetricEncryptionService.java @@ -14,7 +14,7 @@ * If not, see . */ -package io.hops.hopsworks.common.security; +package io.hops.hopsworks.security.encryption; import org.apache.commons.lang3.tuple.Pair; diff --git a/hopsworks-security/src/main/java/io/hops/hopsworks/security/encryption/SymmetricEncryptionUtil.java b/hopsworks-security/src/main/java/io/hops/hopsworks/security/encryption/SymmetricEncryptionUtil.java new file mode 100644 index 0000000000..ec73653d9f --- /dev/null +++ b/hopsworks-security/src/main/java/io/hops/hopsworks/security/encryption/SymmetricEncryptionUtil.java @@ -0,0 +1,62 @@ +package io.hops.hopsworks.security.encryption; + +import org.apache.commons.net.util.Base64; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +import java.security.Key; + +public class SymmetricEncryptionUtil { + + private static Key generateKey(String userKey, String masterKey) { + // This is for backwards compatibility + // sha256 of 'adminpw' + if (masterKey.equals("5fcf82bc15aef42cd3ec93e6d4b51c04df110cf77ee715f62f3f172ff8ed9de9")) { + return new SecretKeySpec(userKey.substring(0, 16).getBytes(), "AES"); + } + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 8; i++) { + sb.append(userKey.charAt(i)); + if (masterKey.length() > i + 1) { + sb.append(masterKey.charAt(i + 1)); + } else { + sb.append(userKey.charAt(Math.max(0, userKey.length() - i))); + } + } + return new SecretKeySpec(sb.toString().getBytes(), "AES"); + } + + /** + * + * @param key + * @param plaintext + * @return + * @throws Exception + */ + public static String encrypt(String key, String plaintext, String masterEncryptionPassword) + throws Exception { + + Key aesKey = generateKey(key, masterEncryptionPassword); + Cipher cipher = Cipher.getInstance("AES"); + cipher.init(Cipher.ENCRYPT_MODE, aesKey); + byte[] encrypted = cipher.doFinal(plaintext.getBytes()); + return Base64.encodeBase64String(encrypted); + } + + /** + * + * @param key + * @param ciphertext + * @return + * @throws Exception + */ + public static String decrypt(String key, String ciphertext, String masterEncryptionPassword) + throws Exception { + Cipher cipher = Cipher.getInstance("AES"); + Key aesKey = generateKey(key, masterEncryptionPassword); + cipher.init(Cipher.DECRYPT_MODE, aesKey); + String decrypted = new String(cipher.doFinal(Base64.decodeBase64(ciphertext))); + return decrypted; + } +} diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/MasterPasswordChangeResult.java b/hopsworks-security/src/main/java/io/hops/hopsworks/security/password/MasterPasswordChangeResult.java similarity index 97% rename from hopsworks-common/src/main/java/io/hops/hopsworks/common/security/MasterPasswordChangeResult.java rename to hopsworks-security/src/main/java/io/hops/hopsworks/security/password/MasterPasswordChangeResult.java index 36d3341a10..38744a1df0 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/MasterPasswordChangeResult.java +++ b/hopsworks-security/src/main/java/io/hops/hopsworks/security/password/MasterPasswordChangeResult.java @@ -14,7 +14,7 @@ * If not, see . */ -package io.hops.hopsworks.common.security; +package io.hops.hopsworks.security.password; import io.hops.hopsworks.exceptions.EncryptionMasterPasswordException; diff --git a/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/MasterPasswordHandler.java b/hopsworks-security/src/main/java/io/hops/hopsworks/security/password/MasterPasswordHandler.java similarity index 75% rename from hopsworks-common/src/main/java/io/hops/hopsworks/common/security/MasterPasswordHandler.java rename to hopsworks-security/src/main/java/io/hops/hopsworks/security/password/MasterPasswordHandler.java index eb87fa08b4..16ee3459b0 100644 --- a/hopsworks-common/src/main/java/io/hops/hopsworks/common/security/MasterPasswordHandler.java +++ b/hopsworks-security/src/main/java/io/hops/hopsworks/security/password/MasterPasswordHandler.java @@ -14,9 +14,9 @@ * If not, see . */ -package io.hops.hopsworks.common.security; +package io.hops.hopsworks.security.password; -import io.hops.hopsworks.common.util.HopsUtils; +import io.hops.hopsworks.security.encryption.SymmetricEncryptionUtil; /** * Interface for various handlers when Hopsworks master encryption password changes @@ -27,8 +27,8 @@ public interface MasterPasswordHandler { void rollback(MasterPasswordChangeResult result); void post(); default String getNewUserPassword(String userPassword, String cipherText, String oldMasterPassword, - String newMasterPassword) throws Exception { - String plainCertPassword = HopsUtils.decrypt(userPassword, cipherText, oldMasterPassword); - return HopsUtils.encrypt(userPassword, plainCertPassword, newMasterPassword); + String newMasterPassword) throws Exception { + String plainCertPassword = SymmetricEncryptionUtil.decrypt(userPassword, cipherText, oldMasterPassword); + return SymmetricEncryptionUtil.encrypt(userPassword, plainCertPassword, newMasterPassword); } } diff --git a/hopsworks-security/src/main/java/io/hops/hopsworks/security/password/MasterPasswordService.java b/hopsworks-security/src/main/java/io/hops/hopsworks/security/password/MasterPasswordService.java new file mode 100644 index 0000000000..8e4fc914a0 --- /dev/null +++ b/hopsworks-security/src/main/java/io/hops/hopsworks/security/password/MasterPasswordService.java @@ -0,0 +1,274 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2020, Logical Clocks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.security.password; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import io.hops.hopsworks.exceptions.EncryptionMasterPasswordException; +import io.hops.hopsworks.persistence.entity.message.Message; +import io.hops.hopsworks.persistence.entity.user.Users; +import io.hops.hopsworks.security.dao.MessageFacade; +import io.hops.hopsworks.security.dao.UsersFacade; +import io.hops.hopsworks.security.util.SecuritySettings; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.FileUtils; + +import javax.annotation.PostConstruct; +import javax.ejb.AccessTimeout; +import javax.ejb.Asynchronous; +import javax.ejb.ConcurrencyManagement; +import javax.ejb.ConcurrencyManagementType; +import javax.ejb.EJB; +import javax.ejb.Lock; +import javax.ejb.LockType; +import javax.ejb.Singleton; +import javax.enterprise.inject.Any; +import javax.enterprise.inject.Instance; +import javax.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.attribute.PosixFileAttributeView; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +@Singleton +@ConcurrencyManagement(ConcurrencyManagementType.CONTAINER) +public class MasterPasswordService { + private final Logger LOG = Logger.getLogger(MasterPasswordService.class.getName()); + + @EJB + private SecuritySettings securitySettings; + @EJB + private UsersFacade userFacade; + @EJB + private MessageFacade messageFacade; + + @Inject + @Any + private Instance handlers; + + public enum UPDATE_STATUS { + OK, + WORKING, + FAILED, + NOT_FOUND + } + + private File masterPasswordFile; + private final Map handlersResult = new HashMap<>(); + private Cache updateStatus; + private Random rand; + + @PostConstruct + public void init() { + masterPasswordFile = new File(securitySettings.getHopsworksMasterEncPasswordFile()); + if (!masterPasswordFile.exists()) { + throw new IllegalStateException("Master encryption file does not exist"); + } + + try { + PosixFileAttributeView fileView = Files.getFileAttributeView(masterPasswordFile.toPath(), + PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS); + Set filePermissions = fileView.readAttributes().permissions(); + boolean ownerRead = filePermissions.contains(PosixFilePermission.OWNER_READ); + boolean ownerWrite = filePermissions.contains(PosixFilePermission.OWNER_WRITE); + boolean ownerExecute = filePermissions.contains(PosixFilePermission.OWNER_EXECUTE); + boolean groupRead = filePermissions.contains(PosixFilePermission.GROUP_READ); + boolean groupWrite = filePermissions.contains(PosixFilePermission.GROUP_WRITE); + boolean groupExecute = filePermissions.contains(PosixFilePermission.GROUP_EXECUTE); + boolean othersRead = filePermissions.contains(PosixFilePermission.OTHERS_READ); + boolean othersWrite = filePermissions.contains(PosixFilePermission.OTHERS_WRITE); + boolean othersExecute = filePermissions.contains(PosixFilePermission.OTHERS_EXECUTE); + + // Permissions should be 700 + if ((ownerRead && ownerWrite && ownerExecute) + && (!groupRead && !groupWrite && !groupExecute) + && (!othersRead && !othersWrite && !othersExecute)) { + String owner = fileView.readAttributes().owner().getName(); + String group = fileView.readAttributes().group().getName(); + String permStr = PosixFilePermissions.toString(filePermissions); + LOG.log(Level.INFO, "Passed permissions check for file " + masterPasswordFile.getAbsolutePath() + + ". Owner: " + owner + " Group: " + group + " Permissions: " + permStr); + } else { + throw new IllegalStateException("Wrong permissions for file " + masterPasswordFile.getAbsolutePath() + + ", it should be 700"); + } + + updateStatus = CacheBuilder.newBuilder() + .maximumSize(100) + .expireAfterWrite(12L, TimeUnit.HOURS) + .build(); + rand = new Random(); + } catch (UnsupportedOperationException ex) { + LOG.log(Level.WARNING, "Associated filesystem is not POSIX compliant. " + + "Continue without checking the permissions of " + masterPasswordFile.getAbsolutePath() + + " This might be a security problem."); + } catch (IOException ex) { + throw new IllegalStateException("Error while getting POSIX permissions of " + masterPasswordFile + .getAbsolutePath()); + } + } + + @Lock(LockType.READ) + @AccessTimeout(value = 3, unit = TimeUnit.SECONDS) + public String getMasterEncryptionPassword() throws IOException { + return FileUtils.readFileToString(masterPasswordFile).trim(); + } + + /** + * Validates the provided password against the configured one + * @param providedPassword Password to validate + * @param userRequestedEmail User requested the password check + * @throws IOException + * @throws EncryptionMasterPasswordException + */ + @Lock(LockType.READ) + @AccessTimeout(value = 3, unit = TimeUnit.SECONDS) + public void checkPassword(String providedPassword, String userRequestedEmail) + throws IOException, EncryptionMasterPasswordException { + String sha = DigestUtils.sha256Hex(providedPassword); + if (!getMasterEncryptionPassword().equals(sha)) { + Users user = userFacade.findByEmail(userRequestedEmail); + String logMsg = "*** Attempt to change master encryption password with wrong credentials"; + if (user != null) { + LOG.log(Level.INFO, logMsg + " by user <" + user.getUsername() + ">"); + } else { + LOG.log(Level.INFO, logMsg); + } + throw new EncryptionMasterPasswordException("Provided password is incorrect"); + } + } + + public Integer initUpdateOperation() { + Integer operationId = rand.nextInt(); + updateStatus.put(operationId, UPDATE_STATUS.WORKING); + return operationId; + } + + public UPDATE_STATUS getOperationStatus(Integer operationId) { + UPDATE_STATUS status = updateStatus.getIfPresent(operationId); + return status != null ? status : UPDATE_STATUS.NOT_FOUND; + } + + /** + * Decrypt secrets using the old master password and encrypt them with the new + * Both for project specific and project generic certificates + * @param newMasterPasswd new master encryption password + * @param userRequested User requested password change + */ + @SuppressWarnings("unchecked") + @Asynchronous + @Lock(LockType.WRITE) + @AccessTimeout(value = 500) + public void resetMasterEncryptionPassword(Integer operationId, String newMasterPasswd, String userRequested) { + try { + String newDigest = DigestUtils.sha256Hex(newMasterPasswd); + callUpdateHandlers(newDigest); + updateMasterEncryptionPassword(newDigest); + StringBuilder successLog = gatherLogs(); + sendSuccessfulMessage(successLog, userRequested); + updateStatus.put(operationId, UPDATE_STATUS.OK); + LOG.log(Level.INFO, "Master encryption password changed!"); + } catch (EncryptionMasterPasswordException ex) { + String errorMsg = "*** Master encryption password update failed!!! Rolling back..."; + LOG.log(Level.SEVERE, errorMsg, ex); + updateStatus.put(operationId, UPDATE_STATUS.FAILED); + callRollbackHandlers(); + sendUnsuccessfulMessage(errorMsg + "\n" + ex.getMessage(), userRequested); + } catch (IOException ex) { + String errorMsg = "*** Failed to write new encryption password to file: " + masterPasswordFile.getAbsolutePath() + + ". Rolling back..."; + LOG.log(Level.SEVERE, errorMsg, ex); + updateStatus.put(operationId, UPDATE_STATUS.FAILED); + callRollbackHandlers(); + sendUnsuccessfulMessage(errorMsg + "\n" + ex.getMessage(), userRequested); + } finally { + handlersResult.clear(); + } + } + + private void callUpdateHandlers(String newDigest) throws EncryptionMasterPasswordException, IOException { + for (MasterPasswordHandler handler : handlers) { + MasterPasswordChangeResult result = handler.perform(getMasterEncryptionPassword(), newDigest); + handlersResult.put(handler.getClass(), result); + if (result.getCause() != null) { + throw result.getCause(); + } + } + } + + private void callRollbackHandlers() { + for (MasterPasswordHandler handler : handlers) { + MasterPasswordChangeResult result = handlersResult.get(handler.getClass()); + if (result != null) { + handler.rollback(result); + } + } + } + + private StringBuilder gatherLogs() { + StringBuilder successLog = new StringBuilder(); + for (MasterPasswordChangeResult result : handlersResult.values()) { + if (result.getSuccessLog() != null) { + successLog.append(result.getSuccessLog()); + successLog.append("\n\n"); + } + } + return successLog; + } + + private void updateMasterEncryptionPassword(String newPassword) throws IOException { + FileUtils.writeStringToFile(masterPasswordFile, newPassword); + } + + private void sendSuccessfulMessage(StringBuilder successLog, String userRequested) { + sendInbox(successLog.toString(), "Changed successfully", userRequested); + } + + private void sendUnsuccessfulMessage(String message, String userRequested) { + sendInbox(message, "Change failed!", userRequested); + } + + private void sendInbox(String message, String preview, String userRequested) { + Users to = userFacade.findByEmail(userRequested); + Users from = userFacade.findByEmail(securitySettings.getAdminEmail()); + send(to, from, message, preview); + } + + private void send(Users to, Users from, String msg, String preview) { + Date now = new Date(); + String date = new SimpleDateFormat("yyyy-MM-dd HH:mm").format(now); + String dateAndWriter = "On " + date + ", " + from.getFname() + " " + from.getLname() + " wrote:

"; + String message = "
" + dateAndWriter + msg; + Message newMsg = new Message(from, to, now, message, true, false); + newMsg.setPath(""); + newMsg.setSubject("Master encryption password changed"); + newMsg.setPreview(preview); + messageFacade.save(newMsg); + } + +} diff --git a/hopsworks-security/src/main/java/io/hops/hopsworks/security/util/SecuritySettings.java b/hopsworks-security/src/main/java/io/hops/hopsworks/security/util/SecuritySettings.java new file mode 100644 index 0000000000..2ad74c3373 --- /dev/null +++ b/hopsworks-security/src/main/java/io/hops/hopsworks/security/util/SecuritySettings.java @@ -0,0 +1,95 @@ +/* + * This file is part of Hopsworks + * Copyright (C) 2020, Logical Clocks AB. All rights reserved + * + * Hopsworks is free software: you can redistribute it and/or modify it under the terms of + * the GNU Affero General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see . + */ +package io.hops.hopsworks.security.util; + +import io.hops.hopsworks.persistence.entity.util.Variables; +import io.hops.hopsworks.security.dao.VariablesFacade; + +import javax.ejb.ConcurrencyManagement; +import javax.ejb.ConcurrencyManagementType; +import javax.ejb.EJB; +import javax.ejb.Singleton; +import java.io.File; + +@Singleton +@ConcurrencyManagement(ConcurrencyManagementType.BEAN) +public class SecuritySettings { + + private static final String VARIABLE_CERTS_DIRS = "certs_dir"; + private static final String VARIABLE_ADMIN_EMAIL = "admin_email"; + + private String CERTS_DIR = "/srv/hops/certs-dir"; + private String ADMIN_EMAIL = "admin@hopsworks.ai"; + + private boolean cached = false; + + @EJB + private VariablesFacade variablesFacade; + + private String setDirVar(String varName, String defaultValue) { + Variables dirName = variablesFacade.find(varName); + if (dirName != null && dirName.getValue() != null && (new File(dirName.getValue()).isDirectory())) { + String val = dirName.getValue(); + if (val != null && !val.isEmpty()) { + return val; + } + } + return defaultValue; + } + + private String setVar(String varName, String defaultValue) { + Variables var = variablesFacade.find(varName); + if (var != null && var.getValue() != null && (!var.getValue().isEmpty())) { + String val = var.getValue(); + if (val != null && !val.isEmpty()) { + return val; + } + } + return defaultValue; + } + + private void populateCache() { + if (!cached) { + ADMIN_EMAIL = setVar(VARIABLE_ADMIN_EMAIL, ADMIN_EMAIL); + CERTS_DIR = setDirVar(VARIABLE_CERTS_DIRS, CERTS_DIR); + } + } + + private void checkCache() { + if (!cached) { + populateCache(); + } + } + + public synchronized void refreshCache() { + cached = false; + populateCache(); + } + + public synchronized String getCertsDir() { + checkCache(); + return CERTS_DIR; + } + + public synchronized String getHopsworksMasterEncPasswordFile() { + return getCertsDir() + File.separator + "encryption_master_password"; + } + + public synchronized String getAdminEmail() { + checkCache(); + return ADMIN_EMAIL; + } +} diff --git a/pom.xml b/pom.xml index 10e7f52707..daf9d367dd 100755 --- a/pom.xml +++ b/pom.xml @@ -60,6 +60,7 @@ hopsworks-IT hopsworks-rest-utils hopsworks-UT + hopsworks-security