Bye bye Groovy, you won't be missed :(

This commit is contained in:
Maxime Dor
2017-09-25 02:31:31 +02:00
parent af19fed6e7
commit 33263d3cff
140 changed files with 1711 additions and 1678 deletions

View File

@@ -1,5 +1,3 @@
import java.util.regex.Pattern
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
@@ -20,7 +18,9 @@ import java.util.regex.Pattern
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
apply plugin: 'groovy'
import java.util.regex.Pattern
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
def confFileName = "application.example.yaml"
@@ -70,9 +70,6 @@ repositories {
}
dependencies {
// We are a groovy project
compile 'org.codehaus.groovy:groovy-all:2.4.7'
// Easy file management
compile 'commons-io:commons-io:2.5'

View File

@@ -1,193 +0,0 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program 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.
*
* This program 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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.backend.firebase
import com.google.firebase.FirebaseApp
import com.google.firebase.FirebaseOptions
import com.google.firebase.auth.*
import com.google.firebase.internal.NonNull
import com.google.firebase.tasks.OnFailureListener
import com.google.firebase.tasks.OnSuccessListener
import io.kamax.matrix.ThreePidMedium
import io.kamax.matrix._MatrixID
import io.kamax.mxisd.ThreePid
import io.kamax.mxisd.UserIdType
import io.kamax.mxisd.auth.provider.AuthenticatorProvider
import io.kamax.mxisd.auth.provider.BackendAuthResult
import org.apache.commons.lang.StringUtils
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.regex.Pattern
public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
private Logger log = LoggerFactory.getLogger(GoogleFirebaseAuthenticator.class);
private static final Pattern matrixIdLaxPattern = Pattern.compile("@(.*):(.+)"); // FIXME use matrix-java-sdk
private boolean isEnabled;
private String domain;
private FirebaseApp fbApp;
private FirebaseAuth fbAuth;
private void waitOnLatch(BackendAuthResult result, CountDownLatch l, long timeout, TimeUnit unit, String purpose) {
try {
l.await(timeout, unit);
} catch (InterruptedException e) {
log.warn("Interrupted while waiting for " + purpose);
result.failure();
}
}
public GoogleFirebaseAuthenticator(boolean isEnabled) {
this.isEnabled = isEnabled;
}
public GoogleFirebaseAuthenticator(String credsPath, String db, String domain) {
this(true);
this.domain = domain;
try {
fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db), "AuthenticationProvider");
fbAuth = FirebaseAuth.getInstance(fbApp);
log.info("Google Firebase Authentication is ready");
} catch (IOException e) {
throw new RuntimeException("Error when initializing Firebase", e);
}
}
private FirebaseCredential getCreds(String credsPath) throws IOException {
if (StringUtils.isNotBlank(credsPath)) {
return FirebaseCredentials.fromCertificate(new FileInputStream(credsPath));
} else {
return FirebaseCredentials.applicationDefault();
}
}
private FirebaseOptions getOpts(String credsPath, String db) throws IOException {
if (StringUtils.isBlank(db)) {
throw new IllegalArgumentException("Firebase database is not configured");
}
return new FirebaseOptions.Builder()
.setCredential(getCreds(credsPath))
.setDatabaseUrl(db)
.build();
}
@Override
public boolean isEnabled() {
return isEnabled;
}
private void waitOnLatch(CountDownLatch l) {
try {
l.await(30, TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.warn("Interrupted while waiting for Firebase auth check");
}
}
@Override
public BackendAuthResult authenticate(_MatrixID mxid, String password) {
if (!isEnabled()) {
throw new IllegalStateException();
}
log.info("Trying to authenticate {}", mxid);
BackendAuthResult result = BackendAuthResult.failure();
String localpart = m.group(1);
CountDownLatch l = new CountDownLatch(1);
fbAuth.verifyIdToken(password).addOnSuccessListener(new OnSuccessListener<FirebaseToken>() {
@Override
void onSuccess(FirebaseToken token) {
try {
if (!StringUtils.equals(localpart, token.getUid())) {
log.info("Failture to authenticate {}: Matrix ID localpart '{}' does not match Firebase UID '{}'", id, localpart, token.getUid());
result = BackendAuthResult.failure();
return;
}
result = BackendAuthResult.success(mxid.getId(), UserIdType.MatrixID, token.getName());
log.info("{} was successfully authenticated", mxid);
log.info("Fetching profile for {}", mxid);
CountDownLatch userRecordLatch = new CountDownLatch(1);
fbAuth.getUser(token.getUid()).addOnSuccessListener(new OnSuccessListener<UserRecord>() {
@Override
void onSuccess(UserRecord user) {
try {
if (StringUtils.isNotBlank(user.getEmail())) {
result.withThreePid(new ThreePid(ThreePidMedium.Email.getId(), user.getEmail()));
}
if (StringUtils.isNotBlank(user.getPhoneNumber())) {
result.withThreePid(new ThreePid(ThreePidMedium.PhoneNumber.getId(), user.getPhoneNumber()));
}
} finally {
userRecordLatch.countDown();
}
}
}).addOnFailureListener(new OnFailureListener() {
@Override
void onFailure(@NonNull Exception e) {
try {
log.warn("Unable to fetch Firebase user profile for {}", mxid);
result = BackendAuthResult.failure();
} finally {
userRecordLatch.countDown();
}
}
});
waitOnLatch(result, userRecordLatch, 30, TimeUnit.SECONDS, "Firebase user profile");
} finally {
l.countDown()
}
}
}).addOnFailureListener(new OnFailureListener() {
@Override
void onFailure(@NonNull Exception e) {
try {
if (e instanceof IllegalArgumentException) {
log.info("Failure to authenticate {}: invalid firebase token", mxid);
} else {
log.info("Failure to authenticate {}: {}", id, e.getMessage(), e);
log.info("Exception", e);
}
result = BackendAuthResult.failure();
} finally {
l.countDown()
}
}
});
waitOnLatch(result, l, 30, TimeUnit.SECONDS, "Firebase auth check");
return result;
}
}

View File

@@ -1,169 +0,0 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program 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.
*
* This program 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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.backend.ldap
import io.kamax.mxisd.config.MatrixConfig
import io.kamax.mxisd.lookup.SingleLookupReply
import io.kamax.mxisd.lookup.SingleLookupRequest
import io.kamax.mxisd.lookup.ThreePidMapping
import io.kamax.mxisd.lookup.provider.IThreePidProvider
import org.apache.commons.lang.StringUtils
import org.apache.directory.api.ldap.model.cursor.CursorLdapReferralException
import org.apache.directory.api.ldap.model.cursor.EntryCursor
import org.apache.directory.api.ldap.model.entry.Attribute
import org.apache.directory.api.ldap.model.entry.Entry
import org.apache.directory.api.ldap.model.message.SearchScope
import org.apache.directory.ldap.client.api.LdapConnection
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
@Component
class LdapThreePidProvider extends LdapGenericBackend implements IThreePidProvider {
public static final String UID = "uid"
public static final String MATRIX_ID = "mxid"
private Logger log = LoggerFactory.getLogger(LdapThreePidProvider.class)
@Autowired
private MatrixConfig mxCfg
@Override
boolean isEnabled() {
return getCfg().isEnabled()
}
private String getUidAttribute() {
return getCfg().getAttribute().getUid().getValue();
}
@Override
boolean isLocal() {
return true
}
@Override
int getPriority() {
return 20
}
Optional<String> lookup(LdapConnection conn, String medium, String value) {
String uidAttribute = getUidAttribute()
Optional<String> queryOpt = getCfg().getIdentity().getQuery(medium)
if (!queryOpt.isPresent()) {
log.warn("{} is not a configured 3PID type for LDAP lookup", medium)
return Optional.empty()
}
String searchQuery = queryOpt.get().replaceAll("%3pid", value)
EntryCursor cursor = conn.search(getCfg().getConn().getBaseDn(), searchQuery, SearchScope.SUBTREE, uidAttribute)
try {
while (cursor.next()) {
Entry entry = cursor.get()
log.info("Found possible match, DN: {}", entry.getDn().getName())
Attribute attribute = entry.get(uidAttribute)
if (attribute == null) {
log.info("DN {}: no attribute {}, skpping", entry.getDn(), getCfg().getAttribute())
continue
}
String data = attribute.get().toString()
if (data.length() < 1) {
log.info("DN {}: empty attribute {}, skipping", getCfg().getAttribute())
continue
}
StringBuilder matrixId = new StringBuilder()
// TODO Should we turn this block into a map of functions?
String uidType = getCfg().getAttribute().getUid().getType()
if (StringUtils.equals(UID, uidType)) {
matrixId.append("@").append(data).append(":").append(mxCfg.getDomain())
} else if (StringUtils.equals(MATRIX_ID, uidType)) {
matrixId.append(data)
} else {
log.warn("Bind was found but type {} is not supported", uidType)
continue
}
log.info("DN {} is a valid match", entry.getDn().getName())
return Optional.of(matrixId.toString())
}
} catch (CursorLdapReferralException e) {
log.warn("3PID {} is only available via referral, skipping", value)
} finally {
cursor.close()
}
return Optional.empty()
}
@Override
Optional<SingleLookupReply> find(SingleLookupRequest request) {
log.info("Performing LDAP lookup ${request.getThreePid()} of type ${request.getType()}")
LdapConnection conn = getConn()
try {
bind(conn)
Optional<String> mxid = lookup(conn, request.getType(), request.getThreePid())
if (mxid.isPresent()) {
return Optional.of(new SingleLookupReply(request, mxid.get()));
}
} finally {
conn.close()
}
log.info("No match found")
return Optional.empty()
}
@Override
List<ThreePidMapping> populate(List<ThreePidMapping> mappings) {
log.info("Looking up {} mappings", mappings.size())
List<ThreePidMapping> mappingsFound = new ArrayList<>()
LdapConnection conn = getConn()
try {
bind(conn)
for (ThreePidMapping mapping : mappings) {
try {
Optional<String> mxid = lookup(conn, mapping.getMedium(), mapping.getValue())
if (mxid.isPresent()) {
mapping.setMxid(mxid.get())
mappingsFound.add(mapping)
}
} catch (IllegalArgumentException e) {
log.warn("{} is not a supported 3PID type for LDAP lookup", mapping.getMedium())
}
}
} finally {
conn.close()
}
return mappingsFound
}
}

