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 * mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor * 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/>. * 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' apply plugin: 'org.springframework.boot'
def confFileName = "application.example.yaml" def confFileName = "application.example.yaml"
@@ -70,9 +70,6 @@ repositories {
} }
dependencies { dependencies {
// We are a groovy project
compile 'org.codehaus.groovy:groovy-all:2.4.7'
// Easy file management // Easy file management
compile 'commons-io:commons-io:2.5' 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/>. * 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.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication @SpringBootApplication
class MatrixIdentityServerApplication { class MatrixIdentityServerApplication {
static void main(String[] args) throws Exception { public static void main(String[] args) {
SpringApplication.run(MatrixIdentityServerApplication.class, args) SpringApplication.run(MatrixIdentityServerApplication.class, args);
} }
} }

View File

@@ -49,20 +49,27 @@ public class BackendAuthResult {
return r; return r;
} }
public void fail() {
success = false;
}
public static BackendAuthResult success(String id, UserIdType type, String displayName) { public static BackendAuthResult success(String id, UserIdType type, String displayName) {
return success(id, type.getId(), displayName); return success(id, type.getId(), displayName);
} }
public static BackendAuthResult success(String id, String type, String displayName) { public static BackendAuthResult success(String id, String type, String displayName) {
BackendAuthResult r = new BackendAuthResult(); BackendAuthResult r = new BackendAuthResult();
r.success = true; r.succeed(id, type, displayName);
r.id = new UserID(type, id);
r.profile = new BackendAuthProfile();
r.profile.displayName = displayName;
return r; 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 Boolean success;
private UserID id; private UserID id;
private BackendAuthProfile profile = new BackendAuthProfile(); 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/>. * 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.FirebaseApp;
import com.google.firebase.FirebaseOptions import com.google.firebase.FirebaseOptions;
import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseCredential import com.google.firebase.auth.FirebaseCredential;
import com.google.firebase.auth.FirebaseCredentials import com.google.firebase.auth.FirebaseCredentials;
import com.google.firebase.auth.UserRecord import com.google.firebase.auth.UserRecord;
import com.google.firebase.internal.NonNull import com.google.firebase.tasks.OnFailureListener;
import com.google.firebase.tasks.OnFailureListener import com.google.firebase.tasks.OnSuccessListener;
import com.google.firebase.tasks.OnSuccessListener import io.kamax.matrix.MatrixID;
import io.kamax.matrix.ThreePidMedium import io.kamax.matrix.ThreePidMedium;
import io.kamax.mxisd.lookup.SingleLookupReply import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.lookup.SingleLookupRequest import io.kamax.mxisd.lookup.SingleLookupRequest;
import io.kamax.mxisd.lookup.ThreePidMapping import io.kamax.mxisd.lookup.ThreePidMapping;
import io.kamax.mxisd.lookup.provider.IThreePidProvider import io.kamax.mxisd.lookup.provider.IThreePidProvider;
import org.apache.commons.lang.StringUtils import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger import org.slf4j.Logger;
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory;
import java.util.concurrent.CountDownLatch import java.io.FileInputStream;
import java.util.concurrent.TimeUnit import java.io.IOException;
import java.util.function.Consumer import java.util.ArrayList;
import java.util.regex.Pattern import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class GoogleFirebaseProvider implements IThreePidProvider { public class GoogleFirebaseProvider implements IThreePidProvider {
private Logger log = LoggerFactory.getLogger(GoogleFirebaseProvider.class); private Logger log = LoggerFactory.getLogger(GoogleFirebaseProvider.class);
private static final Pattern matrixIdLaxPattern = Pattern.compile("@(.*):(.+)");
private boolean isEnabled; private boolean isEnabled;
private String domain; private String domain;
private FirebaseApp fbApp;
private FirebaseAuth fbAuth; private FirebaseAuth fbAuth;
public GoogleFirebaseProvider(boolean isEnabled) { public GoogleFirebaseProvider(boolean isEnabled) {
@@ -61,8 +61,9 @@ public class GoogleFirebaseProvider implements IThreePidProvider {
public GoogleFirebaseProvider(String credsPath, String db, String domain) { public GoogleFirebaseProvider(String credsPath, String db, String domain) {
this(true); this(true);
this.domain = domain; this.domain = domain;
try { try {
fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db), "ThreePidProvider"); FirebaseApp fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db), "ThreePidProvider");
fbAuth = FirebaseAuth.getInstance(fbApp); fbAuth = FirebaseAuth.getInstance(fbApp);
log.info("Google Firebase Authentication is ready"); log.info("Google Firebase Authentication is ready");
@@ -91,7 +92,7 @@ public class GoogleFirebaseProvider implements IThreePidProvider {
} }
private String getMxid(UserRecord record) { private String getMxid(UserRecord record) {
return "@${record.getUid()}:${domain}"; return new MatrixID(record.getUid(), domain).getId();
} }
@Override @Override
@@ -118,71 +119,59 @@ public class GoogleFirebaseProvider implements IThreePidProvider {
} }
private Optional<UserRecord> findInternal(String medium, String address) { private Optional<UserRecord> findInternal(String medium, String address) {
UserRecord r; final UserRecord[] r = new UserRecord[1];
CountDownLatch l = new CountDownLatch(1); CountDownLatch l = new CountDownLatch(1);
OnSuccessListener<UserRecord> success = new OnSuccessListener<UserRecord>() { OnSuccessListener<UserRecord> success = result -> {
@Override log.info("Found 3PID match for {}:{} - UID is {}", medium, address, result.getUid());
void onSuccess(UserRecord result) { r[0] = result;
log.info("Found 3PID match for {}:{} - UID is {}", medium, address, result.getUid()) l.countDown();
r = result;
l.countDown()
}
}; };
OnFailureListener failure = new OnFailureListener() { OnFailureListener failure = e -> {
@Override log.info("No 3PID match for {}:{} - {}", medium, address, e.getMessage());
void onFailure(@NonNull Exception e) { r[0] = null;
log.info("No 3PID match for {}:{} - {}", medium, address, e.getMessage()) l.countDown();
r = null;
l.countDown()
}
}; };
if (ThreePidMedium.Email.is(medium)) { 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) fbAuth.getUserByEmail(address)
.addOnSuccessListener(success) .addOnSuccessListener(success)
.addOnFailureListener(failure); .addOnFailureListener(failure);
waitOnLatch(l); waitOnLatch(l);
} else if (ThreePidMedium.PhoneNumber.is(medium)) { } 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) fbAuth.getUserByPhoneNumber(address)
.addOnSuccessListener(success) .addOnSuccessListener(success)
.addOnFailureListener(failure); .addOnFailureListener(failure);
waitOnLatch(l); waitOnLatch(l);
} else { } else {
log.info("{} is not a supported 3PID medium", medium); log.info("{} is not a supported 3PID medium", medium);
r = null; r[0] = null;
} }
return Optional.ofNullable(r); return Optional.ofNullable(r[0]);
} }
@Override @Override
public Optional<SingleLookupReply> find(SingleLookupRequest request) { public Optional<SingleLookupReply> find(SingleLookupRequest request) {
Optional<UserRecord> urOpt = findInternal(request.getType(), request.getThreePid()) Optional<UserRecord> urOpt = findInternal(request.getType(), request.getThreePid());
if (urOpt.isPresent()) { return urOpt.map(userRecord -> new SingleLookupReply(request, getMxid(userRecord)));
return Optional.of(new SingleLookupReply(request, getMxid(urOpt.get())));
}
return Optional.empty();
} }
@Override @Override
public List<ThreePidMapping> populate(List<ThreePidMapping> mappings) { public List<ThreePidMapping> populate(List<ThreePidMapping> mappings) {
List<ThreePidMapping> results = new ArrayList<>(); List<ThreePidMapping> results = new ArrayList<>();
mappings.parallelStream().forEach(new Consumer<ThreePidMapping>() { mappings.parallelStream().forEach(o -> {
@Override Optional<UserRecord> urOpt = findInternal(o.getMedium(), o.getValue());
void accept(ThreePidMapping o) { if (urOpt.isPresent()) {
Optional<UserRecord> urOpt = findInternal(o.getMedium(), o.getValue()); ThreePidMapping result = new ThreePidMapping();
if (urOpt.isPresent()) { result.setMedium(o.getMedium());
ThreePidMapping result = new ThreePidMapping(); result.setValue(o.getValue());
result.setMedium(o.getMedium()) result.setMxid(getMxid(urOpt.get()));
result.setValue(o.getValue()) results.add(result);
result.setMxid(getMxid(urOpt.get()))
results.add(result)
}
} }
}); });
return results; 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) { if (!enabled) {
return new GoogleFirebaseAuthenticator(false); return new GoogleFirebaseAuthenticator(false);
} else { } 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/>. * 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.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
@Configuration @Configuration
@ConfigurationProperties(prefix = "forward") @ConfigurationProperties(prefix = "forward")
class ForwardConfig { public class ForwardConfig {
private List<String> servers = new ArrayList<>() private List<String> servers = new ArrayList<>();
List<String> getServers() { public List<String> getServers() {
return servers return servers;
} }
void setServers(List<String> servers) { public void setServers(List<String> servers) {
this.servers = servers this.servers = servers;
} }
} }

View File

@@ -18,32 +18,33 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * 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 io.kamax.mxisd.exception.ConfigurationException;
import org.apache.commons.lang.StringUtils import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.InitializingBean import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Configuration
import javax.annotation.PostConstruct;
@Configuration @Configuration
@ConfigurationProperties(prefix = "key") @ConfigurationProperties(prefix = "key")
class KeyConfig implements InitializingBean { public class KeyConfig {
private String path private String path;
void setPath(String path) { public void setPath(String path) {
this.path = path this.path = path;
} }
String getPath() { public String getPath() {
return path return path;
} }
@Override @PostConstruct
void afterPropertiesSet() throws Exception { public void build() {
if (StringUtils.isBlank(getPath())) { 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/>. * 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.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration @Configuration
@ConfigurationProperties(prefix = "lookup.recursive") @ConfigurationProperties(prefix = "lookup.recursive")
class RecursiveLookupConfig { public class RecursiveLookupConfig {
private boolean enabled private boolean enabled;
private List<String> allowedCidr private List<String> allowedCidr;
private RecursiveLookupBridgeConfig bridge private RecursiveLookupBridgeConfig bridge;
boolean isEnabled() { public boolean isEnabled() {
return enabled return enabled;
} }
void setEnabled(boolean enabled) { public void setEnabled(boolean enabled) {
this.enabled = enabled this.enabled = enabled;
} }
List<String> getAllowedCidr() { public List<String> getAllowedCidr() {
return allowedCidr return allowedCidr;
} }
void setAllowedCidr(List<String> allowedCidr) { public void setAllowedCidr(List<String> allowedCidr) {
this.allowedCidr = allowedCidr this.allowedCidr = allowedCidr;
} }
RecursiveLookupBridgeConfig getBridge() { public RecursiveLookupBridgeConfig getBridge() {
return bridge return bridge;
} }
void setBridge(RecursiveLookupBridgeConfig bridge) { public void setBridge(RecursiveLookupBridgeConfig bridge) {
this.bridge = bridge this.bridge = bridge;
} }
} }

View File

@@ -18,56 +18,59 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * 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.apache.commons.lang.StringUtils;
import org.slf4j.Logger import org.slf4j.Logger;
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Configuration
import javax.annotation.PostConstruct;
import java.net.MalformedURLException;
import java.net.URL;
@Configuration @Configuration
@ConfigurationProperties(prefix = "server") @ConfigurationProperties(prefix = "server")
class ServerConfig implements InitializingBean { public class ServerConfig {
private Logger log = LoggerFactory.getLogger(ServerConfig.class); private Logger log = LoggerFactory.getLogger(ServerConfig.class);
@Autowired @Autowired
private MatrixConfig mxCfg; private MatrixConfig mxCfg;
private String name private String name;
private int port private int port;
private String publicUrl private String publicUrl;
String getName() { public String getName() {
return name return name;
} }
void setName(String name) { public void setName(String name) {
this.name = name this.name = name;
} }
int getPort() { public int getPort() {
return port return port;
} }
void setPort(int port) { public void setPort(int port) {
this.port = port this.port = port;
} }
String getPublicUrl() { public String getPublicUrl() {
return publicUrl return publicUrl;
} }
void setPublicUrl(String publicUrl) { public void setPublicUrl(String publicUrl) {
this.publicUrl = publicUrl this.publicUrl = publicUrl;
} }
@Override @PostConstruct
void afterPropertiesSet() throws Exception { public void build() {
log.info("--- Server config ---") log.info("--- Server config ---");
if (StringUtils.isBlank(getName())) { if (StringUtils.isBlank(getName())) {
setName(mxCfg.getDomain()); setName(mxCfg.getDomain());
@@ -75,21 +78,21 @@ class ServerConfig implements InitializingBean {
} }
if (StringUtils.isBlank(getPublicUrl())) { if (StringUtils.isBlank(getPublicUrl())) {
setPublicUrl("https://${getName()}"); setPublicUrl("https://" + getName());
log.debug("Public URL is empty, generating from name"); log.debug("Public URL is empty, generating from name");
} else { } else {
setPublicUrl(StringUtils.replace(getPublicUrl(), "%SERVER_NAME%", getName())); setPublicUrl(StringUtils.replace(getPublicUrl(), "%SERVER_NAME%", getName()));
} }
try { try {
new URL(getPublicUrl()) new URL(getPublicUrl());
} catch (MalformedURLException e) { } 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("Name: {}", getName());
log.info("Port: {}", getPort()) log.info("Port: {}", getPort());
log.info("Public URL: {}", getPublicUrl()) 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/>. * 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() { private List<List<String>> threepids = new ArrayList<>();
return threepids
public List<List<String>> getThreepids() {
return threepids;
} }
void setThreepids(List<List<String>> threepids) { public void setThreepids(List<List<String>> threepids) {
this.threepids = threepids this.threepids = threepids;
} }
void setMappings(List<ThreePidMapping> mappings) { public void setMappings(List<ThreePidMapping> mappings) {
for (ThreePidMapping mapping : mappings) { for (ThreePidMapping mapping : mappings) {
List<String> threepid = new ArrayList<>() List<String> threepid = new ArrayList<>();
threepid.add(mapping.getMedium()) threepid.add(mapping.getMedium());
threepid.add(mapping.getValue()) threepid.add(mapping.getValue());
threepids.add(threepid) threepids.add(threepid);
} }
} }

View File

@@ -18,47 +18,49 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * 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 com.google.gson.Gson;
import io.kamax.matrix.MatrixID import io.kamax.matrix.MatrixID;
import io.kamax.mxisd.config.ServerConfig import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.controller.v1.io.ThreePidInviteReplyIO import io.kamax.mxisd.controller.v1.io.ThreePidInviteReplyIO;
import io.kamax.mxisd.invitation.IThreePidInvite import io.kamax.mxisd.invitation.IThreePidInvite;
import io.kamax.mxisd.invitation.IThreePidInviteReply import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.invitation.InvitationManager import io.kamax.mxisd.invitation.InvitationManager;
import io.kamax.mxisd.invitation.ThreePidInvite import io.kamax.mxisd.invitation.ThreePidInvite;
import io.kamax.mxisd.key.KeyManager import io.kamax.mxisd.key.KeyManager;
import org.slf4j.Logger import org.slf4j.Logger;
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.CrossOrigin import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController 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 @RestController
@CrossOrigin @CrossOrigin
@RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) @RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
class InvitationController { class InvitationController {
private Logger log = LoggerFactory.getLogger(InvitationController.class) private Logger log = LoggerFactory.getLogger(InvitationController.class);
@Autowired @Autowired
private InvitationManager mgr private InvitationManager mgr;
@Autowired @Autowired
private KeyManager keyMgr private KeyManager keyMgr;
@Autowired @Autowired
private ServerConfig srvCfg private ServerConfig srvCfg;
private Gson gson = new Gson() private Gson gson = new Gson();
@RequestMapping(value = "/store-invite", method = POST) @RequestMapping(value = "/store-invite", method = POST)
String store( String store(
@@ -67,14 +69,14 @@ class InvitationController {
@RequestParam String medium, @RequestParam String medium,
@RequestParam String address, @RequestParam String address,
@RequestParam("room_id") String roomId) { @RequestParam("room_id") String roomId) {
Map<String, String> parameters = new HashMap<>() Map<String, String> parameters = new HashMap<>();
for (String key : request.getParameterMap().keySet()) { for (String key : request.getParameterMap().keySet()) {
parameters.put(key, request.getParameter(key)); parameters.put(key, request.getParameter(key));
} }
IThreePidInvite invite = new ThreePidInvite(new MatrixID(sender), medium, address, roomId, parameters) IThreePidInvite invite = new ThreePidInvite(new MatrixID(sender), medium, address, roomId, parameters);
IThreePidInviteReply reply = mgr.storeInvite(invite) 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/>. * 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 com.google.gson.Gson;
import groovy.json.JsonOutput import com.google.gson.JsonObject;
import io.kamax.mxisd.controller.v1.io.KeyValidityJson import io.kamax.mxisd.controller.v1.io.KeyValidityJson;
import io.kamax.mxisd.exception.BadRequestException import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.key.KeyManager import io.kamax.mxisd.key.KeyManager;
import org.apache.commons.lang.StringUtils import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger import org.slf4j.Logger;
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.* 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 @RestController
@CrossOrigin @CrossOrigin
@RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) @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 @Autowired
private KeyManager keyMgr private KeyManager keyMgr;
private Gson gson = new Gson(); private Gson gson = new Gson();
private String validKey = gson.toJson(new KeyValidityJson(true)); private String validKey = gson.toJson(new KeyValidityJson(true));
private String invalidKey = gson.toJson(new KeyValidityJson(false)); private String invalidKey = gson.toJson(new KeyValidityJson(false));
@RequestMapping(value = "/pubkey/{keyType}:{keyId}", method = GET) @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)) { if (!"ed25519".contentEquals(keyType)) {
throw new BadRequestException("Invalid algorithm: " + keyType) throw new BadRequestException("Invalid algorithm: " + keyType);
} }
log.info("Key {}:{} was requested", keyType, keyId) log.info("Key {}:{} was requested", keyType, keyId);
return JsonOutput.toJson([ JsonObject obj = new JsonObject();
public_key: keyMgr.getPublicKeyBase64(keyId) obj.addProperty("public_key", keyMgr.getPublicKeyBase64(keyId));
]) return gson.toJson(obj);
} }
@RequestMapping(value = "/pubkey/ephemeral/isvalid", method = GET) @RequestMapping(value = "/pubkey/ephemeral/isvalid", method = GET)
String checkEphemeralKeyValidity(HttpServletRequest request) { public String checkEphemeralKeyValidity(HttpServletRequest request) {
log.warn("Ephemeral key was request but no ephemeral key are generated, replying not valid") 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) @RequestMapping(value = "/pubkey/isvalid", method = GET)
String checkKeyValidity(HttpServletRequest request, @RequestParam("public_key") String pubKey) { public String checkKeyValidity(HttpServletRequest request, @RequestParam("public_key") String pubKey) {
log.info("Validating public key {}", pubKey) log.info("Validating public key {}", pubKey);
// TODO do in manager // TODO do in manager
boolean valid = StringUtils.equals(pubKey, keyMgr.getPublicKeyBase64(keyMgr.getCurrentIndex())) boolean valid = StringUtils.equals(pubKey, keyMgr.getPublicKeyBase64(keyMgr.getCurrentIndex()));
return valid ? validKey : invalidKey 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/>. * 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.ServerConfig;
import io.kamax.mxisd.config.ViewConfig import io.kamax.mxisd.config.ViewConfig;
import io.kamax.mxisd.controller.v1.remote.RemoteIdentityAPIv1 import io.kamax.mxisd.controller.v1.remote.RemoteIdentityAPIv1;
import io.kamax.mxisd.session.SessionMananger import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.session.ValidationResult import io.kamax.mxisd.session.SessionMananger;
import org.slf4j.Logger import io.kamax.mxisd.session.ValidationResult;
import org.slf4j.LoggerFactory import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ui.Model import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Controller @Controller
@RequestMapping(path = IdentityAPIv1.BASE) @RequestMapping(path = IdentityAPIv1.BASE)
class SessionController { class SessionController {
private Logger log = LoggerFactory.getLogger(SessionController.class) private Logger log = LoggerFactory.getLogger(SessionController.class);
@Autowired @Autowired
private ServerConfig srvCfg; private ServerConfig srvCfg;
@Autowired @Autowired
private SessionMananger mgr private SessionMananger mgr;
@Autowired @Autowired
private ViewConfig viewCfg; private ViewConfig viewCfg;
;
@RequestMapping(value = "/validate/{medium}/submitToken") @RequestMapping(value = "/validate/{medium}/submitToken")
String validate( public String validate(
HttpServletRequest request, HttpServletRequest request,
HttpServletResponse response, HttpServletResponse response,
@RequestParam String sid, @RequestParam String sid,
@@ -60,21 +63,27 @@ class SessionController {
@RequestParam String token, @RequestParam String token,
Model model Model model
) { ) {
log.info("Requested: {}?{}", request.getRequestURL(), request.getQueryString()) log.info("Requested: {}?{}", request.getRequestURL(), request.getQueryString());
ValidationResult r = mgr.validate(sid, secret, token) ValidationResult r = mgr.validate(sid, secret, token);
log.info("Session {} was validated", sid) log.info("Session {} was validated", sid);
if (r.getNextUrl().isPresent()) { if (r.getNextUrl().isPresent()) {
String url = srvCfg.getPublicUrl() + r.getNextUrl().get() String url = srvCfg.getPublicUrl() + r.getNextUrl().get();
log.info("Session {} validation: next URL is present, redirecting to {}", sid, url) log.info("Session {} validation: next URL is present, redirecting to {}", sid, url);
response.sendRedirect(url) try {
response.sendRedirect(url);
return "";
} catch (IOException e) {
log.warn("Unable to redirect user to {}", url);
throw new InternalServerError(e);
}
} else { } else {
if (r.isCanRemote()) { if (r.isCanRemote()) {
String url = srvCfg.getPublicUrl() + RemoteIdentityAPIv1.getRequestToken(r.getSession().getId(), r.getSession().getSecret()); String url = srvCfg.getPublicUrl() + RemoteIdentityAPIv1.getRequestToken(r.getSession().getId(), r.getSession().getSecret());
model.addAttribute("remoteSessionLink", url) model.addAttribute("remoteSessionLink", url);
return viewCfg.getSession().getLocalRemote().getOnTokenSubmit().getSuccess() return viewCfg.getSession().getLocalRemote().getOnTokenSubmit().getSuccess();
} else { } 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/>. * 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.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value = HttpStatus.BAD_REQUEST) @ResponseStatus(value = HttpStatus.BAD_REQUEST)
class BadRequestException extends RuntimeException { public class BadRequestException extends RuntimeException {
BadRequestException(String s) { public BadRequestException(String s) {
super(s) super(s);
} }
} }

View File

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

View File

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

View File

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