diff --git a/jans-fido2/common/pom.xml b/jans-fido2/common/pom.xml new file mode 100644 index 00000000000..9e9b92a2dd6 --- /dev/null +++ b/jans-fido2/common/pom.xml @@ -0,0 +1,170 @@ + + + 4.0.0 + jans-fido2-common + jar + FIDO2 Common + + + io.jans + jans-fido2-parent + 1.0.7-SNAPSHOT + + + + + + src/main/resources + true + + **/*.xml + **/services/* + **/*.properties + + + + + + + src/test/resources + true + + **/*.xml + **/services/* + **/*.properties + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + false + + + + + + + + + io.jans + jans-fido2-model + ${janssen.version} + + + io.jans + jans-orm-model + ${janssen.version} + + + io.jans + jans-core-model + + + io.jans + jans-orm-ldap + + + io.jans + jans-orm-couchbase + + + io.jans + jans-orm-sql + + + ${project.groupId} + jans-orm-hybrid + + + io.jans + jans-core-util + + + io.jans + jans-core-service + + + io.jans + jans-core-cache + + + + io.jans + jans-auth-client + + + + io.jans + jans-auth-common + + + + + jakarta.enterprise + jakarta.enterprise.cdi-api + provided + + + jakarta.servlet + jakarta.servlet-api + provided + + + org.jboss.weld + weld-core-impl + provided + + + + jakarta.inject + jakarta.inject-api + + + org.glassfish + jakarta.faces + + + jakarta.validation + jakarta.validation-api + + + + jakarta.ejb + jakarta.ejb-api + provided + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-cbor + + + + + + + org.quartz-scheduler + quartz + + + + + org.testng + testng + + + + + + + org.jboss.resteasy + resteasy-client + + + + + \ No newline at end of file diff --git a/jans-fido2/common/src/main/java/io/jans/fido2/exception/Fido2RuntimeException.java b/jans-fido2/common/src/main/java/io/jans/fido2/exception/Fido2RuntimeException.java new file mode 100644 index 00000000000..98b110a895a --- /dev/null +++ b/jans-fido2/common/src/main/java/io/jans/fido2/exception/Fido2RuntimeException.java @@ -0,0 +1,49 @@ +/* + * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +package io.jans.fido2.exception; + +import io.jans.fido2.model.error.Fido2RPError; + +/** + * Parent class of all FIDO2 RuntimeExceptions + * + */ +public class Fido2RuntimeException extends RuntimeException { + + private static final long serialVersionUID = -118563205092295773L; + + private final String status; + private final String errorMessage; + + public Fido2RuntimeException(String errorMessage) { + super(errorMessage); + this.status = "failed"; + this.errorMessage = errorMessage; + } + + public Fido2RuntimeException(String errorMessage, Throwable cause) { + super(errorMessage, cause); + this.status = "failed"; + this.errorMessage = errorMessage; + } + + public Fido2RuntimeException(String status, String errorMessage) { + super(errorMessage); + this.status = status; + this.errorMessage = errorMessage; + } + + public Fido2RuntimeException(String status, String errorMessage, Throwable cause) { + super(errorMessage, cause); + this.status = status; + this.errorMessage = errorMessage; + } + + public Fido2RPError getFormattedMessage() { + return new Fido2RPError(status, errorMessage); + } +} diff --git a/jans-fido2/common/src/main/java/io/jans/fido2/service/Base64Service.java b/jans-fido2/common/src/main/java/io/jans/fido2/service/Base64Service.java new file mode 100644 index 00000000000..ab9d8801c2d --- /dev/null +++ b/jans-fido2/common/src/main/java/io/jans/fido2/service/Base64Service.java @@ -0,0 +1,77 @@ +/* + * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +package io.jans.fido2.service; + +import java.util.Base64; +import java.util.Base64.Decoder; +import java.util.Base64.Encoder; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.slf4j.Logger; + +/** + * Utility methods for base64 encoding / decoding + * @author Yuriy Movchan + * @version May 08, 2020 + */ + +@ApplicationScoped +public class Base64Service { + + @Inject + private Logger log; + + private Encoder base64Encoder; + private Decoder base64Decoder; + + private Encoder base64UrlEncoder; + private Decoder base64UrlDecoder; + + @PostConstruct + public void init() { + this.base64Encoder = Base64.getEncoder().withoutPadding(); + this.base64Decoder = Base64.getDecoder(); + + this.base64UrlEncoder = Base64.getUrlEncoder().withoutPadding(); + this.base64UrlDecoder = Base64.getUrlDecoder(); + } + + public String encodeToString(byte[] src) { + return base64Encoder.encodeToString(src); + } + + public byte[] encode(byte[] src) { + return base64Encoder.encode(src); + } + + public byte[] decode(byte[] src) { + return base64Decoder.decode(src); + } + + public byte[] decode(String src) { + return base64Decoder.decode(src); + } + + public String urlEncodeToString(byte[] src) { + return base64UrlEncoder.encodeToString(src); + } + + public String urlEncodeToStringWithoutPadding(byte[] src) { + return base64UrlEncoder.withoutPadding().encodeToString(src); + } + + public byte[] urlDecode(byte[] src) { + return base64UrlDecoder.decode(src); + } + + public byte[] urlDecode(String src) { + return base64UrlDecoder.decode(src); + } +} \ No newline at end of file diff --git a/jans-fido2/common/src/main/java/io/jans/fido2/service/CoseService.java b/jans-fido2/common/src/main/java/io/jans/fido2/service/CoseService.java new file mode 100644 index 00000000000..a9289cdc136 --- /dev/null +++ b/jans-fido2/common/src/main/java/io/jans/fido2/service/CoseService.java @@ -0,0 +1,234 @@ +/* + * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +/* + * Copyright (c) 2018 Mastercard + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + */ + +package io.jans.fido2.service; + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.security.AlgorithmParameters; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.InvalidParameterSpecException; +import java.security.spec.RSAPublicKeySpec; +import java.util.Arrays; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.apache.commons.codec.binary.Hex; +import org.bouncycastle.asn1.sec.SECNamedCurves; +import org.bouncycastle.asn1.x9.X9ECParameters; +import io.jans.fido2.ctap.CoseEC2Algorithm; +import io.jans.fido2.ctap.CoseKeyType; +import io.jans.fido2.ctap.CoseRSAAlgorithm; +import io.jans.fido2.exception.Fido2RuntimeException; +import io.jans.as.model.exception.SignatureException; +import org.slf4j.Logger; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * Utility classes for COSE key structure. + * + */ +@ApplicationScoped +public class CoseService { + + private static final byte UNCOMPRESSED_POINT_INDICATOR = 0x04; + + @Inject + private Logger log; + + @Inject + private Base64Service base64Service; + + @Inject + private DataMapperService dataMapperService; + + private static String convertCoseCurveToSunCurveName(int curve) { + switch (curve) { + case 1: + return "secp256r1"; + default: + throw new Fido2RuntimeException("Unsupported curve"); + } + } + + public int getCodeCurve(JsonNode uncompressedECPointNode) { + return uncompressedECPointNode.get("-1").asInt(); + } + + public PublicKey createUncompressedPointFromCOSEPublicKey(JsonNode uncompressedECPointNode) { + int keyToUse = uncompressedECPointNode.get("1").asInt(); + int algorithmToUse = uncompressedECPointNode.get("3").asInt(); + CoseKeyType keyType = CoseKeyType.fromNumericValue(keyToUse); + + switch (keyType) { + case RSA: { + CoseRSAAlgorithm coseRSAAlgorithm = CoseRSAAlgorithm.fromNumericValue(algorithmToUse); + switch (coseRSAAlgorithm) { + case RS65535: + case RS256: { + byte[] rsaKey_n = base64Service.decode(uncompressedECPointNode.get("-1").asText()); + byte[] rsaKey_e = base64Service.decode(uncompressedECPointNode.get("-2").asText()); + return convertUncompressedPointToRSAKey(rsaKey_n, rsaKey_e); + } + default: { + throw new Fido2RuntimeException("Don't know what to do with this key" + keyType); + } + } + } + case EC2: { + CoseEC2Algorithm coseEC2Algorithm = CoseEC2Algorithm.fromNumericValue(algorithmToUse); + switch (coseEC2Algorithm) { + case ES256: { + int curve = uncompressedECPointNode.get("-1").asInt(); + byte[] x = base64Service.decode(uncompressedECPointNode.get("-2").asText()); + byte[] y = base64Service.decode(uncompressedECPointNode.get("-3").asText()); + byte[] buffer = ByteBuffer.allocate(1 + x.length + y.length).put(UNCOMPRESSED_POINT_INDICATOR).put(x).put(y).array(); + return convertUncompressedPointToECKey(buffer, curve); + } + default: { + throw new Fido2RuntimeException("Don't know what to do with this key" + keyType + " and algorithm " + coseEC2Algorithm); + } + } + } + case OKP: { + throw new Fido2RuntimeException("Don't know what to do with this key" + keyType); + } + default: + throw new Fido2RuntimeException("Don't know what to do with this key" + keyType); + } + } + + private PublicKey convertUncompressedPointToRSAKey(byte[] rsaKey_n, byte[] rsaKey_e) { + try { + BigInteger n = new BigInteger(1, rsaKey_n); + BigInteger e = new BigInteger(1, rsaKey_e); + RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(n, e); + final KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + return keyFactory.generatePublic(publicKeySpec); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + log.error("Problem here ", e); + throw new Fido2RuntimeException(e.getMessage()); + } + } + + public ECPublicKey convertUncompressedPointToECKey(final byte[] uncompressedPoint, int curve) { + AlgorithmParameters parameters = null; + try { + parameters = AlgorithmParameters.getInstance("EC"); + + parameters.init(new ECGenParameterSpec(convertCoseCurveToSunCurveName(curve))); + ECParameterSpec params = parameters.getParameterSpec(ECParameterSpec.class); + + int offset = 0; + if (uncompressedPoint[offset++] != UNCOMPRESSED_POINT_INDICATOR) { + throw new IllegalArgumentException("Invalid uncompressedPoint encoding, no uncompressed point indicator"); + } + + int keySizeBytes = (params.getOrder().bitLength() + Byte.SIZE - 1) / Byte.SIZE; + + if (uncompressedPoint.length != 1 + 2 * keySizeBytes) { + throw new IllegalArgumentException("Invalid uncompressedPoint encoding, not the correct size"); + } + + final BigInteger x = new BigInteger(1, Arrays.copyOfRange(uncompressedPoint, offset, offset + keySizeBytes)); + offset += keySizeBytes; + final BigInteger y = new BigInteger(1, Arrays.copyOfRange(uncompressedPoint, offset, offset + keySizeBytes)); + final ECPoint w = new ECPoint(x, y); + final ECPublicKeySpec ecPublicKeySpec = new ECPublicKeySpec(w, params); + final KeyFactory keyFactory = KeyFactory.getInstance("EC"); + return (ECPublicKey) keyFactory.generatePublic(ecPublicKeySpec); + } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidParameterSpecException e) { + throw new Fido2RuntimeException(e.getMessage()); + } + } + + public PublicKey getPublicKeyFromUncompressedECPoint(byte[] uncompressedECPointCOSEPubKey) { + JsonNode uncompressedECPointNode = null; + try { + uncompressedECPointNode = dataMapperService.cborReadTree(uncompressedECPointCOSEPubKey); + } catch (IOException e) { + throw new Fido2RuntimeException("Unable to parse the structure"); + } + log.debug("Uncompressed ECpoint node {}", uncompressedECPointNode.toString()); + PublicKey publicKey = createUncompressedPointFromCOSEPublicKey(uncompressedECPointNode); + log.debug("EC Public key hex {}", Hex.encodeHexString(publicKey.getEncoded())); + return publicKey; + } + + public JsonNode convertECKeyToUncompressedPoint(byte[] encodedPublicKey) { + X9ECParameters curve = SECNamedCurves.getByName("secp256r1"); + org.bouncycastle.math.ec.ECPoint point = curve.getCurve().decodePoint(encodedPublicKey); + int keySizeBytes = (curve.getN().bitLength() + Byte.SIZE - 1) / Byte.SIZE; + + ObjectNode uncompressedECPointNode = dataMapperService.createObjectNode(); + uncompressedECPointNode.put("1", 2); + uncompressedECPointNode.put("3", -7); + uncompressedECPointNode.put("-1", 1); + uncompressedECPointNode.put("-2", toUncompressedCoord(point.getAffineXCoord().toBigInteger().toByteArray(), keySizeBytes)); + uncompressedECPointNode.put("-3", toUncompressedCoord(point.getAffineYCoord().toBigInteger().toByteArray(), keySizeBytes)); + + return uncompressedECPointNode; + } + + public static byte[] toUncompressedCoord(final byte[] coord, int keySizeBytes) { + final byte[] uncompressedPoint = new byte[keySizeBytes]; + + if (coord.length <= keySizeBytes) { + return coord; + } else if ((coord.length == keySizeBytes + 1) && (coord[0] == 0)) { + System.arraycopy(coord, 1, uncompressedPoint, 0, keySizeBytes); + return uncompressedPoint; + } else { + throw new IllegalStateException("coord value is too large"); + } + } + + public PublicKey decodePublicKey(byte[] encodedPublicKey) throws SignatureException { + X9ECParameters curve = SECNamedCurves.getByName("secp256r1"); + org.bouncycastle.math.ec.ECPoint point = curve.getCurve().decodePoint(encodedPublicKey); + + try { + return KeyFactory.getInstance("ECDSA").generatePublic( + new org.bouncycastle.jce.spec.ECPublicKeySpec(point, + new org.bouncycastle.jce.spec.ECParameterSpec( + curve.getCurve(), + curve.getG(), + curve.getN(), + curve.getH() + ) + ) + ); + } catch (GeneralSecurityException ex) { + throw new SignatureException(ex); + } +} + +} diff --git a/jans-fido2/common/src/main/java/io/jans/fido2/service/DataMapperService.java b/jans-fido2/common/src/main/java/io/jans/fido2/service/DataMapperService.java new file mode 100644 index 00000000000..aa7df988bb2 --- /dev/null +++ b/jans-fido2/common/src/main/java/io/jans/fido2/service/DataMapperService.java @@ -0,0 +1,84 @@ +/* + * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +package io.jans.fido2.service; + +import java.io.BufferedReader; +import java.io.IOException; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.slf4j.Logger; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.dataformat.cbor.CBORFactory; +import com.fasterxml.jackson.dataformat.cbor.CBORParser; + +/** + * Conversions to/from JSON format and to/from CBOR format + * @author Yuriy Movchan + * @version May 08, 2020 + */ +@ApplicationScoped +public class DataMapperService { + + @Inject + private Logger log; + + private ObjectMapper objectMapper; + + private CBORFactory cborFactory; + private ObjectMapper cborObjectMapper; + + @PostConstruct + public void init() { + this.objectMapper = new ObjectMapper(); + this.cborFactory = new CBORFactory(); + this.cborObjectMapper = new ObjectMapper(cborFactory); + } + + public JsonNode readTree(byte[] content) throws IOException { + return objectMapper.readTree(content); + } + + public JsonNode readTree(String content) throws IOException { + return objectMapper.readTree(content); + } + + public JsonNode readTree(BufferedReader reader) throws IOException { + return objectMapper.readTree(reader); + } + + public ObjectNode createObjectNode() { + return objectMapper.createObjectNode(); + } + + public ArrayNode createArrayNode() { + return objectMapper.createArrayNode(); + } + + public JsonNode cborReadTree(byte[] content) throws IOException { + return cborObjectMapper.readTree(content); + } + + public byte[] cborWriteAsBytes(JsonNode jsonNode) throws IOException { + return cborObjectMapper.writeValueAsBytes(jsonNode); + } + + public CBORParser cborCreateParser(byte[] data) throws IOException { + return cborFactory.createParser(data); + } + + public T convertValue(Object fromValue, Class toValueType) { + return objectMapper.convertValue(fromValue, toValueType); + } + +} diff --git a/jans-fido2/common/src/main/java/io/jans/fido2/service/DeviceRegistrationService.java b/jans-fido2/common/src/main/java/io/jans/fido2/service/DeviceRegistrationService.java new file mode 100644 index 00000000000..fc65bce3c51 --- /dev/null +++ b/jans-fido2/common/src/main/java/io/jans/fido2/service/DeviceRegistrationService.java @@ -0,0 +1,203 @@ +/* + * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +package io.jans.fido2.service; + +import java.io.IOException; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +import io.jans.orm.model.fido2.Fido2RegistrationEntry; +import io.jans.orm.model.fido2.Fido2RegistrationStatus; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import io.jans.entry.DeviceRegistration; +import io.jans.entry.DeviceRegistrationStatus; +import io.jans.fido2.ctap.AttestationFormat; +import io.jans.fido2.ctap.CoseEC2Algorithm; +import io.jans.orm.model.fido2.Fido2RegistrationData; +import io.jans.fido2.service.Base64Service; +import io.jans.fido2.service.CoseService; +import io.jans.fido2.service.DataMapperService; +import io.jans.fido2.service.persist.RegistrationPersistenceService; +import io.jans.as.model.config.StaticConfiguration; + +/* + * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +import io.jans.as.common.service.common.UserService; +import io.jans.orm.PersistenceEntryManager; +import io.jans.orm.model.base.SimpleBranch; +import io.jans.orm.search.filter.Filter; +import io.jans.service.net.NetworkService; +import io.jans.util.StringHelper; +import org.slf4j.Logger; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Provides search operations with user U2F devices + * + * @author Yuriy Movchan Date: 05/27/2020 + */ +@ApplicationScoped +public class DeviceRegistrationService { + + @Inject + private Logger log; + + @Inject + private PersistenceEntryManager persistenceEntryManager; + + @Inject + private RegistrationPersistenceService registrationPersistenceService; + + @Inject + private UserService userService; + + @Inject + private StaticConfiguration staticConfiguration; + + @Inject + private NetworkService networkService; + + @Inject + private CoseService coseService; + + @Inject + private Base64Service base64Service; + + @Inject + private DataMapperService dataMapperService; + + public boolean containsBranch(final String baseDn) { + return persistenceEntryManager.contains(baseDn, SimpleBranch.class); + } + + public List findAllRegisteredByUsername(String username, String domain, String... returnAttributes) { + String userInum = userService.getUserInum(username); + if (userInum == null) { + return Collections.emptyList(); + } + + String baseDn = getBaseDnForU2fUserDevices(userInum); + + if (persistenceEntryManager.hasBranchesSupport(baseDn)) { + if (!containsBranch(baseDn)) { + return Collections.emptyList(); + } + } + + Filter resultFilter = Filter.createEqualityFilter("jansStatus", DeviceRegistrationStatus.ACTIVE.getValue()); + + List fidoRegistrations = persistenceEntryManager.findEntries(baseDn, DeviceRegistration.class, resultFilter, + returnAttributes); + + fidoRegistrations = fidoRegistrations.parallelStream() + .filter(f -> StringHelper.equals(domain, networkService.getHost(f.getApplication()))) + .filter(f -> (f.getDeviceData() == null)) /* Ignore Super Gluu */ + .collect(Collectors.toList()); + + return fidoRegistrations; + } + + public void migrateToFido2(List fidoRegistrations, String documentDomain, String username) { + for (DeviceRegistration fidoRegistration: fidoRegistrations) { + + Fido2RegistrationData fido2RegistrationData; + try { + fido2RegistrationData = convertToFido2RegistrationData(documentDomain, username, fidoRegistration); + } catch (IOException ex) { + log.error("Faield to migrate Fido to Fido2 device: {}" , fidoRegistration.getId()); + continue; + } + + // Save converted Fido2 entry + Date enrollmentDate = fidoRegistration.getCreationDate(); + Fido2RegistrationEntry fido2RegistrationEntry = registrationPersistenceService.buildFido2RegistrationEntry(fido2RegistrationData); + + // Restore dates modified by buildFido2RegistrationEntry + fido2RegistrationEntry.getRegistrationData().setCreatedDate(enrollmentDate); + fido2RegistrationEntry.setCreationDate(enrollmentDate); + + fido2RegistrationEntry.setDisplayName(fidoRegistration.getDisplayName()); + fido2RegistrationEntry.setPublicKeyId(fido2RegistrationData.getPublicKeyId()); + persistenceEntryManager.persist(fido2RegistrationEntry); + +// Testing code +// JsonNode uncompressedECPointNode; +// try { +// uncompressedECPointNode = dataMapperService.cborReadTree(base64Service.urlDecode(fido2RegistrationData.getUncompressedECPoint())); +// PublicKey publicKey = coseService.createUncompressedPointFromCOSEPublicKey(uncompressedECPointNode); +// } catch (IOException e) { +// e.printStackTrace(); +// } + + // Mark Fido registration entry as migrated + fidoRegistration.setStatus(DeviceRegistrationStatus.MIGRATED); + fidoRegistration.setDeletable(false); + + persistenceEntryManager.merge(fidoRegistration); + } + } + + protected Fido2RegistrationData convertToFido2RegistrationData(String documentDomain, String username, + DeviceRegistration fidoRegistration) throws IOException { + Fido2RegistrationData registrationData = new Fido2RegistrationData(); + + registrationData.setCreatedDate(fidoRegistration.getCreationDate()); + registrationData.setUpdatedDate(new Date()); + registrationData.setCreatedBy(username); + registrationData.setUpdatedBy(username); + + registrationData.setUsername(username); + registrationData.setDomain(documentDomain); + + JsonNode uncompressedECPoint = coseService.convertECKeyToUncompressedPoint( + base64Service.urlDecode(fidoRegistration.getDeviceRegistrationConfiguration().getPublicKey())); + registrationData.setUncompressedECPoint(base64Service.urlEncodeToString(dataMapperService.cborWriteAsBytes(uncompressedECPoint))); + + registrationData.setPublicKeyId(fidoRegistration.getKeyHandle()); + + registrationData.setCounter((int) fidoRegistration.getCounter()); + if (registrationData.getCounter() == -1) { + registrationData.setCounter(0); + } + + registrationData.setType("public-key"); + registrationData.setAttestationType(AttestationFormat.fido_u2f.getFmt()); + registrationData.setSignatureAlgorithm(CoseEC2Algorithm.ES256.getNumericValue()); + + registrationData.setStatus(Fido2RegistrationStatus.registered); + + registrationData.setApplicationId(fidoRegistration.getApplication()); + + return registrationData; + } + + /** + * Build DN string for U2F user device + */ + public String getDnForU2fDevice(String userInum, String jsId) { + String baseDnForU2fDevices = getBaseDnForU2fUserDevices(userInum); + if (StringHelper.isEmpty(jsId)) { + return baseDnForU2fDevices; + } + return String.format("jansId=%s,%s", jsId, baseDnForU2fDevices); + } + + public String getBaseDnForU2fUserDevices(String userInum) { + final String peopleDn = staticConfiguration.getBaseDn().getPeople(); + return String.format("ou=fido,inum=%s,%s", userInum, peopleDn); + } + +} diff --git a/jans-fido2/common/src/main/java/io/jans/fido2/service/RegistrationPersistenceService.java b/jans-fido2/common/src/main/java/io/jans/fido2/service/RegistrationPersistenceService.java new file mode 100644 index 00000000000..931a3459189 --- /dev/null +++ b/jans-fido2/common/src/main/java/io/jans/fido2/service/RegistrationPersistenceService.java @@ -0,0 +1,304 @@ +/* + * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +package io.jans.fido2.service.persist; + +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Optional; +import java.util.TimeZone; +import java.util.UUID; + +import io.jans.orm.model.fido2.Fido2RegistrationData; +import io.jans.orm.model.fido2.Fido2RegistrationEntry; +import io.jans.orm.model.fido2.Fido2RegistrationStatus; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.apache.commons.lang.StringUtils; +import io.jans.fido2.exception.Fido2RuntimeException; +import io.jans.fido2.model.conf.AppConfiguration; +import io.jans.fido2.service.shared.UserService; +import io.jans.as.common.model.common.User; +import io.jans.as.model.config.StaticConfiguration; +import io.jans.orm.PersistenceEntryManager; +import io.jans.orm.model.BatchOperation; +import io.jans.orm.model.ProcessBatchOperation; +import io.jans.orm.model.SearchScope; +import io.jans.orm.model.base.SimpleBranch; +import io.jans.orm.search.filter.Filter; +import io.jans.util.StringHelper; +import org.slf4j.Logger; + +/** + * Every registration is persisted under Person Entry + * @author Yuriy Movchan + * @version May 08, 2020 + */ +@ApplicationScoped +public class RegistrationPersistenceService { + + @Inject + private Logger log; + + @Inject + private StaticConfiguration staticConfiguration; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private UserService userService; + + @Inject + private PersistenceEntryManager persistenceEntryManager; + + public void save(Fido2RegistrationData registrationData) { + Fido2RegistrationEntry registrationEntry = buildFido2RegistrationEntry(registrationData); + + persistenceEntryManager.persist(registrationEntry); + } + + public Fido2RegistrationEntry buildFido2RegistrationEntry(Fido2RegistrationData registrationData) { + String userName = registrationData.getUsername(); + + User user = userService.getUser(userName, "inum"); + if (user == null) { + if (appConfiguration.getFido2Configuration().isUserAutoEnrollment()) { + user = userService.addDefaultUser(userName); + } else { + throw new Fido2RuntimeException("Auto user enrollment was disabled. User not exists!"); + } + } + String userInum = userService.getUserInum(user); + + prepareBranch(userInum); + + Date now = new GregorianCalendar(TimeZone.getTimeZone("UTC")).getTime(); + final String id = UUID.randomUUID().toString(); + final String challenge = registrationData.getChallenge(); + + String dn = getDnForRegistrationEntry(userInum, id); + Fido2RegistrationEntry registrationEntry = new Fido2RegistrationEntry(dn, id, now, userInum, registrationData, challenge); + registrationEntry.setRegistrationStatus(registrationData.getStatus()); + if (StringUtils.isNotEmpty(challenge)) { + registrationEntry.setChallangeHash(String.valueOf(getChallengeHashCode(challenge))); + } + + registrationData.setCreatedDate(now); + registrationData.setCreatedBy(userName); + + return registrationEntry; + } + + public void update(Fido2RegistrationEntry registrationEntry) { + Date now = new GregorianCalendar(TimeZone.getTimeZone("UTC")).getTime(); + + Fido2RegistrationData registrationData = registrationEntry.getRegistrationData(); + registrationData.setUpdatedDate(now); + registrationData.setUpdatedBy(registrationData.getUsername()); + + registrationEntry.setPublicKeyId(registrationData.getPublicKeyId()); + registrationEntry.setRegistrationStatus(registrationData.getStatus()); + + persistenceEntryManager.merge(registrationEntry); + } + + public void addBranch(final String baseDn) { + SimpleBranch branch = new SimpleBranch(); + branch.setOrganizationalUnitName("fido2_register"); + branch.setDn(baseDn); + + persistenceEntryManager.persist(branch); + } + + public boolean containsBranch(final String baseDn) { + return persistenceEntryManager.contains(baseDn, SimpleBranch.class); + } + + public void prepareBranch(final String userInum) { + String baseDn = getBaseDnForFido2RegistrationEntries(userInum); + if (!persistenceEntryManager.hasBranchesSupport(baseDn)) { + return; + } + + // Create Fido2 base branch for registration entries if needed + if (!containsBranch(baseDn)) { + addBranch(baseDn); + } + } + + public Optional findByPublicKeyId(String publicKeyId) { + String baseDn = getBaseDnForFido2RegistrationEntries(null); + + Filter publicKeyIdFilter = Filter.createEqualityFilter("jansPublicKeyId", publicKeyId); + List fido2RegistrationnEntries = persistenceEntryManager.findEntries(baseDn, Fido2RegistrationEntry.class, publicKeyIdFilter); + + if (fido2RegistrationnEntries.size() > 0) { + return Optional.of(fido2RegistrationnEntries.get(0)); + } + + return Optional.empty(); + } + + public List findAllByUsername(String username) { + String userInum = userService.getUserInum(username); + if (userInum == null) { + return Collections.emptyList(); + } + + String baseDn = getBaseDnForFido2RegistrationEntries(userInum); + if (persistenceEntryManager.hasBranchesSupport(baseDn)) { + if (!containsBranch(baseDn)) { + return Collections.emptyList(); + } + } + + Filter userFilter = Filter.createEqualityFilter("personInum", userInum); + + List fido2RegistrationnEntries = persistenceEntryManager.findEntries(baseDn, Fido2RegistrationEntry.class, userFilter); + + return fido2RegistrationnEntries; + } + + public List findAllRegisteredByUsername(String username) { + String userInum = userService.getUserInum(username); + if (userInum == null) { + return Collections.emptyList(); + } + + String baseDn = getBaseDnForFido2RegistrationEntries(userInum); + if (persistenceEntryManager.hasBranchesSupport(baseDn)) { + if (!containsBranch(baseDn)) { + return Collections.emptyList(); + } + } + + Filter userInumFilter = Filter.createEqualityFilter("personInum", userInum); + Filter registeredFilter = Filter.createEqualityFilter("jansStatus", Fido2RegistrationStatus.registered.getValue()); + Filter filter = Filter.createANDFilter(userInumFilter, registeredFilter); + + List fido2RegistrationnEntries = persistenceEntryManager.findEntries(baseDn, Fido2RegistrationEntry.class, filter); + + return fido2RegistrationnEntries; + } + + public List findByChallenge(String challenge) { + String baseDn = getBaseDnForFido2RegistrationEntries(null); + + Filter codeChallengFilter = Filter.createEqualityFilter("jansCodeChallenge", challenge); + Filter codeChallengHashCodeFilter = Filter.createEqualityFilter("jansCodeChallengeHash", String.valueOf(getChallengeHashCode(challenge))); + Filter filter = Filter.createANDFilter(codeChallengFilter, codeChallengHashCodeFilter); + + List fido2RegistrationnEntries = persistenceEntryManager.findEntries(baseDn, Fido2RegistrationEntry.class, filter); + + return fido2RegistrationnEntries; + } + + public String getDnForRegistrationEntry(String userInum, String jsId) { + // Build DN string for Fido2 registration entry + String baseDn = getBaseDnForFido2RegistrationEntries(userInum); + if (StringHelper.isEmpty(jsId)) { + return baseDn; + } + return String.format("jansId=%s,%s", jsId, baseDn); + } + + public String getBaseDnForFido2RegistrationEntries(String userInum) { + final String userBaseDn = getDnForUser(userInum); // "ou=fido2_register,inum=1234,ou=people,o=jans" + if (StringHelper.isEmpty(userInum)) { + return userBaseDn; + } + + return String.format("ou=fido2_register,%s", userBaseDn); + } + + public String getDnForUser(String userInum) { + String peopleDn = staticConfiguration.getBaseDn().getPeople(); + if (StringHelper.isEmpty(userInum)) { + return peopleDn; + } + + return String.format("inum=%s,%s", userInum, peopleDn); + } + + + public void cleanup(Date now, int batchSize) { + // Cleaning expired entries + BatchOperation cleanerRegistrationBatchService = new ProcessBatchOperation() { + @Override + public void performAction(List entries) { + for (Fido2RegistrationEntry p : entries) { + log.debug("Removing Fido2 registration entry: {}, Creation date: {}", p.getChallange(), p.getCreationDate()); + try { + persistenceEntryManager.remove(p); + } catch (Exception e) { + log.error("Failed to remove entry", e); + } + } + } + }; + String baseDn = getDnForUser(null); + persistenceEntryManager.findEntries(baseDn, Fido2RegistrationEntry.class, getExpiredRegistrationFilter(baseDn), SearchScope.SUB, new String[] {"jansCodeChallenge", "creationDate"}, cleanerRegistrationBatchService, 0, 0, batchSize); + + String branchDn = getDnForUser(null); + if (persistenceEntryManager.hasBranchesSupport(branchDn)) { + // Cleaning empty branches + BatchOperation cleanerBranchBatchService = new ProcessBatchOperation() { + @Override + public void performAction(List entries) { + for (SimpleBranch p : entries) { + try { + persistenceEntryManager.remove(p); + } catch (Exception e) { + log.error("Failed to remove entry", e); + } + } + } + }; + persistenceEntryManager.findEntries(branchDn, SimpleBranch.class, getEmptyRegistrationBranchFilter(), SearchScope.SUB, new String[] {"ou"}, cleanerBranchBatchService, 0, 0, batchSize); + } + } + + private Filter getExpiredRegistrationFilter(String baseDn) { + int unfinishedRequestExpiration = appConfiguration.getFido2Configuration().getUnfinishedRequestExpiration(); + unfinishedRequestExpiration = unfinishedRequestExpiration == 0 ? 120 : unfinishedRequestExpiration; + + Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + calendar.add(Calendar.SECOND, -unfinishedRequestExpiration); + final Date unfinishedRequestExpirationDate = calendar.getTime(); + + // Build unfinished request expiration filter + Filter registrationStatusFilter = Filter.createNOTFilter(Filter.createEqualityFilter("jansStatus", Fido2RegistrationStatus.registered.getValue())); + Filter compomisedStatusFilter = Filter.createNOTFilter(Filter.createEqualityFilter("jansStatus", Fido2RegistrationStatus.compromised.getValue())); + + Filter exirationDateFilter = Filter.createLessOrEqualFilter("creationDate", + persistenceEntryManager.encodeTime(baseDn, unfinishedRequestExpirationDate)); + + Filter unfinishedRequestFilter = Filter.createANDFilter(registrationStatusFilter, compomisedStatusFilter, exirationDateFilter); + + return unfinishedRequestFilter; + } + + private Filter getEmptyRegistrationBranchFilter() { + return Filter.createANDFilter(Filter.createEqualityFilter("ou", "fido2_register"), Filter.createORFilter( + Filter.createEqualityFilter("numsubordinates", "0"), Filter.createEqualityFilter("hasSubordinates", "FALSE"))); + } + + public int getChallengeHashCode(String challenge) { + int hash = 0; + byte[] challengeBytes = challenge.getBytes(); + for (int j = 0; j < challengeBytes.length; j++) { + hash += challengeBytes[j]*j; + } + + return hash; + } + +} diff --git a/jans-fido2/common/src/main/java/io/jans/fido2/service/shared/UserService.java b/jans-fido2/common/src/main/java/io/jans/fido2/service/shared/UserService.java new file mode 100644 index 00000000000..c00caf5b783 --- /dev/null +++ b/jans-fido2/common/src/main/java/io/jans/fido2/service/shared/UserService.java @@ -0,0 +1,45 @@ +/* + * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +package io.jans.fido2.service.shared; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import io.jans.fido2.model.conf.AppConfiguration; +import io.jans.as.common.util.AttributeConstants; +import io.jans.as.model.config.StaticConfiguration; + +/** + * Provides operations with users. + * + * @author Yuriy Movchan + * @version @version May 20, 2020 + */ +@ApplicationScoped +public class UserService extends io.jans.as.common.service.common.UserService { + + public static final String[] USER_OBJECT_CLASSES = new String[] { AttributeConstants.JANS_PERSON }; + + @Inject + private StaticConfiguration staticConfiguration; + + @Inject + private AppConfiguration appConfiguration; + + @Override + public List getPersonCustomObjectClassList() { + return appConfiguration.getPersonCustomObjectClassList(); + } + + @Override + public String getPeopleBaseDn() { + return staticConfiguration.getBaseDn().getPeople(); + } + +} diff --git a/jans-fido2/common/src/main/resources/META-INF/beans.xml b/jans-fido2/common/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000000..b7930c568e8 --- /dev/null +++ b/jans-fido2/common/src/main/resources/META-INF/beans.xml @@ -0,0 +1,6 @@ + + + diff --git a/jans-fido2/model/src/main/java/io/jans/fido2/model/error/Fido2RPError.java b/jans-fido2/model/src/main/java/io/jans/fido2/model/error/Fido2RPError.java new file mode 100644 index 00000000000..3a365b8959f --- /dev/null +++ b/jans-fido2/model/src/main/java/io/jans/fido2/model/error/Fido2RPError.java @@ -0,0 +1,42 @@ +/* + * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +/* + * Copyright (c) 2018 Mastercard + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + */ + +package io.jans.fido2.model.error; + +/** + * Error class for FIDO2 RP Errors + * + */ +public class Fido2RPError { + + private final String status; + private final String errorMessage; + + public String getStatus() { + return status; + } + + public String getErrorMessage() { + return errorMessage; + } + + public Fido2RPError(String status, String errorMessage) { + this.status = status; + this.errorMessage = errorMessage; + } +} diff --git a/jans-fido2/pom.xml b/jans-fido2/pom.xml index da48914c817..13f63adbe0c 100644 --- a/jans-fido2/pom.xml +++ b/jans-fido2/pom.xml @@ -72,6 +72,7 @@ model client + common server @@ -106,6 +107,16 @@ jans-fido2-model ${project.version} + + io.jans + jans-fido2-common + ${project.version} + + + io.jans + jans-orm-model + ${janssen.version} + diff --git a/jans-fido2/server/pom.xml b/jans-fido2/server/pom.xml index 8116f3c4595..ac4c7b0642f 100644 --- a/jans-fido2/server/pom.xml +++ b/jans-fido2/server/pom.xml @@ -99,6 +99,14 @@ io.jans jans-fido2-model + + io.jans + jans-fido2-common + + + io.jans + jans-orm-model + io.jans jans-orm-core