View File

@@ -1,83 +0,0 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program 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.
*
* This program 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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.InitializingBean
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration
@Configuration
@ConfigurationProperties(prefix = "lookup.recursive.bridge")
class RecursiveLookupBridgeConfig implements InitializingBean {
private Logger log = LoggerFactory.getLogger(RecursiveLookupBridgeConfig.class)
private boolean enabled
private boolean recursiveOnly
private String server
private Map<String, String> mappings = new HashMap<>()
boolean getEnabled() {
return enabled
}
void setEnabled(boolean enabled) {
this.enabled = enabled
}
boolean getRecursiveOnly() {
return recursiveOnly
}
void setRecursiveOnly(boolean recursiveOnly) {
this.recursiveOnly = recursiveOnly
}
String getServer() {
return server
}
void setServer(String server) {
this.server = server
}
Map<String, String> getMappings() {
return mappings
}
void setMappings(Map<String, String> mappings) {
this.mappings = mappings
}
@Override
void afterPropertiesSet() throws Exception {
log.info("--- Bridge integration lookups config ---")
log.info("Enabled: {}", getEnabled())
if (getEnabled()) {
log.info("Recursive only: {}", getRecursiveOnly())
log.info("Fallback Server: {}", getServer())
log.info("Mappings: {}", mappings.size())
}
}
}

View File

@@ -1,129 +0,0 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program 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.
*
* This program 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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config.ldap
import groovy.json.JsonOutput
import io.kamax.mxisd.backend.ldap.LdapThreePidProvider
import org.apache.commons.lang.StringUtils
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration
import javax.annotation.PostConstruct
@Configuration
@ConfigurationProperties(prefix = "ldap")
class LdapConfig {
private Logger log = LoggerFactory.getLogger(LdapConfig.class)
private boolean enabled
@Autowired
private LdapConnectionConfig conn
private LdapAttributeConfig attribute
private LdapAuthConfig auth
private LdapIdentityConfig identity
boolean isEnabled() {
return enabled
}
void setEnabled(boolean enabled) {
this.enabled = enabled
}
LdapConnectionConfig getConn() {
return conn
}
void setConn(LdapConnectionConfig conn) {
this.conn = conn
}
LdapAttributeConfig getAttribute() {
return attribute
}
void setAttribute(LdapAttributeConfig attribute) {
this.attribute = attribute
}
LdapAuthConfig getAuth() {
return auth
}
void setAuth(LdapAuthConfig auth) {
this.auth = auth
}
LdapIdentityConfig getIdentity() {
return identity
}
void setIdentity(LdapIdentityConfig identity) {
this.identity = identity
}
@PostConstruct
void afterPropertiesSet() {
log.info("--- LDAP Config ---")
log.info("Enabled: {}", isEnabled())
if (!isEnabled()) {
return
}
if (StringUtils.isBlank(conn.getHost())) {
throw new IllegalStateException("LDAP Host must be configured!")
}
if (1 > conn.getPort() || 65535 < conn.getPort()) {
throw new IllegalStateException("LDAP port is not valid")
}
if (StringUtils.isBlank(attribute.getUid().getType())) {
throw new IllegalStateException("Attribute UID Type cannot be empty")
}
if (StringUtils.isBlank(attribute.getUid().getValue())) {
throw new IllegalStateException("Attribute UID value cannot be empty")
}
String uidType = attribute.getUid().getType();
if (!StringUtils.equals(LdapThreePidProvider.UID, uidType) && !StringUtils.equals(LdapThreePidProvider.MATRIX_ID, uidType)) {
throw new IllegalArgumentException("Unsupported LDAP UID type: " + uidType)
}
log.info("Host: {}", conn.getHost())
log.info("Port: {}", conn.getPort())
log.info("Bind DN: {}", conn.getBindDn())
log.info("Base DN: {}", conn.getBaseDn())
log.info("Attribute: {}", JsonOutput.toJson(attribute))
log.info("Auth: {}", JsonOutput.toJson(auth))
log.info("Identity: {}", JsonOutput.toJson(identity))
}
}

View File

@@ -1,121 +0,0 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program 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.
*
* This program 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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.controller.v1
import com.google.gson.Gson
import com.google.gson.JsonObject
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import io.kamax.mxisd.controller.v1.io.SingeLookupReplyJson
import io.kamax.mxisd.lookup.*
import io.kamax.mxisd.lookup.strategy.LookupStrategy
import io.kamax.mxisd.signature.SignatureManager
import org.apache.commons.lang.StringUtils
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.CrossOrigin
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import javax.servlet.http.HttpServletRequest
import static org.springframework.web.bind.annotation.RequestMethod.GET
import static org.springframework.web.bind.annotation.RequestMethod.POST
@RestController
@CrossOrigin
@RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
class MappingController {
private Logger log = LoggerFactory.getLogger(MappingController.class)
private JsonSlurper json = new JsonSlurper()
private Gson gson = new Gson()
@Autowired
private LookupStrategy strategy
@Autowired
private SignatureManager signMgr
private void setRequesterInfo(ALookupRequest lookupReq, HttpServletRequest req) {
lookupReq.setRequester(req.getRemoteAddr())
String xff = req.getHeader("X-FORWARDED-FOR")
lookupReq.setRecursive(StringUtils.isNotBlank(xff))
if (lookupReq.isRecursive()) {
lookupReq.setRecurseHosts(Arrays.asList(xff.split(",")))
}
lookupReq.setUserAgent(req.getHeader("USER-AGENT"))
}
@RequestMapping(value = "/lookup", method = GET)
String lookup(HttpServletRequest request, @RequestParam String medium, @RequestParam String address) {
SingleLookupRequest lookupRequest = new SingleLookupRequest()
setRequesterInfo(lookupRequest, request)
lookupRequest.setType(medium)
lookupRequest.setThreePid(address)
log.info("Got single lookup request from {} with client {} - Is recursive? {}", lookupRequest.getRequester(), lookupRequest.getUserAgent(), lookupRequest.isRecursive())
Optional<SingleLookupReply> lookupOpt = strategy.find(lookupRequest)
if (!lookupOpt.isPresent()) {
log.info("No mapping was found, return empty JSON object")
return "{}"
}
SingleLookupReply lookup = lookupOpt.get()
if (lookup.isSigned()) {
log.info("Lookup is already signed, sending as-is")
return lookup.getBody();
} else {
log.info("Lookup is not signed, signing")
JsonObject obj = new Gson().toJsonTree(new SingeLookupReplyJson(lookup)).getAsJsonObject()
obj.add("signatures", signMgr.signMessageGson(gson.toJson(obj)))
return gson.toJson(obj)
}
}
@RequestMapping(value = "/bulk_lookup", method = POST)
String bulkLookup(HttpServletRequest request) {
BulkLookupRequest lookupRequest = new BulkLookupRequest()
setRequesterInfo(lookupRequest, request)
log.info("Got single lookup request from {} with client {} - Is recursive? {}", lookupRequest.getRequester(), lookupRequest.getUserAgent(), lookupRequest.isRecursive())
ClientBulkLookupRequest input = (ClientBulkLookupRequest) json.parseText(request.getInputStream().getText())
List<ThreePidMapping> mappings = new ArrayList<>()
for (List<String> mappingRaw : input.getThreepids()) {
ThreePidMapping mapping = new ThreePidMapping()
mapping.setMedium(mappingRaw.get(0))
mapping.setValue(mappingRaw.get(1))
mappings.add(mapping)
}
lookupRequest.setMappings(mappings)
ClientBulkLookupAnswer answer = new ClientBulkLookupAnswer()
answer.addAll(strategy.find(lookupRequest))
return JsonOutput.toJson(answer)
}
}

View File

