Compare commits

...

17 Commits

Author SHA1 Message Date
Anatoly Sablin
b4776b50e2 https://github.com/ma1uta/ma1sd/issues/3 Allow extended character sets for backward compatibility. 2019-10-23 00:09:29 +03:00
Anatoly Sablin
2458b38b75 Add the configuration description for enable/disable unbind feature in the session.md 2019-10-22 23:54:53 +03:00
ma1uta
249e28a8b5 Merge pull request #5 from eyecreate/patch-2
Allow configuring unbind notifications to be sent or not.
2019-10-22 20:52:46 +00:00
ma1uta
ba9e2d6121 Merge pull request #4 from eyecreate/patch-1
make sure destination only contains the hostname value and not whole URL
2019-10-21 20:27:33 +00:00
eyecreate
f042b82a50 add missing import 2019-10-21 16:06:23 -04:00
eyecreate
59071177ad typo 2019-10-21 15:33:28 -04:00
eyecreate
6450cd1f20 have session manager check session config for sending notifications on unbinds. 2019-10-21 15:30:46 -04:00
eyecreate
90bc244f3e update docs to something that works. 2019-10-21 15:29:55 -04:00
eyecreate
6e52a509db update session config to allow unbindin emails to not be sent. 2019-10-21 15:28:30 -04:00
eyecreate
5ca666981a make sure destination only contains the hostname value and not whole URL
When testing this, the public URL is containing the protocol "https://" which differs from synapse's values. This code makes sure to remove that extra data to signatures match. Is there ever a situation in which the public url is any different?
2019-10-21 14:31:40 -04:00
Anatoly Sablin
36f22e5ca6 Update remarks of the fork. 2019-08-15 22:46:53 +03:00
Anatoly Sablin
a112a5e57c Improve request verification. Allow unbind only for configured matrix domain. 2019-08-07 21:47:10 +03:00
Anatoly Sablin
dbc764fe65 Add description about the MSC1915 configuration. 2019-08-05 22:15:53 +03:00
Anatoly Sablin
d5680b2dfe MSC1915. Add the option to enable/disable unbind. 2019-07-31 23:22:21 +03:00
Anatoly Sablin
5aad4fb81e MSC1915. Add unbind email notification. 2019-07-31 00:13:44 +03:00
Anatoly Sablin
a1f64f5159 Reworked MSC1915. Add request validation. 2019-07-27 15:51:01 +03:00
Anatoly Sablin
a96920f533 Add migration guide. 2019-07-21 23:44:42 +03:00
31 changed files with 385 additions and 255 deletions

View File