@@ -1,106 +0,0 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program 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.
*
* This program 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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.key
import io.kamax.mxisd.config.KeyConfig
import net.i2p.crypto.eddsa.EdDSAEngine
import net.i2p.crypto.eddsa.EdDSAPrivateKey
import net.i2p.crypto.eddsa.EdDSAPublicKey
import net.i2p.crypto.eddsa.KeyPairGenerator
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable
import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec
import org.apache.commons.io.FileUtils
import org.springframework.beans.factory.InitializingBean
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.security.KeyPair
import java.security.MessageDigest
import java.security.PrivateKey
@Component
class KeyManager implements InitializingBean {
@Autowired
private KeyConfig keyCfg
private EdDSAParameterSpec keySpecs
private EdDSAEngine signEngine
private List<KeyPair> keys
@Override
void afterPropertiesSet() throws Exception {
keySpecs = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.CURVE_ED25519_SHA512)
signEngine = new EdDSAEngine(MessageDigest.getInstance(keySpecs.getHashAlgorithm()))
keys = new ArrayList<>()
Path privKey = Paths.get(keyCfg.getPath())
if (!Files.exists(privKey)) {
KeyPair pair = (new KeyPairGenerator()).generateKeyPair()
String keyEncoded = Base64.getEncoder().encodeToString(pair.getPrivate().getEncoded())
FileUtils.writeStringToFile(privKey.toFile(), keyEncoded, StandardCharsets.ISO_8859_1)
keys.add(pair)
} else {
if (Files.isDirectory(privKey)) {
throw new RuntimeException("Invalid path for private key: ${privKey.toString()}")
}
if (Files.isReadable(privKey)) {
byte[] seed = Base64.getDecoder().decode(FileUtils.readFileToString(privKey.toFile(), StandardCharsets.ISO_8859_1))
EdDSAPrivateKeySpec privKeySpec = new EdDSAPrivateKeySpec(seed, keySpecs)
EdDSAPublicKeySpec pubKeySpec = new EdDSAPublicKeySpec(privKeySpec.getA(), keySpecs)
keys.add(new KeyPair(new EdDSAPublicKey(pubKeySpec), new EdDSAPrivateKey(privKeySpec)))
}
}
}
int getCurrentIndex() {
return 0
}
KeyPair getKeys(int index) {
return keys.get(index)
}
PrivateKey getPrivateKey(int index) {
return getKeys(index).getPrivate()
}
EdDSAPublicKey getPublicKey(int index) {
return (EdDSAPublicKey) getKeys(index).getPublic()
}
EdDSAParameterSpec getSpecs() {
return keySpecs
}
String getPublicKeyBase64(int index) {
return Base64.getEncoder().encodeToString(getPublicKey(index).getAbyte())
}
}

View File

@@ -1,135 +0,0 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program 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.
*
* This program 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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.lookup.provider
import groovy.json.JsonException
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import io.kamax.mxisd.controller.v1.ClientBulkLookupRequest
import io.kamax.mxisd.lookup.SingleLookupReply
import io.kamax.mxisd.lookup.SingleLookupRequest
import io.kamax.mxisd.lookup.ThreePidMapping
import io.kamax.mxisd.lookup.fetcher.IRemoteIdentityServerFetcher
import io.kamax.mxisd.matrix.IdentityServerUtils
import org.apache.http.HttpEntity
import org.apache.http.HttpResponse
import org.apache.http.client.HttpClient
import org.apache.http.client.entity.EntityBuilder
import org.apache.http.client.methods.HttpPost
import org.apache.http.entity.ContentType
import org.apache.http.impl.client.HttpClients
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.context.annotation.Lazy
import org.springframework.context.annotation.Scope
import org.springframework.stereotype.Component
@Component
@Scope("prototype")
@Lazy
public class RemoteIdentityServerFetcher implements IRemoteIdentityServerFetcher {
private Logger log = LoggerFactory.getLogger(RemoteIdentityServerFetcher.class)
private JsonSlurper json = new JsonSlurper()
@Override
boolean isUsable(String remote) {
return IdentityServerUtils.isUsable(remote)
}
@Override
Optional<SingleLookupReply> find(String remote, SingleLookupRequest request) {
log.info("Looking up {} 3PID {} using {}", request.getType(), request.getThreePid(), remote)
HttpURLConnection rootSrvConn = (HttpURLConnection) new URL(
"${remote}/_matrix/identity/api/v1/lookup?medium=${request.getType()}&address=${request.getThreePid()}"
).openConnection()
try {
String outputRaw = rootSrvConn.getInputStream().getText()
def output = json.parseText(outputRaw)
if (output['address']) {
log.info("Found 3PID mapping: {}", output)
return Optional.of(SingleLookupReply.fromRecursive(request, outputRaw))
}
log.info("Empty 3PID mapping from {}", remote)
return Optional.empty()
} catch (IOException e) {
log.warn("Error looking up 3PID mapping {}: {}", request.getThreePid(), e.getMessage())
return Optional.empty()
} catch (JsonException e) {
log.warn("Invalid JSON answer from {}", remote)
return Optional.empty()
}
}
@Override
List<ThreePidMapping> find(String remote, List<ThreePidMapping> mappings) {
List<ThreePidMapping> mappingsFound = new ArrayList<>()
ClientBulkLookupRequest mappingRequest = new ClientBulkLookupRequest()
mappingRequest.setMappings(mappings)
String url = "${remote}/_matrix/identity/api/v1/bulk_lookup"
HttpClient client = HttpClients.createDefault()
try {
HttpPost request = new HttpPost(url)
request.setEntity(
EntityBuilder.create()
.setText(JsonOutput.toJson(mappingRequest))
.setContentType(ContentType.APPLICATION_JSON)
.build()
)
HttpResponse response = client.execute(request)
try {
if (response.getStatusLine().getStatusCode() != 200) {
log.info("Could not perform lookup at {} due to HTTP return code: {}", url, response.getStatusLine().getStatusCode())
return mappingsFound
}
HttpEntity entity = response.getEntity()
if (entity != null) {
ClientBulkLookupRequest input = (ClientBulkLookupRequest) json.parseText(entity.getContent().getText())
for (List<String> mappingRaw : input.getThreepids()) {
ThreePidMapping mapping = new ThreePidMapping()
mapping.setMedium(mappingRaw.get(0))
mapping.setValue(mappingRaw.get(1))
mapping.setMxid(mappingRaw.get(2))
mappingsFound.add(mapping)
}
} else {
log.info("HTTP response from {} was empty", remote)
}
return mappingsFound
} finally {
response.close()
}
} finally {
client.close()
}
}
}

View File

@@ -1,210 +0,0 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program 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.
*
* This program 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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.lookup.strategy
import edazdarevic.commons.net.CIDRUtils
import io.kamax.mxisd.config.RecursiveLookupConfig
import io.kamax.mxisd.lookup.*
import io.kamax.mxisd.lookup.fetcher.IBridgeFetcher
import io.kamax.mxisd.lookup.provider.IThreePidProvider
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.InitializingBean
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import java.util.function.Predicate
import java.util.stream.Collectors
@Component
class RecursivePriorityLookupStrategy implements LookupStrategy, InitializingBean {
private Logger log = LoggerFactory.getLogger(RecursivePriorityLookupStrategy.class)
@Autowired
private RecursiveLookupConfig recursiveCfg
@Autowired
private List<IThreePidProvider> providers
@Autowired
private IBridgeFetcher bridge
private List<CIDRUtils> allowedCidr = new ArrayList<>()
@Override
void afterPropertiesSet() throws Exception {
log.info("Found ${providers.size()} providers")
providers.sort(new Comparator<IThreePidProvider>() {
@Override
int compare(IThreePidProvider o1, IThreePidProvider o2) {
return Integer.compare(o2.getPriority(), o1.getPriority())
}
})
log.info("Recursive lookup enabled: {}", recursiveCfg.isEnabled())
for (String cidr : recursiveCfg.getAllowedCidr()) {
log.info("{} is allowed for recursion", cidr)
allowedCidr.add(new CIDRUtils(cidr))
}
}
boolean isAllowedForRecursive(String source) {
boolean canRecurse = false
if (recursiveCfg.isEnabled()) {
log.debug("Checking {} CIDRs for recursion", allowedCidr.size())
for (CIDRUtils cidr : allowedCidr) {
if (cidr.isInRange(source)) {
log.debug("{} is in range {}, allowing recursion", source, cidr.getNetworkAddress())
canRecurse = true
break
} else {
log.debug("{} is not in range {}", source, cidr.getNetworkAddress())
}
}
}
return canRecurse
}
List<IThreePidProvider> listUsableProviders(ALookupRequest request) {
return listUsableProviders(request, false);
}
List<IThreePidProvider> listUsableProviders(ALookupRequest request, boolean forceRecursive) {
List<IThreePidProvider> usableProviders = new ArrayList<>()
boolean canRecurse = forceRecursive || isAllowedForRecursive(request.getRequester())
log.info("Host {} allowed for recursion: {}", request.getRequester(), canRecurse)
for (IThreePidProvider provider : providers) {
if (provider.isEnabled() && (provider.isLocal() || canRecurse || forceRecursive)) {
usableProviders.add(provider)
}
}
return usableProviders
}
@Override
List<IThreePidProvider> getLocalProviders() {
return providers.stream().filter(new Predicate<IThreePidProvider>() {
@Override
boolean test(IThreePidProvider iThreePidProvider) {
return iThreePidProvider.isEnabled() && iThreePidProvider.isLocal()
}
}).collect(Collectors.toList())
}
List<IThreePidProvider> getRemoteProviders() {
return providers.stream().filter(new Predicate<IThreePidProvider>() {
@Override
boolean test(IThreePidProvider iThreePidProvider) {
return iThreePidProvider.isEnabled() && !iThreePidProvider.isLocal()
}
}).collect(Collectors.toList())
}
private static SingleLookupRequest build(String medium, String address) {
SingleLookupRequest req = new SingleLookupRequest();
req.setType(medium)
req.setThreePid(address)
req.setRequester("Internal")
return req;
}
@Override
Optional<SingleLookupReply> find(String medium, String address, boolean recursive) {
return find(build(medium, address), recursive)
}
@Override
Optional<SingleLookupReply> findLocal(String medium, String address) {
return find(build(medium, address), getLocalProviders())
}
@Override
Optional<SingleLookupReply> findRemote(String medium, String address) {
return find(build(medium, address), getRemoteProviders())
}
Optional<SingleLookupReply> find(SingleLookupRequest request, boolean forceRecursive) {
return find(request, listUsableProviders(request, forceRecursive));
}
Optional<SingleLookupReply> find(SingleLookupRequest request, List<IThreePidProvider> providers) {
for (IThreePidProvider provider : providers) {
Optional<SingleLookupReply> lookupDataOpt = provider.find(request)
if (lookupDataOpt.isPresent()) {
return lookupDataOpt
}
}
if (
recursiveCfg.getBridge() != null &&
recursiveCfg.getBridge().getEnabled() &&
(!recursiveCfg.getBridge().getRecursiveOnly() || isAllowedForRecursive(request.getRequester()))
) {
log.info("Using bridge failover for lookup")
return bridge.find(request)
}
return Optional.empty()
}
@Override
Optional<SingleLookupReply> find(SingleLookupRequest request) {
return find(request, false)
}
@Override
Optional<SingleLookupReply> findRecursive(SingleLookupRequest request) {
return find(request, true)
}
@Override
List<ThreePidMapping> find(BulkLookupRequest request) {
List<ThreePidMapping> mapToDo = new ArrayList<>(request.getMappings())
List<ThreePidMapping> mapFoundAll = new ArrayList<>()
for (IThreePidProvider provider : listUsableProviders(request)) {
if (mapToDo.isEmpty()) {
log.info("No more mappings to lookup")
break
} else {
log.info("{} mappings remaining overall", mapToDo.size())
}
log.info("Using provider {} for remaining mappings", provider.getClass().getSimpleName())
List<ThreePidMapping> mapFound = provider.populate(mapToDo)
log.info("Provider {} returned {} mappings", provider.getClass().getSimpleName(), mapFound.size())
mapFoundAll.addAll(mapFound)
mapToDo.removeAll(mapFound)
}
return mapFoundAll
}
}

View File

@@ -1,78 +0,0 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program 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.
*
* This program 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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.signature
import com.google.gson.JsonObject
import io.kamax.mxisd.config.ServerConfig
import io.kamax.mxisd.key.KeyManager
import net.i2p.crypto.eddsa.EdDSAEngine
import org.json.JSONObject
import org.springframework.beans.factory.InitializingBean
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import java.security.MessageDigest
@Component
class SignatureManager implements InitializingBean {
@Autowired
private KeyManager keyMgr
@Autowired
private ServerConfig srvCfg
private EdDSAEngine signEngine
private String sign(String message) {
byte[] signRaw = signEngine.signOneShot(message.getBytes())
return Base64.getEncoder().encodeToString(signRaw)
}
JSONObject signMessageJson(String message) {
String sign = sign(message)
JSONObject keySignature = new JSONObject()
keySignature.put("ed25519:${keyMgr.getCurrentIndex()}", sign)
JSONObject signature = new JSONObject()
signature.put("${srvCfg.getName()}", keySignature)
return signature
}
JsonObject signMessageGson(String message) {
String sign = sign(message)
JsonObject keySignature = new JsonObject()
keySignature.addProperty("ed25519:${keyMgr.getCurrentIndex()}", sign)
JsonObject signature = new JsonObject()
signature.add("${srvCfg.getName()}", keySignature);
return signature
}
@Override
void afterPropertiesSet() throws Exception {
signEngine = new EdDSAEngine(MessageDigest.getInstance(keyMgr.getSpecs().getHashAlgorithm()))
signEngine.initSign(keyMgr.getPrivateKey(keyMgr.getCurrentIndex()))
}
}

View File

@@ -18,16 +18,16 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd
package io.kamax.mxisd;
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
class MatrixIdentityServerApplication {
static void main(String[] args) throws Exception {
SpringApplication.run(MatrixIdentityServerApplication.class, args)
public static void main(String[] args) {
SpringApplication.run(MatrixIdentityServerApplication.class, args);
}
}

View File

@@ -49,20 +49,27 @@ public class BackendAuthResult {
return r;
}
public void fail() {
success = false;
}
public static BackendAuthResult success(String id, UserIdType type, String displayName) {
return success(id, type.getId(), displayName);
}
public static BackendAuthResult success(String id, String type, String displayName) {
BackendAuthResult r = new BackendAuthResult();
r.success = true;
r.id = new UserID(type, id);
r.profile = new BackendAuthProfile();
r.profile.displayName = displayName;
r.succeed(id, type, displayName);
return r;
}
public void succeed(String id, String type, String displayName) {
this.success = true;
this.id = new UserID(type, id);
this.profile = new BackendAuthProfile();
this.profile.displayName = displayName;
}
private Boolean success;
private UserID id;
private BackendAuthProfile profile = new BackendAuthProfile();

View File

@@ -0,0 +1,177 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program 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.
*
* This program 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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.backend.firebase;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseCredential;
import com.google.firebase.auth.FirebaseCredentials;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.matrix._MatrixID;
import io.kamax.mxisd.ThreePid;
import io.kamax.mxisd.UserIdType;
import io.kamax.mxisd.auth.provider.AuthenticatorProvider;
import io.kamax.mxisd.auth.provider.BackendAuthResult;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
private Logger log = LoggerFactory.getLogger(GoogleFirebaseAuthenticator.class);
private boolean isEnabled;
private FirebaseApp fbApp;
private FirebaseAuth fbAuth;
private void waitOnLatch(BackendAuthResult result, CountDownLatch l, String purpose) {
try {
l.await(30, TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.warn("Interrupted while waiting for " + purpose);
result.fail();
}
}
public GoogleFirebaseAuthenticator(boolean isEnabled) {
this.isEnabled = isEnabled;
}
public GoogleFirebaseAuthenticator(String credsPath, String db) {
this(true);
try {
fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db), "AuthenticationProvider");
fbAuth = FirebaseAuth.getInstance(fbApp);
log.info("Google Firebase Authentication is ready");
} catch (IOException e) {
throw new RuntimeException("Error when initializing Firebase", e);
}
}
private FirebaseCredential getCreds(String credsPath) throws IOException {
if (StringUtils.isNotBlank(credsPath)) {
return FirebaseCredentials.fromCertificate(new FileInputStream(credsPath));
} else {
return FirebaseCredentials.applicationDefault();
}
}
private FirebaseOptions getOpts(String credsPath, String db) throws IOException {
if (StringUtils.isBlank(db)) {
throw new IllegalArgumentException("Firebase database is not configured");
}
return new FirebaseOptions.Builder()
.setCredential(getCreds(credsPath))
.setDatabaseUrl(db)
.build();
}
@Override
public boolean isEnabled() {
return isEnabled;
}
private void waitOnLatch(CountDownLatch l) {
try {
l.await(30, TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.warn("Interrupted while waiting for Firebase auth check");
}
}
@Override
public BackendAuthResult authenticate(_MatrixID mxid, String password) {
if (!isEnabled()) {
throw new IllegalStateException();
}
log.info("Trying to authenticate {}", mxid);
final BackendAuthResult result = BackendAuthResult.failure();
String localpart = mxid.getLocalPart();
CountDownLatch l = new CountDownLatch(1);
fbAuth.verifyIdToken(password).addOnSuccessListener(token -> {
try {
if (!StringUtils.equals(localpart, token.getUid())) {
log.info("Failure to authenticate {}: Matrix ID localpart '{}' does not match Firebase UID '{}'", mxid, localpart, token.getUid());
result.fail();
return;
}
result.succeed(mxid.getId(), UserIdType.MatrixID.getId(), token.getName());
log.info("{} was successfully authenticated", mxid);
log.info("Fetching profile for {}", mxid);
CountDownLatch userRecordLatch = new CountDownLatch(1);
fbAuth.getUser(token.getUid()).addOnSuccessListener(user -> {
try {
if (StringUtils.isNotBlank(user.getEmail())) {
result.withThreePid(new ThreePid(ThreePidMedium.Email.getId(), user.getEmail()));
}
if (StringUtils.isNotBlank(user.getPhoneNumber())) {
result.withThreePid(new ThreePid(ThreePidMedium.PhoneNumber.getId(), user.getPhoneNumber()));
}
} finally {
userRecordLatch.countDown();
}
}).addOnFailureListener(e -> {
try {
log.warn("Unable to fetch Firebase user profile for {}", mxid);
result.fail();
} finally {
userRecordLatch.countDown();
}
});
waitOnLatch(result, userRecordLatch, "Firebase user profile");
} finally {
l.countDown();
}
}).addOnFailureListener(e -> {
try {
if (e instanceof IllegalArgumentException) {
log.info("Failure to authenticate {}: invalid firebase token", mxid);
} else {
log.info("Failure to authenticate {}: {}", mxid, e.getMessage(), e);
log.info("Exception", e);
}
result.fail();
} finally {
l.countDown();
}
});
waitOnLatch(result, l, "Firebase auth check");
return result;
}
}

View File