@@ -10,11 +10,13 @@ ma1sd - Federated Matrix Identity Server
- [Contribute](#contribute) - [Contribute](#contribute)
- [Powered by ma1sd](#powered-by-ma1sd) - [Powered by ma1sd](#powered-by-ma1sd)
- [FAQ](#faq) - [FAQ](#faq)
- [Migration from mxisd](#migration-from-mxisd)
- [Contact](#contact) - [Contact](#contact)
--- ---
* This project is a fork of the https://github.com/kamax-matrix/mxisd which has been archived and no longer supported. * * This project is a fork (not successor) of the https://github.com/kamax-matrix/mxisd, which has been archived and no longer maintained as a standalone product.
Also, ma1sd is supported by the volunteer not developers of the original project.
--- ---
@@ -108,6 +110,10 @@ The following projects can use ma1sd under the hood for some or all their featur
# FAQ # FAQ
See the [dedicated document](docs/faq.md) See the [dedicated document](docs/faq.md)
# Migration from mxisd
See the [migration guide](docs/migration-from-mxisd.md)
# Contact # Contact
Get in touch via: Get in touch via:
- Matrix: [#ma1sd:ru-matrix.org](https://matrix.to/#/#ma1sd:ru-matrix.org) - Matrix: [#ma1sd:ru-matrix.org](https://matrix.to/#/#ma1sd:ru-matrix.org)

View File

@@ -45,6 +45,14 @@ Create a list under the label `myOtherServers` containing two Identity servers:
- `server.port`: HTTP port to listen on (unencrypted) - `server.port`: HTTP port to listen on (unencrypted)
- `server.publicUrl`: Defaults to `https://{server.name}` - `server.publicUrl`: Defaults to `https://{server.name}`
## Unbind (MSC1915)
- `session.policy.unbind.enabled`: Enable or disable unbind functionality (MSC1915). (Defaults to true).
*Warning*: Unbind check incoming request by two ways:
- session validation.
- request signature via `X-Matrix` header and uses `server.publicUrl` property to construct the signing json;
Commonly the `server.publicUrl` should be the same value as the `trusted_third_party_id_servers` property in the synapse config.
## Storage ## Storage
### SQLite ### SQLite
`storage.provider.sqlite.database`: Absolute location of the SQLite database `storage.provider.sqlite.database`: Absolute location of the SQLite database

View File

@@ -131,7 +131,7 @@ trusted_third_party_id_servers:
It is **highly recommended** to remove `matrix.org` and `vector.im` (or any other default entry) from your configuration It is **highly recommended** to remove `matrix.org` and `vector.im` (or any other default entry) from your configuration
so only your own Identity server is authoritative for your HS. so only your own Identity server is authoritative for your HS.
## Validate ## Validate (Under reconstruction)
**NOTE:** In case your homeserver has no working federation, step 5 will not happen. If step 4 took place, consider **NOTE:** In case your homeserver has no working federation, step 5 will not happen. If step 4 took place, consider
your installation validated. your installation validated.

View File

@@ -0,0 +1,16 @@
# Migration from mxisd
Version 2.0.0 of the ma1sd uses the same format of the database schema and main configuration file as mxisd.
Migration from mxisd:
- install ma1sd via deb package, docker image or zip/tar archive
- stop mxisd
- copy configuration file (by default /etc/mxisd/mxisd.yaml to /etc/ma1sd/ma1sd.yaml)
- copy key store (by default /var/lib/mxisd/keys folder to /var/lib/ma1sd/keys)
- copy storage (by default /var/lib/mxisd/store.db to /var/lib/ma1sd/store.db)
- change paths in the new config file (ma1sd.yaml). There are options: `key.path` and `storage.provider.sqlite`
- start ma1sd
Due to ma1sd uses the same ports by default as mxisd it isn't necessary to change nginx/apache configuration.
If you have any troubles with migration don't hesitate to ask questions in [#ma1sd:ru-matrix.org](https://matrix.to/#/#ma1sd:ru-matrix.org) room.

View File

@@ -31,8 +31,8 @@ notification:
text: <Path to file containing the raw text part of the email. Do not set to not use one> text: <Path to file containing the raw text part of the email. Do not set to not use one>
html: <Path to file containing the HTML part of the email. Do not set to not use one> html: <Path to file containing the HTML part of the email. Do not set to not use one>
unbind: unbind:
fraudulent: notification:
subject: <Subject of the email notification sent for potentially fraudulent 3PID unbinds> subject: <Subject of the email notification sent for 3PID unbinds>
body: body:
text: <Path to file containing the raw text part of the email. Do not set to not use one> text: <Path to file containing the raw text part of the email. Do not set to not use one>
html: <Path to file containing the raw text part of the email. Do not set to not use one> html: <Path to file containing the raw text part of the email. Do not set to not use one>

View File

@@ -9,7 +9,7 @@ provide your own custom templates.
Templates for the following events/actions are available: Templates for the following events/actions are available:
- [3PID invite](../../features/identity.md) - [3PID invite](../../features/identity.md)
- [3PID session: validation](../session/session.md) - [3PID session: validation](../session/session.md)
- [3PID session: fraudulent unbind](https://github.com/kamax-matrix/ma1sd/wiki/ma1sd-and-your-privacy#improving-your-privacy-one-commit-at-the-time) - [3PID session: unbind](https://github.com/kamax-matrix/ma1sd/wiki/ma1sd-and-your-privacy#improving-your-privacy-one-commit-at-the-time)
- [Matrix ID invite](../../features/experimental/application-service.md#email-notification-about-room-invites-by-matrix-ids) - [Matrix ID invite](../../features/experimental/application-service.md#email-notification-about-room-invites-by-matrix-ids)
## Placeholders ## Placeholders
@@ -71,7 +71,7 @@ under the namespace `threepid.medium.<medium>.generators.template`.
Under such namespace, the following keys are available: Under such namespace, the following keys are available:
- `invite`: Path to the 3PID invite notification template - `invite`: Path to the 3PID invite notification template
- `session.validation`: Path to the 3PID session validation notification template - `session.validation`: Path to the 3PID session validation notification template
- `session.unbind.fraudulent`: Path to the 3PID session fraudulent unbind notification template - `session.unbind`: Path to the 3PID session unbind notification template
- `generic.matrixId`: Path to the Matrix ID invite notification template - `generic.matrixId`: Path to the Matrix ID invite notification template
- `placeholder`: Map of key/values to set static values for some placeholders. - `placeholder`: Map of key/values to set static values for some placeholders.
@@ -104,7 +104,7 @@ threepid:
session: session:
validation: '/path/to/validate-template.eml' validation: '/path/to/validate-template.eml'
unbind: unbind:
fraudulent: '/path/to/unbind-fraudulent-template.eml' notification: '/path/to/unbind-notification-template.eml'
generic: generic:
matrixId: '/path/to/mxid-invite-template.eml' matrixId: '/path/to/mxid-invite-template.eml'
placeholder: placeholder:

View File

@@ -103,8 +103,8 @@ session:
validation: validation:
enabled: true enabled: true
unbind: unbind:
fraudulent: notifications: true
sendWarning: true enabled: true
# DO NOT COPY/PASTE AS-IS IN YOUR CONFIGURATION # DO NOT COPY/PASTE AS-IS IN YOUR CONFIGURATION
# CONFIGURATION EXAMPLE # CONFIGURATION EXAMPLE
@@ -115,11 +115,7 @@ are allowed to do in terms of 3PID sessions. The policy has a global on/off swit
--- ---
`unbind.fraudulent` controls warning notifications if an illegal/fraudulent 3PID removal is attempted on the Identity server. `unbind` controls warning notifications for 3PID removal. Setting `notifications` for `unbind` to false will prevent unbind emails from sending.
This is directly related to synapse disregard for privacy and new GDPR laws in Europe in an attempt to inform users about
potential privacy leaks.
For more information, see the corresponding [synapse issue](https://github.com/matrix-org/synapse/issues/4540).
### Web views ### Web views
Once a user click on a validation link, it is taken to the Identity Server validation page where the token is submitted. Once a user click on a validation link, it is taken to the Identity Server validation page where the token is submitted.

View File

@@ -118,7 +118,7 @@ public class Mxisd {
idStrategy = new RecursivePriorityLookupStrategy(cfg.getLookup(), ThreePidProviders.get(), bridgeFetcher); idStrategy = new RecursivePriorityLookupStrategy(cfg.getLookup(), ThreePidProviders.get(), bridgeFetcher);
pMgr = new ProfileManager(ProfileProviders.get(), clientDns, httpClient); pMgr = new ProfileManager(ProfileProviders.get(), clientDns, httpClient);
notifMgr = new NotificationManager(cfg.getNotification(), NotificationHandlers.get()); notifMgr = new NotificationManager(cfg.getNotification(), NotificationHandlers.get());
sessMgr = new SessionManager(cfg.getSession(), cfg.getMatrix(), store, notifMgr); sessMgr = new SessionManager(cfg, store, notifMgr, resolver, httpClient, signMgr);
invMgr = new InvitationManager(cfg, store, idStrategy, keyMgr, signMgr, resolver, notifMgr, pMgr); invMgr = new InvitationManager(cfg, store, idStrategy, keyMgr, signMgr, resolver, notifMgr, pMgr);
authMgr = new AuthManager(cfg, AuthProviders.get(), idStrategy, invMgr, clientDns, httpClient); authMgr = new AuthManager(cfg, AuthProviders.get(), idStrategy, invMgr, clientDns, httpClient);
dirMgr = new DirectoryManager(cfg.getDirectory(), clientDns, httpClient, DirectoryProviders.get()); dirMgr = new DirectoryManager(cfg.getDirectory(), clientDns, httpClient, DirectoryProviders.get());

View File

@@ -140,7 +140,7 @@ public class AuthManager {
} }
try { try {
MatrixID.asValid(mxId); MatrixID.asAcceptable(mxId);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
log.warn("The returned User ID {} is not a valid Matrix ID. Login might fail at the Homeserver level", mxId); log.warn("The returned User ID {} is not a valid Matrix ID. Login might fail at the Homeserver level", mxId);
} }

View File

@@ -46,34 +46,31 @@ public class SessionConfig {
public static class PolicyUnbind { public static class PolicyUnbind {
public static class PolicyUnbindFraudulent { private boolean enabled = true;
private boolean sendWarning = true; private boolean notifications = true;
public boolean getSendWarning() { public boolean getEnabled() {
return sendWarning; return enabled;
}
public void setSendWarning(boolean sendWarning) {
this.sendWarning = sendWarning;
}
} }
public void setEnabled(boolean enabled) {
private PolicyUnbindFraudulent fraudulent = new PolicyUnbindFraudulent(); this.enabled = enabled;
public PolicyUnbindFraudulent getFraudulent() {
return fraudulent;
} }
public void setFraudulent(PolicyUnbindFraudulent fraudulent) { public boolean shouldNotify() {
this.fraudulent = fraudulent; return notifications;
} }
public void setNotifications(boolean notifications) {
this.notifications = notifications;
}
} }
public Policy() { public Policy() {
validation.enabled = true; validation.enabled = true;
unbind.enabled = true;
unbind.notifications = true;
} }
private PolicyTemplate validation = new PolicyTemplate(); private PolicyTemplate validation = new PolicyTemplate();

View File

@@ -115,24 +115,10 @@ public class EmailSendGridConfig {
public static class Templates { public static class Templates {
public static class TemplateSessionUnbind {
private EmailTemplate fraudulent = new EmailTemplate();
public EmailTemplate getFraudulent() {
return fraudulent;
}
public void setFraudulent(EmailTemplate fraudulent) {
this.fraudulent = fraudulent;
}
}
public static class TemplateSession { public static class TemplateSession {
private EmailTemplate validation = new EmailTemplate(); private EmailTemplate validation = new EmailTemplate();
private TemplateSessionUnbind unbind = new TemplateSessionUnbind(); private EmailTemplate unbind = new EmailTemplate();
public EmailTemplate getValidation() { public EmailTemplate getValidation() {
return validation; return validation;
@@ -142,11 +128,11 @@ public class EmailSendGridConfig {
this.validation = validation; this.validation = validation;
} }
public TemplateSessionUnbind getUnbind() { public EmailTemplate getUnbind() {
return unbind; return unbind;
} }
public void setUnbind(TemplateSessionUnbind unbind) { public void setUnbind(EmailTemplate unbind) {
this.unbind = unbind; this.unbind = unbind;
} }

View File

@@ -31,7 +31,7 @@ public class EmailTemplateConfig extends GenericTemplateConfig {
setInvite("classpath:/threepids/email/invite-template.eml"); setInvite("classpath:/threepids/email/invite-template.eml");
getGeneric().put("matrixId", "classpath:/threepids/email/mxid-template.eml"); getGeneric().put("matrixId", "classpath:/threepids/email/mxid-template.eml");
getSession().setValidation("classpath:/threepids/email/validate-template.eml"); getSession().setValidation("classpath:/threepids/email/validate-template.eml");
getSession().getUnbind().setFraudulent("classpath:/threepids/email/unbind-fraudulent.eml"); getSession().getUnbind().setNotification("classpath:/threepids/email/unbind-notification.eml");
} }
public EmailTemplateConfig build() { public EmailTemplateConfig build() {
@@ -40,7 +40,7 @@ public class EmailTemplateConfig extends GenericTemplateConfig {
log.info("Session:"); log.info("Session:");
log.info(" Validation: {}", getSession().getValidation()); log.info(" Validation: {}", getSession().getValidation());
log.info(" Unbind:"); log.info(" Unbind:");
log.info(" Fraudulent: {}", getSession().getUnbind().getFraudulent()); log.info(" Notification: {}", getSession().getUnbind().getNotification());
return this; return this;
} }

View File

@@ -41,16 +41,25 @@ public class GenericTemplateConfig {
public static class SessionUnbind { public static class SessionUnbind {
private String fraudulent; private String validation;
public String getFraudulent() { private String notification;
return fraudulent;
public String getValidation() {
return validation;
} }
public void setFraudulent(String fraudulent) { public void setValidation(String validation) {
this.fraudulent = fraudulent; this.validation = validation;
} }
public String getNotification() {
return notification;
}
public void setNotification(String notification) {
this.notification = notification;
}
} }
private String validation; private String validation;

View File

@@ -30,7 +30,8 @@ public class PhoneSmsTemplateConfig extends GenericTemplateConfig {
public PhoneSmsTemplateConfig() { public PhoneSmsTemplateConfig() {
setInvite("classpath:/threepids/sms/invite-template.txt"); setInvite("classpath:/threepids/sms/invite-template.txt");
getSession().setValidation("classpath:/threepids/sms/validate-template.txt"); getSession().setValidation("classpath:/threepids/sms/validate-template.txt");
getSession().getUnbind().setFraudulent("classpath:/threepids/sms/unbind-fraudulent.txt"); getSession().getUnbind().setValidation("classpath:/threepids/sms/unbind-validation.txt");
getSession().getUnbind().setNotification("classpath:/threepids/sms/unbind-notification.txt");
} }
public PhoneSmsTemplateConfig build() { public PhoneSmsTemplateConfig build() {
@@ -39,7 +40,8 @@ public class PhoneSmsTemplateConfig extends GenericTemplateConfig {
log.info("Session:"); log.info("Session:");
log.info(" Validation: {}", getSession().getValidation()); log.info(" Validation: {}", getSession().getValidation());
log.info(" Unbind:"); log.info(" Unbind:");
log.info(" Fraudulent: {}", getSession().getUnbind().getFraudulent()); log.info(" Validation: {}", getSession().getUnbind().getValidation());
log.info(" Notification: {}", getSession().getUnbind().getNotification());
return this; return this;
} }

View File

@@ -58,5 +58,4 @@ public class CryptoFactory {
public static SignatureManager getSignatureManager(MxisdConfig cfg, Ed25519KeyManager keyMgr) { public static SignatureManager getSignatureManager(MxisdConfig cfg, Ed25519KeyManager keyMgr) {
return new Ed25519SignatureManager(cfg, keyMgr); return new Ed25519SignatureManager(cfg, keyMgr);
} }
} }

View File

@@ -26,6 +26,7 @@ import io.kamax.matrix.event.EventKey;
import io.kamax.matrix.json.MatrixJson; import io.kamax.matrix.json.MatrixJson;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.util.Objects; import java.util.Objects;
public interface SignatureManager { public interface SignatureManager {
@@ -106,4 +107,13 @@ public interface SignatureManager {
*/ */
Signature sign(byte[] data); Signature sign(byte[] data);
/**
* Verify the data.
*
* @param publicKey public key to verify
* @param signature signature to verify
* @param data the data to verify
* @return {@code true} if signature is valid, else {@code false}
*/
boolean verify(PublicKey publicKey, String signature, byte[] data);
} }

View File

@@ -33,7 +33,9 @@ import net.i2p.crypto.eddsa.EdDSAEngine;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SignatureException; import java.security.SignatureException;
import java.util.Base64;
public class Ed25519SignatureManager implements SignatureManager { public class Ed25519SignatureManager implements SignatureManager {
@@ -92,4 +94,15 @@ public class Ed25519SignatureManager implements SignatureManager {
} }
} }
@Override
public boolean verify(PublicKey publicKey, String signature, byte[] data) {
try {
EdDSAEngine signEngine = new EdDSAEngine(MessageDigest.getInstance(keyMgr.getKeySpecs().getHashAlgorithm()));
signEngine.initVerify(publicKey);
signEngine.update(data);
return signEngine.verify(Base64.getDecoder().decode(signature));
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
throw new RuntimeException(e);
}
}
} }

View File

@@ -21,13 +21,10 @@
package io.kamax.mxisd.http.undertow.handler.identity.v1; package io.kamax.mxisd.http.undertow.handler.identity.v1;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.exception.NotAllowedException;
import io.kamax.mxisd.http.IsAPIv1; import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.session.SessionManager; import io.kamax.mxisd.session.SessionManager;
import io.undertow.server.HttpServerExchange; import io.undertow.server.HttpServerExchange;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -46,20 +43,9 @@ public class SessionTpidUnbindHandler extends BasicHttpHandler {
@Override @Override
public void handleRequest(HttpServerExchange exchange) { public void handleRequest(HttpServerExchange exchange) {
String auth = exchange.getRequestHeaders().getFirst("Authorization"); String auth = exchange.getRequestHeaders().getFirst("Authorization");
if (StringUtils.isNotEmpty(auth)) {
// We have a auth header to process
if (StringUtils.startsWith(auth, "X-Matrix ")) {
log.warn("A remote host attempted to unbind without proper authorization. Request was denied");
log.warn("See https://github.com/kamax-matrix/mxisd/wiki/mxisd-and-your-privacy for more info");
throw new NotAllowedException("3PID can only be removed via 3PID sessions, not via Homeserver signature");
} else {
throw new BadRequestException("Illegal authorization type");
}
}
JsonObject body = parseJsonObject(exchange); JsonObject body = parseJsonObject(exchange);
sessionMgr.unbind(body); sessionMgr.unbind(auth, body);
writeBodyAsUtf8(exchange, "{}"); writeBodyAsUtf8(exchange, "{}");
} }
} }

View File

@@ -37,6 +37,6 @@ public interface NotificationHandler {
void sendForValidation(IThreePidSession session); void sendForValidation(IThreePidSession session);
void sendForFraudulentUnbind(ThreePid tpid); void sendForUnbind(ThreePid tpid);
} }

View File

@@ -78,8 +78,8 @@ public class NotificationManager {
ensureMedium(session.getThreePid().getMedium()).sendForValidation(session); ensureMedium(session.getThreePid().getMedium()).sendForValidation(session);
} }
public void sendForFraudulentUnbind(ThreePid tpid) throws NotImplementedException { public void sendForUnbind(ThreePid tpid) throws NotImplementedException {
ensureMedium(tpid.getMedium()).sendForFraudulentUnbind(tpid); ensureMedium(tpid.getMedium()).sendForUnbind(tpid);
} }
} }

View File

@@ -20,52 +20,74 @@
package io.kamax.mxisd.session; package io.kamax.mxisd.session;
import static io.kamax.mxisd.config.SessionConfig.Policy.PolicyTemplate;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import io.kamax.matrix.MatrixID; import io.kamax.matrix.MatrixID;
import io.kamax.matrix.ThreePid; import io.kamax.matrix.ThreePid;
import io.kamax.matrix._MatrixID; import io.kamax.matrix._MatrixID;
import io.kamax.matrix.json.GsonUtil; import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.config.MatrixConfig; import io.kamax.matrix.json.MatrixJson;
import io.kamax.mxisd.config.SessionConfig; import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.crypto.SignatureManager;
import io.kamax.mxisd.exception.BadRequestException; import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.exception.NotAllowedException; import io.kamax.mxisd.exception.NotAllowedException;
import io.kamax.mxisd.exception.RemoteHomeServerException;
import io.kamax.mxisd.exception.SessionNotValidatedException; import io.kamax.mxisd.exception.SessionNotValidatedException;
import io.kamax.mxisd.exception.SessionUnknownException; import io.kamax.mxisd.exception.SessionUnknownException;
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.ThreePidValidation; import io.kamax.mxisd.lookup.ThreePidValidation;
import io.kamax.mxisd.matrix.HomeserverFederationResolver;
import io.kamax.mxisd.notification.NotificationManager; import io.kamax.mxisd.notification.NotificationManager;
import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.IStorage;
import io.kamax.mxisd.storage.dao.IThreePidSessionDao; import io.kamax.mxisd.storage.dao.IThreePidSessionDao;
import io.kamax.mxisd.threepid.session.ThreePidSession; import io.kamax.mxisd.threepid.session.ThreePidSession;
import net.i2p.crypto.eddsa.EdDSAPublicKey;
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec;
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Calendar;
import java.util.Optional; import java.util.Optional;
import static io.kamax.mxisd.config.SessionConfig.Policy.PolicyTemplate;
public class SessionManager { public class SessionManager {
private static final Logger log = LoggerFactory.getLogger(SessionManager.class); private static final Logger log = LoggerFactory.getLogger(SessionManager.class);
private SessionConfig cfg; private MxisdConfig cfg;
private MatrixConfig mxCfg;
private IStorage storage; private IStorage storage;
private NotificationManager notifMgr; private NotificationManager notifMgr;
private HomeserverFederationResolver resolver;
private CloseableHttpClient client;
private SignatureManager signatureManager;
public SessionManager( public SessionManager(
SessionConfig cfg, MxisdConfig cfg,
MatrixConfig mxCfg, IStorage storage,
IStorage storage, NotificationManager notifMgr,
NotificationManager notifMgr HomeserverFederationResolver resolver,
CloseableHttpClient client,
SignatureManager signatureManager
) { ) {
this.cfg = cfg; this.cfg = cfg;
this.mxCfg = mxCfg;
this.storage = storage; this.storage = storage;
this.notifMgr = notifMgr; this.notifMgr = notifMgr;
this.resolver = resolver;
this.client = client;
this.signatureManager = signatureManager;
} }
private ThreePidSession getSession(String sid, String secret) { private ThreePidSession getSession(String sid, String secret) {
@@ -86,7 +108,7 @@ public class SessionManager {
} }
public String create(String server, ThreePid tpid, String secret, int attempt, String nextLink) { public String create(String server, ThreePid tpid, String secret, int attempt, String nextLink) {
PolicyTemplate policy = cfg.getPolicy().getValidation(); PolicyTemplate policy = cfg.getSession().getPolicy().getValidation();
if (!policy.isEnabled()) { if (!policy.isEnabled()) {
throw new NotAllowedException("Validating 3PID is disabled"); throw new NotAllowedException("Validating 3PID is disabled");
} }
@@ -98,7 +120,8 @@ public class SessionManager {
ThreePidSession session = new ThreePidSession(dao.get()); ThreePidSession session = new ThreePidSession(dao.get());
log.info("We already have a session for {}: {}", tpid, session.getId()); log.info("We already have a session for {}: {}", tpid, session.getId());
if (session.getAttempt() < attempt) { if (session.getAttempt() < attempt) {
log.info("Received attempt {} is greater than stored attempt {}, sending validation communication", attempt, session.getAttempt()); log.info("Received attempt {} is greater than stored attempt {}, sending validation communication", attempt,
session.getAttempt());
notifMgr.sendForValidation(session); notifMgr.sendForValidation(session);
log.info("Sent validation notification to {}", tpid); log.info("Sent validation notification to {}", tpid);
session.increaseAttempt(); session.increaseAttempt();
@@ -161,12 +184,13 @@ public class SessionManager {
_MatrixID mxid = MatrixID.asAcceptable(mxidRaw); _MatrixID mxid = MatrixID.asAcceptable(mxidRaw);
// Only accept binds if the domain matches our own // Only accept binds if the domain matches our own
if (!StringUtils.equalsIgnoreCase(mxCfg.getDomain(), mxid.getDomain())) { final String domain = cfg.getMatrix().getDomain();
throw new NotAllowedException("Only Matrix IDs from domain " + mxCfg.getDomain() + " can be bound"); if (!StringUtils.equalsIgnoreCase(domain, mxid.getDomain())) {
throw new NotAllowedException("Only Matrix IDs from domain " + domain + " can be bound");
} }
log.info("Session {}: Binding of {}:{} to Matrix ID {} is accepted", log.info("Session {}: Binding of {}:{} to Matrix ID {} is accepted",
session.getId(), session.getThreePid().getMedium(), session.getThreePid().getAddress(), mxid.getId()); session.getId(), session.getThreePid().getMedium(), session.getThreePid().getAddress(), mxid.getId());
SingleLookupRequest request = new SingleLookupRequest(); SingleLookupRequest request = new SingleLookupRequest();
request.setType(session.getThreePid().getMedium()); request.setType(session.getThreePid().getMedium());
@@ -174,7 +198,12 @@ public class SessionManager {
return new SingleLookupReply(request, mxid); return new SingleLookupReply(request, mxid);
} }
public void unbind(JsonObject reqData) { public void unbind(String auth, JsonObject reqData) {
if (!cfg.getSession().getPolicy().getUnbind().getEnabled()) {
log.error("Unbind disabled.");
throw new NotAllowedException("Unbinding 3PID is disabled");
}
_MatrixID mxid; _MatrixID mxid;
try { try {
mxid = MatrixID.asAcceptable(GsonUtil.getStringOrThrow(reqData, "mxid")); mxid = MatrixID.asAcceptable(GsonUtil.getStringOrThrow(reqData, "mxid"));
@@ -186,6 +215,146 @@ public class SessionManager {
String secret = GsonUtil.getStringOrNull(reqData, "client_secret"); String secret = GsonUtil.getStringOrNull(reqData, "client_secret");
ThreePid tpid = GsonUtil.get().fromJson(GsonUtil.getObj(reqData, "threepid"), ThreePid.class); ThreePid tpid = GsonUtil.get().fromJson(GsonUtil.getObj(reqData, "threepid"), ThreePid.class);
if (tpid == null || StringUtils.isBlank(tpid.getAddress()) || StringUtils.isBlank(tpid.getMedium())) {
throw new BadRequestException("Missing required 3PID");
}
// We only allow unbind for the domain we manage, mirroring bind
final CharSequence domain = cfg.getMatrix().getDomain();
if (!StringUtils.equalsIgnoreCase(domain, mxid.getDomain())) {
throw new NotAllowedException("Only Matrix IDs from domain " + domain + " can be unbound");
}
log.info("Request was authorized.");
if (StringUtils.isNotBlank(sid) && StringUtils.isNotBlank(secret)) {
checkSession(sid, secret, tpid);
} else if (StringUtils.isNotBlank(auth)) {
checkAuthorization(auth, reqData);
} else {
throw new NotAllowedException("Unable to validate request");
}
log.info("Unbinding of {} {} to {} is accepted", tpid.getMedium(), tpid.getAddress(), mxid.getId());
if (cfg.getSession().getPolicy().getUnbind().shouldNotify()) {
notifMgr.sendForUnbind(tpid);
}
}
private void checkAuthorization(String auth, JsonObject reqData) {
if (!auth.startsWith("X-Matrix ")) {
throw new NotAllowedException("Wrong authorization header");
}
if (StringUtils.isBlank(cfg.getServer().getPublicUrl())) {
throw new NotAllowedException("Unable to verify request, missing `server.publicUrl` property");
}
String[] params = auth.substring("X-Matrix ".length()).split(",");
String origin = null;
String key = null;
String sig = null;
for (String param : params) {
String[] paramItems = param.split("=");
String paramKey = paramItems[0];
String paramValue = paramItems[1];
switch (paramKey) {
case "origin":
origin = removeQuotes(paramValue);
break;
case "key":
key = removeQuotes(paramValue);
break;
case "sig":
sig = removeQuotes(paramValue);
break;
default:
log.error("Unknown parameter: {}", param);
throw new BadRequestException("Authorization with unknown parameter");
}
}
if (StringUtils.isBlank(origin) || StringUtils.isBlank(key) || StringUtils.isBlank(sig)) {
log.error("Missing required parameters");
throw new BadRequestException("Missing required header parameters");
}
if (!cfg.getMatrix().getDomain().equalsIgnoreCase(origin)) {
throw new NotAllowedException("Only Matrix IDs from domain " + origin + " can be unbound");
}
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("method", "POST");
jsonObject.addProperty("uri", "/_matrix/identity/api/v1/3pid/unbind");
jsonObject.addProperty("origin", origin);
jsonObject.addProperty("destination_is", URI.create(cfg.getServer().getPublicUrl()).getHost());
jsonObject.add("content", reqData);
String canonical = MatrixJson.encodeCanonical(jsonObject);
String originUrl = resolver.resolve(origin).toString();
validateServerKey(key, sig, canonical, originUrl);
}
private String removeQuotes(String origin) {
return origin.startsWith("\"") && origin.endsWith("\"") ? origin.substring(1, origin.length() - 1) : origin;
}
private void validateServerKey(String key, String signature, String canonical, String originUrl) {
HttpGet request = new HttpGet(originUrl + "/_matrix/key/v2/server");
log.info("Get keys from the server {}", request.getURI());
try (CloseableHttpResponse response = client.execute(request)) {
int statusCode = response.getStatusLine().getStatusCode();
log.info("Answer code: {}", statusCode);
if (statusCode == 200) {
verifyKey(key, signature, canonical, response);
} else {
throw new RemoteHomeServerException("Unable to fetch server keys.");
}
} catch (IOException e) {
String message = "Unable to get server keys: " + originUrl;
log.error(message, e);
throw new IllegalArgumentException(message);
}
}
private void verifyKey(String key, String signature, String canonical, CloseableHttpResponse response) throws IOException {
final String content = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
log.info("Answer body: {}", content);
final JsonObject responseObject = GsonUtil.parseObj(content);
final long validUntilTs = GsonUtil.getLong(responseObject, "valid_until_ts");
final Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(validUntilTs);
if (calendar.before(Calendar.getInstance())) {
final String msg = "Key is expired";
log.error(msg);
throw new RemoteHomeServerException(msg);
}
final JsonObject verifyKeys = GsonUtil.getObj(responseObject, "verify_keys");
final JsonObject keyObject = GsonUtil.getObj(verifyKeys, key);
final String publicKey = GsonUtil.getStringOrNull(keyObject, "key");
if (StringUtils.isBlank(publicKey)) {
throw new RemoteHomeServerException("Missing server key.");
}
EdDSANamedCurveSpec ed25519CurveSpec = EdDSANamedCurveTable.ED_25519_CURVE_SPEC;
EdDSAPublicKeySpec publicKeySpec = new EdDSAPublicKeySpec(Base64.getDecoder().decode(publicKey), ed25519CurveSpec);
EdDSAPublicKey dsaPublicKey = new EdDSAPublicKey(publicKeySpec);
final boolean verificationResult = signatureManager.verify(dsaPublicKey, signature, canonical.getBytes(StandardCharsets.UTF_8));
log.info("Verification result: {}", verificationResult);
if (!verificationResult) {
throw new RemoteHomeServerException("Unable to verify request.");
}
log.info("Request was authorized.");
}
private void checkSession(String sid, String secret, ThreePid tpid) {
// We ensure the session was validated // We ensure the session was validated
ThreePidSession session = getSessionIfValidated(sid, secret); ThreePidSession session = getSessionIfValidated(sid, secret);
@@ -193,14 +362,5 @@ public class SessionManager {
if (!session.getThreePid().equals(tpid)) { if (!session.getThreePid().equals(tpid)) {
throw new BadRequestException("3PID to unbind does not match the one from the validated session"); throw new BadRequestException("3PID to unbind does not match the one from the validated session");
} }
// We only allow unbind for the domain we manage, mirroring bind
if (!StringUtils.equalsIgnoreCase(mxCfg.getDomain(), mxid.getDomain())) {
throw new NotAllowedException("Only Matrix IDs from domain " + mxCfg.getDomain() + " can be unbound");
}
log.info("Session {}: Unbinding of {}:{} to Matrix ID {} is accepted",
session.getId(), session.getThreePid().getMedium(), session.getThreePid().getAddress(), mxid.getId());
} }
} }

View File

@@ -115,7 +115,7 @@ public class EmailSmtpConnector implements EmailConnector {
msg.setRecipients(Message.RecipientType.TO, recipient); msg.setRecipients(Message.RecipientType.TO, recipient);
msg.saveChanges(); msg.saveChanges();
log.info("Sending invite to {} via SMTP using {}:{}", recipient, cfg.getHost(), cfg.getPort()); log.info("Sending email to {} via SMTP using {}:{}", recipient, cfg.getHost(), cfg.getPort());
SMTPTransport transport = (SMTPTransport) session.getTransport("smtp"); SMTPTransport transport = (SMTPTransport) session.getTransport("smtp");
if (cfg.getTls() < 3) { if (cfg.getTls() < 3) {
@@ -134,12 +134,12 @@ public class EmailSmtpConnector implements EmailConnector {
try { try {
transport.sendMessage(msg, InternetAddress.parse(recipient)); transport.sendMessage(msg, InternetAddress.parse(recipient));
log.info("Invite to {} was sent", recipient); log.info("Email to {} was sent", recipient);
} finally { } finally {
transport.close(); transport.close();
} }
} catch (UnsupportedEncodingException | MessagingException e) { } catch (UnsupportedEncodingException | MessagingException e) {
throw new RuntimeException("Unable to send e-mail invite to " + recipient, e); throw new RuntimeException("Unable to send e-mail to " + recipient, e);
} }
} }

View File

@@ -79,9 +79,9 @@ public abstract class GenericTemplateNotificationGenerator extends PlaceholderNo
} }
@Override @Override
public String getForFraudulentUnbind(ThreePid tpid) { public String getForNotificationUnbind(ThreePid tpid) {
log.info("Generating notification content for fraudulent unbind"); log.info("Generating notification content for unbind");
return populateForFraudulentUndind(tpid, getTemplateContent(cfg.getSession().getUnbind().getFraudulent())); return populateForNotificationUndind(tpid, getTemplateContent(cfg.getSession().getUnbind().getNotification()));
} }
} }

View File

@@ -37,6 +37,6 @@ public interface NotificationGenerator {
String getForValidation(IThreePidSession session); String getForValidation(IThreePidSession session);
String getForFraudulentUnbind(ThreePid tpid); String getForNotificationUnbind(ThreePid tpid);
} }

View File

@@ -127,7 +127,7 @@ public abstract class PlaceholderNotificationGenerator {
.replace("%NEXT_URL%", validationLink); .replace("%NEXT_URL%", validationLink);
} }
protected String populateForFraudulentUndind(ThreePid tpid, String input) { protected String populateForNotificationUndind(ThreePid tpid, String input) {
return populateForCommon(tpid, input); return populateForCommon(tpid, input);
} }

View File

@@ -73,8 +73,8 @@ public abstract class GenericNotificationHandler<A extends ThreePidConnector, B
} }
@Override @Override
public void sendForFraudulentUnbind(ThreePid tpid) { public void sendForUnbind(ThreePid tpid) {
send(connector, tpid.getAddress(), generator.getForFraudulentUnbind(tpid)); send(connector, tpid.getAddress(), generator.getForNotificationUnbind(tpid));
} }
} }

View File

@@ -129,10 +129,10 @@ public class EmailSendGridNotificationHandler extends PlaceholderNotificationGen
} }
@Override @Override
public void sendForFraudulentUnbind(ThreePid tpid) { public void sendForUnbind(ThreePid tpid) {
EmailTemplate template = cfg.getTemplates().getSession().getUnbind().getFraudulent(); EmailTemplate template = cfg.getTemplates().getSession().getUnbind();
if (StringUtils.isAllBlank(template.getBody().getText(), template.getBody().getHtml())) { if (StringUtils.isAllBlank(template.getBody().getText(), template.getBody().getHtml())) {
throw new FeatureNotAvailable("No template has been configured for fraudulent unbind notifications"); throw new FeatureNotAvailable("No template has been configured for unbind notifications");
} }
Email email = getEmail(); Email email = getEmail();

View File

@@ -1,135 +0,0 @@
Subject: IMPORTANT - %DOMAIN% Matrix Identity Server - Unauthorized 3PID unbind blocked
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary="7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ"
--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ
Content-Type: text/plain; charset=UTF-8
Content-Disposition: inline
Hi,
**THIS IS IMPORTANT, PLEASE READ CAREFULLY**.
If you are the system administrator of the Matrix installation, read the second section.
This is a notification email that a possibly unauthorized entity has attempted to alter your
3PIDs (email, phone numbers, etc.) settings. The request was denied and no change has been made.
This is so you are aware of a possible failure in case you just tried to remove a 3PID from your account.
If you do not understand this email, please forward it to your System administrator.
-----------
As the system administrator:
If you are using synapse as a Homeserver, this is a known issue related to MSC1194 [1] and abuse of separation of concerns.
As a privacy-centric product and to protect your privacy, the request was actively blocked. We have written a more detailed
explanation on our Privacy wiki page [2] (Direct link [3]) so you can fully grasp the impact for you and your users.
We have open an issue [4] on the synapse repos to reflect the related privacy concerns and GDPR violation(s) and would
appreciate if you could comment on it or simply adds a thumbs up so the concerns are finally dealt with by the synapse dev team.
If you are using another Homeserver or this came following no action from your own users, then you have been the target
of an unbind attack from a rogue entity which was blocked. You may want to check your logs to see the exact source of
the attack and take relevant actions following your policy.
If you would like to disable these notifications, please see the 3PID sessions configuration documentation [5].
Thanks,
%DOMAIN_PRETTY% Admins
---
[1] https://github.com/matrix-org/matrix-doc/issues/1194
[2] https://github.com/kamax-matrix/mxisd/wiki/mxisd-and-your-privacy
[3] https://github.com/kamax-matrix/mxisd/wiki/mxisd-and-your-privacy#msc1194-synapse-and-impacts-on-your-privacy
[4] https://github.com/matrix-org/synapse/issues/4540
[5] https://github.com/kamax-matrix/mxisd/blob/master/docs/threepids/session/session.md#configuration
--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ
Content-Type: multipart/related;
boundary="M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR";
type="text/html"
--M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR
Content-Type: text/html; charset=UTF-8
Content-Disposition: inline
<!doctype html>
<html lang="en">
<head>
<style type="text/css">
body {
margin: 0px;
}
pre, code {
word-break: break-word;
white-space: pre-wrap;
}
#page {
font-family: 'Open Sans', Helvetica, Arial, Sans-Serif;
font-color: #454545;
font-size: 12pt;
width: 100%%;
padding: 20px;
}
#inner {
width: 640px;
}
</style>
</head>
<body>
<table id="page">
<tr>
<td> </td>
<td id="inner">
<p>Hi,</p>
<p><b>THIS IS IMPORTANT, PLEASE READ CAREFULLY</b>.<br/>
If you are the system administrator of the Matrix installation, read the second section.</p>
<p>This is a notification email that a possibly unauthorized entity has attempted to alter your
3PIDs (email, phone numbers, etc.) settings. The request was denied and no change has been made.</p>
<p>This is so you are aware of a possible failure in case you just tried to remove a 3PID from your account.</p>
<p>If you do not understand this email, please forward it to your System administrator.</p>
<hr>
<p>As the system administrator:</p>
<p>If you are using synapse as a Homeserver, this is a known issue related to <a href="https://github.com/matrix-org/matrix-doc/issues/1194">MSC1194</a>
and abuse of separation of concerns. As a privacy-centric product and to protect your privacy, the request was actively
blocked. We have written a more detailed explanation on our <a href="https://github.com/kamax-matrix/mxisd/wiki/mxisd-and-your-privacy">Privacy wiki page</a>
(<a href="https://github.com/kamax-matrix/mxisd/wiki/mxisd-and-your-privacy#msc1194-synapse-and-impacts-on-your-privacy">Direct link to section</a>)
so you can fully grasp the impact for you and your users.</p>
<p>We have open an issue on the synapse repos to reflect the related privacy concerns and GDPR violation(s) and would
appreciate if you could comment on it or simply adds a thumbs up so the concerns are finally dealt with by the synapse dev team.<br/>
Issue: <a href="https://github.com/matrix-org/synapse/issues/4540">https://github.com/matrix-org/synapse/issues/4540</a></p>
<p>If you are using another Homeserver or this came following no action from your own users, then you have been the target
of an unbind attack from a rogue entity which was blocked. You may want to check your logs to see the exact source of
the attack and take relevant actions following your policy.</p>
<p>If you would like to disable these notifications, please see the
<a href="https://github.com/kamax-matrix/mxisd/blob/master/docs/threepids/session/session.md#configuration">3PID sessions configuration documentation.</a></p>
<p>Thanks,</p>
<p>%DOMAIN_PRETTY% Admins</p>
</td>
<td> </td>
</tr>
</table>
</body>
</html>
--M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR--
--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ--

View File

@@ -0,0 +1,77 @@
Subject: Unbind 3PID
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary="7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ"
--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ
Content-Type: text/plain; charset=UTF-8
Content-Disposition: inline
Hello there!
You or a server on your behalf unbinded your email.
If you didn't make this request, please contact the system administrator.
%DOMAIN_PRETTY% Admins
--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ
Content-Type: multipart/related;
boundary="M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR";
type="text/html"
--M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR
Content-Type: text/html; charset=UTF-8
Content-Disposition: inline
<!doctype html>
<html lang="en">
<head>
<style type="text/css">
body {
margin: 0px;
}
pre, code {
word-break: break-word;
white-space: pre-wrap;
}
#page {
font-family: 'Open Sans', Helvetica, Arial, Sans-Serif;
font-color: #454545;
font-size: 12pt;
width: 100%%;
padding: 20px;
}
#inner {
width: 640px;
}
.notif_link a, .footer a {
color: #76CFA6 ! important;
}
</style>
</head>
<body>
<table id="page">
<tr>
<td></td>
<td id="inner">
<p>Hello there!</p>
<p>You or a server on your behalf unbinded your email.</p>
<p>If you didn't make this request, please contact the system administrator.</p>
<p>%DOMAIN_PRETTY% Admins</p>
</td>
<td></td>
</tr>
</table>
</body>
</html>
--M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR--
--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ--

View File

@@ -1 +0,0 @@
INFORMATIONAL ONLY - Someone attempted to change your Matrix 3PIDs, with a potential data leak. Please contact your system administrator.

View File

@@ -0,0 +1 @@
Your Matrix 3PID was unbinded.