@@ -18,40 +18,40 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.backend.firebase
package io.kamax.mxisd.backend.firebase;
import com.google.firebase.FirebaseApp
import com.google.firebase.FirebaseOptions
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseCredential
import com.google.firebase.auth.FirebaseCredentials
import com.google.firebase.auth.UserRecord
import com.google.firebase.internal.NonNull
import com.google.firebase.tasks.OnFailureListener
import com.google.firebase.tasks.OnSuccessListener
import io.kamax.matrix.ThreePidMedium
import io.kamax.mxisd.lookup.SingleLookupReply
import io.kamax.mxisd.lookup.SingleLookupRequest
import io.kamax.mxisd.lookup.ThreePidMapping
import io.kamax.mxisd.lookup.provider.IThreePidProvider
import org.apache.commons.lang.StringUtils
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseCredential;
import com.google.firebase.auth.FirebaseCredentials;
import com.google.firebase.auth.UserRecord;
import com.google.firebase.tasks.OnFailureListener;
import com.google.firebase.tasks.OnSuccessListener;
import io.kamax.matrix.MatrixID;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.lookup.SingleLookupRequest;
import io.kamax.mxisd.lookup.ThreePidMapping;
import io.kamax.mxisd.lookup.provider.IThreePidProvider;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.function.Consumer
import java.util.regex.Pattern
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class GoogleFirebaseProvider implements IThreePidProvider {
private Logger log = LoggerFactory.getLogger(GoogleFirebaseProvider.class);
private static final Pattern matrixIdLaxPattern = Pattern.compile("@(.*):(.+)");
private boolean isEnabled;
private String domain;
private FirebaseApp fbApp;
private FirebaseAuth fbAuth;
public GoogleFirebaseProvider(boolean isEnabled) {
@@ -61,8 +61,9 @@ public class GoogleFirebaseProvider implements IThreePidProvider {
public GoogleFirebaseProvider(String credsPath, String db, String domain) {
this(true);
this.domain = domain;
try {
fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db), "ThreePidProvider");
FirebaseApp fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db), "ThreePidProvider");
fbAuth = FirebaseAuth.getInstance(fbApp);
log.info("Google Firebase Authentication is ready");
@@ -91,7 +92,7 @@ public class GoogleFirebaseProvider implements IThreePidProvider {
}
private String getMxid(UserRecord record) {
return "@${record.getUid()}:${domain}";
return new MatrixID(record.getUid(), domain).getId();
}
@Override
@@ -118,71 +119,59 @@ public class GoogleFirebaseProvider implements IThreePidProvider {
}
private Optional<UserRecord> findInternal(String medium, String address) {
UserRecord r;
final UserRecord[] r = new UserRecord[1];
CountDownLatch l = new CountDownLatch(1);
OnSuccessListener<UserRecord> success = new OnSuccessListener<UserRecord>() {
@Override
void onSuccess(UserRecord result) {
log.info("Found 3PID match for {}:{} - UID is {}", medium, address, result.getUid())
r = result;
l.countDown()
}
OnSuccessListener<UserRecord> success = result -> {
log.info("Found 3PID match for {}:{} - UID is {}", medium, address, result.getUid());
r[0] = result;
l.countDown();
};
OnFailureListener failure = new OnFailureListener() {
@Override
void onFailure(@NonNull Exception e) {
log.info("No 3PID match for {}:{} - {}", medium, address, e.getMessage())
r = null;
l.countDown()
}
OnFailureListener failure = e -> {
log.info("No 3PID match for {}:{} - {}", medium, address, e.getMessage());
r[0] = null;
l.countDown();
};
if (ThreePidMedium.Email.is(medium)) {
log.info("Performing E-mail 3PID lookup for {}", address)
log.info("Performing E-mail 3PID lookup for {}", address);
fbAuth.getUserByEmail(address)
.addOnSuccessListener(success)
.addOnFailureListener(failure);
waitOnLatch(l);
} else if (ThreePidMedium.PhoneNumber.is(medium)) {
log.info("Performing msisdn 3PID lookup for {}", address)
log.info("Performing msisdn 3PID lookup for {}", address);
fbAuth.getUserByPhoneNumber(address)
.addOnSuccessListener(success)
.addOnFailureListener(failure);
waitOnLatch(l);
} else {
log.info("{} is not a supported 3PID medium", medium);
r = null;
r[0] = null;
}
return Optional.ofNullable(r);
return Optional.ofNullable(r[0]);
}
@Override
public Optional<SingleLookupReply> find(SingleLookupRequest request) {
Optional<UserRecord> urOpt = findInternal(request.getType(), request.getThreePid())
if (urOpt.isPresent()) {
return Optional.of(new SingleLookupReply(request, getMxid(urOpt.get())));
}
Optional<UserRecord> urOpt = findInternal(request.getType(), request.getThreePid());
return urOpt.map(userRecord -> new SingleLookupReply(request, getMxid(userRecord)));
return Optional.empty();
}
@Override
public List<ThreePidMapping> populate(List<ThreePidMapping> mappings) {
List<ThreePidMapping> results = new ArrayList<>();
mappings.parallelStream().forEach(new Consumer<ThreePidMapping>() {
@Override
void accept(ThreePidMapping o) {
Optional<UserRecord> urOpt = findInternal(o.getMedium(), o.getValue());
if (urOpt.isPresent()) {
ThreePidMapping result = new ThreePidMapping();
result.setMedium(o.getMedium())
result.setValue(o.getValue())
result.setMxid(getMxid(urOpt.get()))
results.add(result)
}
mappings.parallelStream().forEach(o -> {
Optional<UserRecord> urOpt = findInternal(o.getMedium(), o.getValue());
if (urOpt.isPresent()) {
ThreePidMapping result = new ThreePidMapping();
result.setMedium(o.getMedium());
result.setValue(o.getValue());
result.setMxid(getMxid(urOpt.get()));
results.add(result);
}
});
return results;

View File

@@ -0,0 +1,174 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program 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.
*
* This program 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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.backend.ldap;
import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.lookup.SingleLookupRequest;
import io.kamax.mxisd.lookup.ThreePidMapping;
import io.kamax.mxisd.lookup.provider.IThreePidProvider;
import org.apache.commons.lang.StringUtils;
import org.apache.directory.api.ldap.model.cursor.CursorException;
import org.apache.directory.api.ldap.model.cursor.CursorLdapReferralException;
import org.apache.directory.api.ldap.model.cursor.EntryCursor;
import org.apache.directory.api.ldap.model.entry.Attribute;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.message.SearchScope;
import org.apache.directory.ldap.client.api.LdapConnection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Component
public class LdapThreePidProvider extends LdapGenericBackend implements IThreePidProvider {
public static final String UID = "uid";
public static final String MATRIX_ID = "mxid";
private Logger log = LoggerFactory.getLogger(LdapThreePidProvider.class);
@Autowired
private MatrixConfig mxCfg;
@Override
public boolean isEnabled() {
return getCfg().isEnabled();
}
private String getUidAttribute() {
return getCfg().getAttribute().getUid().getValue();
}
@Override
public boolean isLocal() {
return true;
}
@Override
public int getPriority() {
return 20;
}
private Optional<String> lookup(LdapConnection conn, String medium, String value) {
String uidAttribute = getUidAttribute();
Optional<String> queryOpt = getCfg().getIdentity().getQuery(medium);
if (!queryOpt.isPresent()) {
log.warn("{} is not a configured 3PID type for LDAP lookup", medium);
return Optional.empty();
}
String searchQuery = queryOpt.get().replaceAll("%3pid", value);
try (EntryCursor cursor = conn.search(getCfg().getConn().getBaseDn(), searchQuery, SearchScope.SUBTREE, uidAttribute)) {
while (cursor.next()) {
Entry entry = cursor.get();
log.info("Found possible match, DN: {}", entry.getDn().getName());
Attribute attribute = entry.get(uidAttribute);
if (attribute == null) {
log.info("DN {}: no attribute {}, skpping", entry.getDn(), getCfg().getAttribute());
continue;
}
String data = attribute.get().toString();
if (data.length() < 1) {
log.info("DN {}: empty attribute {}, skipping", getCfg().getAttribute());
continue;
}
StringBuilder matrixId = new StringBuilder();
// TODO Should we turn this block into a map of functions?
String uidType = getCfg().getAttribute().getUid().getType();
if (StringUtils.equals(UID, uidType)) {
matrixId.append("@").append(data).append(":").append(mxCfg.getDomain());
} else if (StringUtils.equals(MATRIX_ID, uidType)) {
matrixId.append(data);
} else {
log.warn("Bind was found but type {} is not supported", uidType);
continue;
}
log.info("DN {} is a valid match", entry.getDn().getName());
return Optional.of(matrixId.toString());
}
} catch (CursorLdapReferralException e) {
log.warn("3PID {} is only available via referral, skipping", value);
} catch (IOException | LdapException | CursorException e) {
throw new InternalServerError(e);
}
return Optional.empty();
}
@Override
public Optional<SingleLookupReply> find(SingleLookupRequest request) {
log.info("Performing LDAP lookup ${request.getThreePid()} of type ${request.getType()}");
try (LdapConnection conn = getConn()) {
bind(conn);
Optional<String> mxid = lookup(conn, request.getType(), request.getThreePid());
if (mxid.isPresent()) {
return Optional.of(new SingleLookupReply(request, mxid.get()));
}
} catch (LdapException | IOException e) {
throw new InternalServerError(e);
}
log.info("No match found");
return Optional.empty();
}
@Override
public List<ThreePidMapping> populate(List<ThreePidMapping> mappings) {
log.info("Looking up {} mappings", mappings.size());
List<ThreePidMapping> mappingsFound = new ArrayList<>();
try (LdapConnection conn = getConn()) {
bind(conn);
for (ThreePidMapping mapping : mappings) {
try {
Optional<String> mxid = lookup(conn, mapping.getMedium(), mapping.getValue());
if (mxid.isPresent()) {
mapping.setMxid(mxid.get());
mappingsFound.add(mapping);
}
} catch (IllegalArgumentException e) {
log.warn("{} is not a supported 3PID type for LDAP lookup", mapping.getMedium());
}
}
} catch (LdapException | IOException e) {
throw new InternalServerError(e);
}
return mappingsFound;
}
}

View File

@@ -85,7 +85,7 @@ public class FirebaseConfig {
if (!enabled) {
return new GoogleFirebaseAuthenticator(false);
} else {
return new GoogleFirebaseAuthenticator(credentials, database, mxCfg.getDomain());
return new GoogleFirebaseAuthenticator(credentials, database);
}
}

View File

@@ -18,23 +18,26 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config
package io.kamax.mxisd.config;
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
@Configuration
@ConfigurationProperties(prefix = "forward")
class ForwardConfig {
public class ForwardConfig {
private List<String> servers = new ArrayList<>()
private List<String> servers = new ArrayList<>();
List<String> getServers() {
return servers
public List<String> getServers() {
return servers;
}
void setServers(List<String> servers) {
this.servers = servers
public void setServers(List<String> servers) {
this.servers = servers;
}
}

View File

@@ -18,32 +18,33 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config
package io.kamax.mxisd.config;
import io.kamax.mxisd.exception.ConfigurationException
import org.apache.commons.lang.StringUtils
import org.springframework.beans.factory.InitializingBean
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration
import io.kamax.mxisd.exception.ConfigurationException;
import org.apache.commons.lang.StringUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Configuration
@ConfigurationProperties(prefix = "key")
class KeyConfig implements InitializingBean {
public class KeyConfig {
private String path
private String path;
void setPath(String path) {
this.path = path
public void setPath(String path) {
this.path = path;
}
String getPath() {
return path
public String getPath() {
return path;
}
@Override
void afterPropertiesSet() throws Exception {
@PostConstruct
public void build() {
if (StringUtils.isBlank(getPath())) {
throw new ConfigurationException("key.path")
throw new ConfigurationException("key.path");
}
}

View File

@@ -0,0 +1,86 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program 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.
*
* This program 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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
@Configuration
@ConfigurationProperties(prefix = "lookup.recursive.bridge")
public class RecursiveLookupBridgeConfig {
private Logger log = LoggerFactory.getLogger(RecursiveLookupBridgeConfig.class);
private boolean enabled;
private boolean recursiveOnly;
private String server;
private Map<String, String> mappings = new HashMap<>();
public boolean getEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean getRecursiveOnly() {
return recursiveOnly;
}
public void setRecursiveOnly(boolean recursiveOnly) {
this.recursiveOnly = recursiveOnly;
}
public String getServer() {
return server;
}
public void setServer(String server) {
this.server = server;
}
public Map<String, String> getMappings() {
return mappings;
}
public void setMappings(Map<String, String> mappings) {
this.mappings = mappings;
}
@PostConstruct
public void build() {
log.info("--- Bridge integration lookups config ---");
log.info("Enabled: {}", getEnabled());
if (getEnabled()) {
log.info("Recursive only: {}", getRecursiveOnly());
log.info("Fallback Server: {}", getServer());
log.info("Mappings: {}", mappings.size());
}
}
}

View File

@@ -18,41 +18,43 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config
package io.kamax.mxisd.config;
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration
@ConfigurationProperties(prefix = "lookup.recursive")
class RecursiveLookupConfig {
public class RecursiveLookupConfig {
private boolean enabled
private List<String> allowedCidr
private RecursiveLookupBridgeConfig bridge
private boolean enabled;
private List<String> allowedCidr;
private RecursiveLookupBridgeConfig bridge;
boolean isEnabled() {
return enabled
public boolean isEnabled() {
return enabled;
}
void setEnabled(boolean enabled) {
this.enabled = enabled
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
List<String> getAllowedCidr() {
return allowedCidr
public List<String> getAllowedCidr() {
return allowedCidr;
}
void setAllowedCidr(List<String> allowedCidr) {
this.allowedCidr = allowedCidr
public void setAllowedCidr(List<String> allowedCidr) {
this.allowedCidr = allowedCidr;
}
RecursiveLookupBridgeConfig getBridge() {
return bridge
public RecursiveLookupBridgeConfig getBridge() {
return bridge;
}
void setBridge(RecursiveLookupBridgeConfig bridge) {
this.bridge = bridge
public void setBridge(RecursiveLookupBridgeConfig bridge) {
this.bridge = bridge;
}
}

View File

@@ -18,56 +18,59 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config
package io.kamax.mxisd.config;
import org.apache.commons.lang.StringUtils
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.InitializingBean
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.net.MalformedURLException;
import java.net.URL;
@Configuration
@ConfigurationProperties(prefix = "server")
class ServerConfig implements InitializingBean {
public class ServerConfig {
private Logger log = LoggerFactory.getLogger(ServerConfig.class);
@Autowired
private MatrixConfig mxCfg;
private String name
private int port
private String publicUrl
private String name;
private int port;
private String publicUrl;
String getName() {
return name
public String getName() {
return name;
}
void setName(String name) {
this.name = name
public void setName(String name) {
this.name = name;
}
int getPort() {
return port
public int getPort() {
return port;
}
void setPort(int port) {
this.port = port
public void setPort(int port) {
this.port = port;
}
String getPublicUrl() {
return publicUrl
public String getPublicUrl() {
return publicUrl;
}
void setPublicUrl(String publicUrl) {
this.publicUrl = publicUrl
public void setPublicUrl(String publicUrl) {
this.publicUrl = publicUrl;
}
@Override
void afterPropertiesSet() throws Exception {
log.info("--- Server config ---")
@PostConstruct
public void build() {
log.info("--- Server config ---");
if (StringUtils.isBlank(getName())) {
setName(mxCfg.getDomain());
@@ -75,21 +78,21 @@ class ServerConfig implements InitializingBean {
}
if (StringUtils.isBlank(getPublicUrl())) {
setPublicUrl("https://${getName()}");
setPublicUrl("https://" + getName());
log.debug("Public URL is empty, generating from name");
} else {
setPublicUrl(StringUtils.replace(getPublicUrl(), "%SERVER_NAME%", getName()));
}
try {
new URL(getPublicUrl())
new URL(getPublicUrl());
} catch (MalformedURLException e) {
log.warn("Public URL is not valid: {}", StringUtils.defaultIfBlank(e.getMessage(), "<no reason provided>"))
log.warn("Public URL is not valid: {}", StringUtils.defaultIfBlank(e.getMessage(), "<no reason provided>"));
}
log.info("Name: {}", getName())
log.info("Port: {}", getPort())
log.info("Public URL: {}", getPublicUrl())
log.info("Name: {}", getName());
log.info("Port: {}", getPort());
log.info("Public URL: {}", getPublicUrl());
}
}

View File

@@ -0,0 +1,131 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program 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.
*
* This program 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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config.ldap;
import com.google.gson.Gson;
import io.kamax.mxisd.backend.ldap.LdapThreePidProvider;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Configuration
@ConfigurationProperties(prefix = "ldap")
public class LdapConfig {
private static Gson gson = new Gson();
private Logger log = LoggerFactory.getLogger(LdapConfig.class);
private boolean enabled;
@Autowired
private LdapConnectionConfig conn;
private LdapAttributeConfig attribute;
private LdapAuthConfig auth;
private LdapIdentityConfig identity;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public LdapConnectionConfig getConn() {
return conn;
}
public void setConn(LdapConnectionConfig conn) {
this.conn = conn;
}
public LdapAttributeConfig getAttribute() {
return attribute;
}
public void setAttribute(LdapAttributeConfig attribute) {
this.attribute = attribute;
}
public LdapAuthConfig getAuth() {
return auth;
}
public void setAuth(LdapAuthConfig auth) {
this.auth = auth;
}
public LdapIdentityConfig getIdentity() {
return identity;
}
public void setIdentity(LdapIdentityConfig identity) {
this.identity = identity;
}
@PostConstruct
public void build() {
log.info("--- LDAP Config ---");
log.info("Enabled: {}", isEnabled());
if (!isEnabled()) {
return;
}
if (StringUtils.isBlank(conn.getHost())) {
throw new IllegalStateException("LDAP Host must be configured!");
}
if (1 > conn.getPort() || 65535 < conn.getPort()) {
throw new IllegalStateException("LDAP port is not valid");
}
if (StringUtils.isBlank(attribute.getUid().getType())) {
throw new IllegalStateException("Attribute UID Type cannot be empty");
}
if (StringUtils.isBlank(attribute.getUid().getValue())) {
throw new IllegalStateException("Attribute UID value cannot be empty");
}
String uidType = attribute.getUid().getType();
if (!StringUtils.equals(LdapThreePidProvider.UID, uidType) && !StringUtils.equals(LdapThreePidProvider.MATRIX_ID, uidType)) {
throw new IllegalArgumentException("Unsupported LDAP UID type: " + uidType);
}
log.info("Host: {}", conn.getHost());
log.info("Port: {}", conn.getPort());
log.info("Bind DN: {}", conn.getBindDn());
log.info("Base DN: {}", conn.getBaseDn());
log.info("Attribute: {}", gson.toJson(attribute));
log.info("Auth: {}", gson.toJson(auth));
log.info("Identity: {}", gson.toJson(identity));
}
}

View File

@@ -18,28 +18,31 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.controller.v1
package io.kamax.mxisd.controller.v1;
import io.kamax.mxisd.lookup.ThreePidMapping
import io.kamax.mxisd.lookup.ThreePidMapping;
class ClientBulkLookupRequest {
import java.util.ArrayList;
import java.util.List;
private List<List<String>> threepids = new ArrayList<>()
public class ClientBulkLookupRequest {
List<List<String>> getThreepids() {
return threepids
private List<List<String>> threepids = new ArrayList<>();
public List<List<String>> getThreepids() {
return threepids;
}
void setThreepids(List<List<String>> threepids) {
this.threepids = threepids
public void setThreepids(List<List<String>> threepids) {
this.threepids = threepids;
}
void setMappings(List<ThreePidMapping> mappings) {
public void setMappings(List<ThreePidMapping> mappings) {
for (ThreePidMapping mapping : mappings) {
List<String> threepid = new ArrayList<>()
threepid.add(mapping.getMedium())
threepid.add(mapping.getValue())
threepids.add(threepid)
List<String> threepid = new ArrayList<>();
threepid.add(mapping.getMedium());
threepid.add(mapping.getValue());
threepids.add(threepid);
}
}

View File

@@ -18,47 +18,49 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.controller.v1
package io.kamax.mxisd.controller.v1;
import com.google.gson.Gson
import io.kamax.matrix.MatrixID
import io.kamax.mxisd.config.ServerConfig
import io.kamax.mxisd.controller.v1.io.ThreePidInviteReplyIO
import io.kamax.mxisd.invitation.IThreePidInvite
import io.kamax.mxisd.invitation.IThreePidInviteReply
import io.kamax.mxisd.invitation.InvitationManager
import io.kamax.mxisd.invitation.ThreePidInvite
import io.kamax.mxisd.key.KeyManager
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.CrossOrigin
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import com.google.gson.Gson;
import io.kamax.matrix.MatrixID;
import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.controller.v1.io.ThreePidInviteReplyIO;
import io.kamax.mxisd.invitation.IThreePidInvite;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.invitation.InvitationManager;
import io.kamax.mxisd.invitation.ThreePidInvite;
import io.kamax.mxisd.key.KeyManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import static org.springframework.web.bind.annotation.RequestMethod.POST
import static org.springframework.web.bind.annotation.RequestMethod.POST;
@RestController
@CrossOrigin
@RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
class InvitationController {
private Logger log = LoggerFactory.getLogger(InvitationController.class)
private Logger log = LoggerFactory.getLogger(InvitationController.class);
@Autowired
private InvitationManager mgr
private InvitationManager mgr;
@Autowired
private KeyManager keyMgr
private KeyManager keyMgr;
@Autowired
private ServerConfig srvCfg
private ServerConfig srvCfg;
private Gson gson = new Gson()
private Gson gson = new Gson();
@RequestMapping(value = "/store-invite", method = POST)
String store(
@@ -67,14 +69,14 @@ class InvitationController {
@RequestParam String medium,
@RequestParam String address,
@RequestParam("room_id") String roomId) {
Map<String, String> parameters = new HashMap<>()
Map<String, String> parameters = new HashMap<>();
for (String key : request.getParameterMap().keySet()) {
parameters.put(key, request.getParameter(key));
}
IThreePidInvite invite = new ThreePidInvite(new MatrixID(sender), medium, address, roomId, parameters)
IThreePidInviteReply reply = mgr.storeInvite(invite)
IThreePidInvite invite = new ThreePidInvite(new MatrixID(sender), medium, address, roomId, parameters);
IThreePidInviteReply reply = mgr.storeInvite(invite);
return gson.toJson(new ThreePidInviteReplyIO(reply, keyMgr.getPublicKeyBase64(keyMgr.getCurrentIndex()), srvCfg.getPublicUrl()))
return gson.toJson(new ThreePidInviteReplyIO(reply, keyMgr.getPublicKeyBase64(keyMgr.getCurrentIndex()), srvCfg.getPublicUrl()));
}
}

View File

@@ -18,64 +18,64 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.controller.v1
package io.kamax.mxisd.controller.v1;
import com.google.gson.Gson
import groovy.json.JsonOutput
import io.kamax.mxisd.controller.v1.io.KeyValidityJson
import io.kamax.mxisd.exception.BadRequestException
import io.kamax.mxisd.key.KeyManager
import org.apache.commons.lang.StringUtils
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.*
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import io.kamax.mxisd.controller.v1.io.KeyValidityJson;
import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.key.KeyManager;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletRequest;
import static org.springframework.web.bind.annotation.RequestMethod.GET
import static org.springframework.web.bind.annotation.RequestMethod.GET;
@RestController
@CrossOrigin
@RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
class KeyController {
public class KeyController {
private Logger log = LoggerFactory.getLogger(KeyController.class)
private Logger log = LoggerFactory.getLogger(KeyController.class);
@Autowired
private KeyManager keyMgr
private KeyManager keyMgr;
private Gson gson = new Gson();
private String validKey = gson.toJson(new KeyValidityJson(true));
private String invalidKey = gson.toJson(new KeyValidityJson(false));
@RequestMapping(value = "/pubkey/{keyType}:{keyId}", method = GET)
String getKey(@PathVariable String keyType, @PathVariable int keyId) {
public String getKey(@PathVariable String keyType, @PathVariable int keyId) {
if (!"ed25519".contentEquals(keyType)) {
throw new BadRequestException("Invalid algorithm: " + keyType)
throw new BadRequestException("Invalid algorithm: " + keyType);
}
log.info("Key {}:{} was requested", keyType, keyId)
return JsonOutput.toJson([
public_key: keyMgr.getPublicKeyBase64(keyId)
])
log.info("Key {}:{} was requested", keyType, keyId);
JsonObject obj = new JsonObject();
obj.addProperty("public_key", keyMgr.getPublicKeyBase64(keyId));
return gson.toJson(obj);
}
@RequestMapping(value = "/pubkey/ephemeral/isvalid", method = GET)
String checkEphemeralKeyValidity(HttpServletRequest request) {
log.warn("Ephemeral key was request but no ephemeral key are generated, replying not valid")
public String checkEphemeralKeyValidity(HttpServletRequest request) {
log.warn("Ephemeral key was request but no ephemeral key are generated, replying not valid");
return invalidKey
return invalidKey;
}
@RequestMapping(value = "/pubkey/isvalid", method = GET)
String checkKeyValidity(HttpServletRequest request, @RequestParam("public_key") String pubKey) {
log.info("Validating public key {}", pubKey)
public String checkKeyValidity(HttpServletRequest request, @RequestParam("public_key") String pubKey) {
log.info("Validating public key {}", pubKey);
// TODO do in manager
boolean valid = StringUtils.equals(pubKey, keyMgr.getPublicKeyBase64(keyMgr.getCurrentIndex()))
return valid ? validKey : invalidKey
boolean valid = StringUtils.equals(pubKey, keyMgr.getPublicKeyBase64(keyMgr.getCurrentIndex()));
return valid ? validKey : invalidKey;
}
}

View File

@@ -0,0 +1,130 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program 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.
*
* This program 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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.controller.v1;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import io.kamax.mxisd.controller.v1.io.SingeLookupReplyJson;
import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.lookup.*;
import io.kamax.mxisd.lookup.strategy.LookupStrategy;
import io.kamax.mxisd.signature.SignatureManager;
import io.kamax.mxisd.util.GsonParser;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.bind.annotation.RequestMethod.POST;
@RestController
@CrossOrigin
@RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public class MappingController {
private Logger log = LoggerFactory.getLogger(MappingController.class);
private Gson gson = new Gson();
private GsonParser parser = new GsonParser(gson);
@Autowired
private LookupStrategy strategy;
@Autowired
private SignatureManager signMgr;
private void setRequesterInfo(ALookupRequest lookupReq, HttpServletRequest req) {
lookupReq.setRequester(req.getRemoteAddr());
String xff = req.getHeader("X-FORWARDED-FOR");
lookupReq.setRecursive(StringUtils.isNotBlank(xff));
if (lookupReq.isRecursive()) {
lookupReq.setRecurseHosts(Arrays.asList(xff.split(",")));
}
lookupReq.setUserAgent(req.getHeader("USER-AGENT"));
}
@RequestMapping(value = "/lookup", method = GET)
String lookup(HttpServletRequest request, @RequestParam String medium, @RequestParam String address) {
SingleLookupRequest lookupRequest = new SingleLookupRequest();
setRequesterInfo(lookupRequest, request);
lookupRequest.setType(medium);
lookupRequest.setThreePid(address);
log.info("Got single lookup request from {} with client {} - Is recursive? {}", lookupRequest.getRequester(), lookupRequest.getUserAgent(), lookupRequest.isRecursive());
Optional<SingleLookupReply> lookupOpt = strategy.find(lookupRequest);
if (!lookupOpt.isPresent()) {
log.info("No mapping was found, return empty JSON object");
return "{}";
}
SingleLookupReply lookup = lookupOpt.get();
if (lookup.isSigned()) {
log.info("Lookup is already signed, sending as-is");
return lookup.getBody();
} else {
log.info("Lookup is not signed, signing");
JsonObject obj = gson.toJsonTree(new SingeLookupReplyJson(lookup)).getAsJsonObject();
obj.add("signatures", signMgr.signMessageGson(gson.toJson(obj)));
return gson.toJson(obj);
}
}
@RequestMapping(value = "/bulk_lookup", method = POST)
String bulkLookup(HttpServletRequest request) {
BulkLookupRequest lookupRequest = new BulkLookupRequest();
setRequesterInfo(lookupRequest, request);
log.info("Got single lookup request from {} with client {} - Is recursive? {}", lookupRequest.getRequester(), lookupRequest.getUserAgent(), lookupRequest.isRecursive());
try {
ClientBulkLookupRequest input = parser.parse(request, ClientBulkLookupRequest.class);
List<ThreePidMapping> mappings = new ArrayList<>();
for (List<String> mappingRaw : input.getThreepids()) {
ThreePidMapping mapping = new ThreePidMapping();
mapping.setMedium(mappingRaw.get(0));
mapping.setValue(mappingRaw.get(1));
mappings.add(mapping);
}
lookupRequest.setMappings(mappings);
ClientBulkLookupAnswer answer = new ClientBulkLookupAnswer();
answer.addAll(strategy.find(lookupRequest));
return gson.toJson(answer);
} catch (IOException e) {
throw new InternalServerError(e);
}
}
}

View File

@@ -18,41 +18,44 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.controller.v1
package io.kamax.mxisd.controller.v1;
import io.kamax.mxisd.config.ServerConfig
import io.kamax.mxisd.config.ViewConfig
import io.kamax.mxisd.controller.v1.remote.RemoteIdentityAPIv1
import io.kamax.mxisd.session.SessionMananger
import io.kamax.mxisd.session.ValidationResult
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.config.ViewConfig;
import io.kamax.mxisd.controller.v1.remote.RemoteIdentityAPIv1;
import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.session.SessionMananger;
import io.kamax.mxisd.session.ValidationResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Controller
@RequestMapping(path = IdentityAPIv1.BASE)
class SessionController {
private Logger log = LoggerFactory.getLogger(SessionController.class)
private Logger log = LoggerFactory.getLogger(SessionController.class);
@Autowired
private ServerConfig srvCfg;
@Autowired
private SessionMananger mgr
private SessionMananger mgr;
@Autowired
private ViewConfig viewCfg;
;
@RequestMapping(value = "/validate/{medium}/submitToken")
String validate(
public String validate(
HttpServletRequest request,
HttpServletResponse response,
@RequestParam String sid,
@@ -60,21 +63,27 @@ class SessionController {
@RequestParam String token,
Model model
) {
log.info("Requested: {}?{}", request.getRequestURL(), request.getQueryString())
log.info("Requested: {}?{}", request.getRequestURL(), request.getQueryString());
ValidationResult r = mgr.validate(sid, secret, token)
log.info("Session {} was validated", sid)
ValidationResult r = mgr.validate(sid, secret, token);
log.info("Session {} was validated", sid);
if (r.getNextUrl().isPresent()) {
String url = srvCfg.getPublicUrl() + r.getNextUrl().get()
log.info("Session {} validation: next URL is present, redirecting to {}", sid, url)
response.sendRedirect(url)
String url = srvCfg.getPublicUrl() + r.getNextUrl().get();
log.info("Session {} validation: next URL is present, redirecting to {}", sid, url);
try {
response.sendRedirect(url);
return "";
} catch (IOException e) {
log.warn("Unable to redirect user to {}", url);
throw new InternalServerError(e);
}
} else {
if (r.isCanRemote()) {
String url = srvCfg.getPublicUrl() + RemoteIdentityAPIv1.getRequestToken(r.getSession().getId(), r.getSession().getSecret());
model.addAttribute("remoteSessionLink", url)
return viewCfg.getSession().getLocalRemote().getOnTokenSubmit().getSuccess()
model.addAttribute("remoteSessionLink", url);
return viewCfg.getSession().getLocalRemote().getOnTokenSubmit().getSuccess();
} else {
return viewCfg.getSession().getLocal().getOnTokenSubmit().getSuccess()
return viewCfg.getSession().getLocal().getOnTokenSubmit().getSuccess();
}
}
}

View File

@@ -18,16 +18,16 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.exception
package io.kamax.mxisd.exception;
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
class BadRequestException extends RuntimeException {
public class BadRequestException extends RuntimeException {
BadRequestException(String s) {
super(s)
public BadRequestException(String s) {
super(s);
}
}

View File

@@ -43,6 +43,10 @@ public class InternalServerError extends MatrixException {
this.internalReason = internalReason;
}
public InternalServerError(Throwable t) {
this(t.getMessage());
}
public String getReference() {
return reference;
}

View File

@@ -18,10 +18,10 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.exception
package io.kamax.mxisd.exception;
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value = HttpStatus.NOT_IMPLEMENTED)
public class NotImplementedException extends RuntimeException {

View File

@@ -21,6 +21,8 @@
package io.kamax.mxisd.invitation;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import io.kamax.matrix.MatrixID;
import io.kamax.mxisd.config.DnsOverwrite;
import io.kamax.mxisd.config.DnsOverwriteEntry;
@@ -45,8 +47,6 @@ import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContextBuilder;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -153,13 +153,13 @@ public class InvitationManager {
return reply.getInvite().getSender().getId() + ":" + reply.getInvite().getRoomId() + ":" + reply.getInvite().getMedium() + ":" + reply.getInvite().getAddress();
}
String getSrvRecordName(String domain) {
private String getSrvRecordName(String domain) {
return "_matrix._tcp." + domain;
}
// TODO use caching mechanism
// TODO export in matrix-java-sdk
String findHomeserverForDomain(String domain) {
private String findHomeserverForDomain(String domain) {
Optional<DnsOverwriteEntry> entryOpt = dns.findHost(domain);
if (entryOpt.isPresent()) {
DnsOverwriteEntry entry = entryOpt.get();
@@ -264,28 +264,28 @@ public class InvitationManager {
new Thread(() -> { // FIXME need to make this retry-able and within a general background working pool
HttpPost req = new HttpPost(hsUrlOpt + "/_matrix/federation/v1/3pid/onbind");
// Expected body: https://matrix.to/#/!HUeDbmFUsWAhxHHvFG:matrix.org/$150469846739DCLWc:matrix.trancendances.fr
JSONObject obj = new JSONObject(); // TODO use Gson instead
obj.put("mxid", mxid);
obj.put("token", reply.getToken());
obj.put("signatures", signMgr.signMessageJson(obj.toString()));
JsonObject obj = new JsonObject();
obj.addProperty("mxid", mxid);
obj.addProperty("token", reply.getToken());
obj.add("signatures", signMgr.signMessageGson(obj.toString()));
JSONObject objUp = new JSONObject();
objUp.put("mxid", mxid);
objUp.put("medium", medium);
objUp.put("address", address);
objUp.put("sender", reply.getInvite().getSender().getId());
objUp.put("room_id", reply.getInvite().getRoomId());
objUp.put("signed", obj);
JsonObject objUp = new JsonObject();
objUp.addProperty("mxid", mxid);
objUp.addProperty("medium", medium);
objUp.addProperty("address", address);
objUp.addProperty("sender", reply.getInvite().getSender().getId());
objUp.addProperty("room_id", reply.getInvite().getRoomId());
objUp.add("signed", obj);
JSONObject content = new JSONObject(); // TODO use Gson instead
JSONArray invites = new JSONArray();
invites.put(objUp);
content.put("invites", invites);
content.put("medium", medium);
content.put("address", address);
content.put("mxid", mxid);
JsonObject content = new JsonObject();
JsonArray invites = new JsonArray();
invites.add(objUp);
content.add("invites", invites);
content.addProperty("medium", medium);
content.addProperty("address", address);
content.addProperty("mxid", mxid);
content.put("signatures", signMgr.signMessageJson(content.toString()));
content.add("signatures", signMgr.signMessageGson(content.toString()));
StringEntity entity = new StringEntity(content.toString(), StandardCharsets.UTF_8);
entity.setContentType("application/json");
@@ -313,7 +313,7 @@ public class InvitationManager {
private IThreePidInviteReply reply;
public MappingChecker(IThreePidInviteReply reply) {
MappingChecker(IThreePidInviteReply reply) {
this.reply = reply;
}

View File

@@ -0,0 +1,115 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program 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.
*
* This program 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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.key;
import io.kamax.mxisd.config.KeyConfig;
import net.i2p.crypto.eddsa.EdDSAEngine;
import net.i2p.crypto.eddsa.EdDSAPrivateKey;
import net.i2p.crypto.eddsa.EdDSAPublicKey;
import net.i2p.crypto.eddsa.KeyPairGenerator;
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec;
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec;
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
@Component
public class KeyManager {
@Autowired
private KeyConfig keyCfg;
private EdDSAParameterSpec keySpecs;
private EdDSAEngine signEngine;
private List<KeyPair> keys;
@PostConstruct
public void build() {
try {
keySpecs = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.CURVE_ED25519_SHA512);
signEngine = new EdDSAEngine(MessageDigest.getInstance(keySpecs.getHashAlgorithm()));
keys = new ArrayList<>();
Path privKey = Paths.get(keyCfg.getPath());
if (!Files.exists(privKey)) {
KeyPair pair = (new KeyPairGenerator()).generateKeyPair();
String keyEncoded = Base64.getEncoder().encodeToString(pair.getPrivate().getEncoded());
FileUtils.writeStringToFile(privKey.toFile(), keyEncoded, StandardCharsets.ISO_8859_1);
keys.add(pair);
} else {
if (Files.isDirectory(privKey)) {
throw new RuntimeException("Invalid path for private key: " + privKey.toString());
}
if (Files.isReadable(privKey)) {
byte[] seed = Base64.getDecoder().decode(FileUtils.readFileToString(privKey.toFile(), StandardCharsets.ISO_8859_1));
EdDSAPrivateKeySpec privKeySpec = new EdDSAPrivateKeySpec(seed, keySpecs);
EdDSAPublicKeySpec pubKeySpec = new EdDSAPublicKeySpec(privKeySpec.getA(), keySpecs);
keys.add(new KeyPair(new EdDSAPublicKey(pubKeySpec), new EdDSAPrivateKey(privKeySpec)));
}
}
} catch (NoSuchAlgorithmException | IOException e) {
throw new RuntimeException(e);
}
}
public int getCurrentIndex() {
return 0;
}
public KeyPair getKeys(int index) {
return keys.get(index);
}
public PrivateKey getPrivateKey(int index) {
return getKeys(index).getPrivate();
}
public EdDSAPublicKey getPublicKey(int index) {
return (EdDSAPublicKey) getKeys(index).getPublic();
}
public EdDSAParameterSpec getSpecs() {
return keySpecs;
}
public String getPublicKeyBase64(int index) {
return Base64.getEncoder().encodeToString(getPublicKey(index).getAbyte());
}
}

Some files were not shown because too many files have changed in this diff Show More