Compare commits

...

10 Commits

Author SHA1 Message Date
Anatoly Sablin
15db563e8d Add documentation. 2019-12-26 22:49:25 +03:00
Anatoly Sablin
82a538c750 Add an option to enable/disable hash lookup via the LDAP provider. 2019-12-25 22:51:44 +03:00
Anatoly Sablin
84ca8ebbd9 Add support of the MSC2134 (Identity hash lookup) for the LDAP provider. 2019-12-25 00:13:07 +03:00
Anatoly Sablin
774ebf4fa8 Fix for #9. Proper wrap the handles with the sanitize handler. 2019-12-16 22:47:24 +03:00
Anatoly Sablin
eb1326c56a Add unique id for the accepted table.
Add a little more logs.
2019-12-10 22:29:00 +03:00
Anatoly Sablin
10cdb4360e Fix homeserver verification with wildcards certificates.
Disable v2 by default.
Add migration to fix the accepted table (due to sqlite unable to change constraint, drop table and create again).
Fix displaying the expiration period of the new token.
Remove duplicated code.
Use v1 single lookup when receive the request with `none` algorithm and the only one argument.
Hide v2 endpoint if v2 API disabled.
2019-12-10 00:10:13 +03:00
Anatoly Sablin
17ebc2a421 Fix hash generation. 2019-12-06 23:15:00 +03:00
Anatoly Sablin
cbb9fced8d Clarify the documentation. Add the hash config to the example config. Uses duration in the delay field instead of the seconds. 2019-12-05 23:27:13 +03:00
Anatoly Sablin
7509174611 Add documentation. Add options to enable/disable the hash providers. Add the option for setup barrier for rotation per requests strategy. 2019-12-02 23:23:17 +03:00
Anatoly Sablin
51d9225dda Don't wrap the AcceptTermsHandler with terms checking. Clear the sql hash storage on shutdown. 2019-12-02 22:31:36 +03:00
31 changed files with 546 additions and 150 deletions

View File

@@ -108,7 +108,7 @@ dependencies {
compile 'net.i2p.crypto:eddsa:0.3.0'
// LDAP connector
compile 'org.apache.directory.api:api-all:1.0.0'
compile 'org.apache.directory.api:api-all:1.0.3'
// DNS lookups
compile 'dnsjava:dnsjava:2.1.9'

View File

@@ -7,7 +7,7 @@ Default values:
```.yaml
matrix:
v1: true # deprecated
v2: true
v2: false
```
To disable change value to `false`.
@@ -17,9 +17,16 @@ NOTE: the v1 is deprecated, therefore recommend to use only v2 and disable v1 (d
matrix:
v1: false
```
NOTE: Riot Web version 1.5.5 and below checks the v1 for backward compatibility.
NOTE: v2 disabled by default in order to preserve backward compatibility.
## Terms
###### Requires: No.
Administrator can omit terms configuration. In this case the terms checking will be disabled.
Example:
```.yaml
policy:
@@ -35,13 +42,105 @@ policy:
url: https://ma1sd.host.tld/term_fr.html # localized url
regexp:
- '/_matrix/identity/v2/account.*'
- '/_matrix/identity/v2/hash_lookup'
- '/_matrix/identity/v2/hash_details'
- '/_matrix/identity/v2/lookup'
```
Where:
- `term_name` -- name of the terms.
- `regexp` -- regexp patterns for API.
- `version` -- the terms version.
- `lang` -- the term language.
- `name` -- the name of the term.
- `url` -- the url of the term. Might be any url (i.e. from another host) for a html page.
- `regexp` -- regexp patterns for API which should be available only after accepting the terms.
API will be checks for accepted terms only with authorization.
There are the next API:
- [`GET /_matrix/identity/v2/account`](https://matrix.org/docs/spec/identity_service/r0.3.0#get-matrix-identity-v2-account) - Gets information about what user owns the access token used in the request.
- [`POST /_matrix/identity/v2/account/logout`](https://matrix.org/docs/spec/identity_service/r0.3.0#post-matrix-identity-v2-account-logout) - Logs out the access token, preventing it from being used to authenticate future requests to the server.
- [`GET /_matrix/identity/v2/hash_details`](https://matrix.org/docs/spec/identity_service/r0.3.0#get-matrix-identity-v2-hash-details) - Gets parameters for hashing identifiers from the server. This can include any of the algorithms defined in this specification.
- [`POST /_matrix/identity/v2/lookup`](https://matrix.org/docs/spec/identity_service/r0.3.0#post-matrix-identity-v2-lookup) - Looks up the set of Matrix User IDs which have bound the 3PIDs given, if bindings are available. Note that the format of the addresses is defined later in this specification.
- [`POST /_matrix/identity/v2/validate/email/requestToken`](https://matrix.org/docs/spec/identity_service/r0.3.0#post-matrix-identity-v2-validate-email-requesttoken) - Create a session for validating an email address.
- [`POST /_matrix/identity/v2/validate/email/submitToken`](https://matrix.org/docs/spec/identity_service/r0.3.0#post-matrix-identity-v2-validate-email-submittoken) - Validate ownership of an email address.
- [`GET /_matrix/identity/v2/validate/email/submitToken`](https://matrix.org/docs/spec/identity_service/r0.3.0#get-matrix-identity-v2-validate-email-submittoken) - Validate ownership of an email address.
- [`POST /_matrix/identity/v2/validate/msisdn/requestToken`](https://matrix.org/docs/spec/identity_service/r0.3.0#post-matrix-identity-v2-validate-msisdn-requesttoken) - Create a session for validating a phone number.
- [`POST /_matrix/identity/v2/validate/msisdn/submitToken`](https://matrix.org/docs/spec/identity_service/r0.3.0#post-matrix-identity-v2-validate-msisdn-submittoken) - Validate ownership of a phone number.
- [`GET /_matrix/identity/v2/validate/msisdn/submitToken`](https://matrix.org/docs/spec/identity_service/r0.3.0#get-matrix-identity-v2-validate-msisdn-submittoken) - Validate ownership of a phone number.
- [`GET /_matrix/identity/v2/3pid/getValidated3pid`](https://matrix.org/docs/spec/identity_service/r0.3.0#get-matrix-identity-v2-3pid-getvalidated3pid) - Determines if a given 3pid has been validated by a user.
- [`POST /_matrix/identity/v2/3pid/bind`](https://matrix.org/docs/spec/identity_service/r0.3.0#post-matrix-identity-v2-3pid-bind) - Publish an association between a session and a Matrix user ID.
- [`POST /_matrix/identity/v2/3pid/unbind`](https://matrix.org/docs/spec/identity_service/r0.3.0#post-matrix-identity-v2-3pid-unbind) - Remove an association between a session and a Matrix user ID.
- [`POST /_matrix/identity/v2/store-invite`](https://matrix.org/docs/spec/identity_service/r0.3.0#post-matrix-identity-v2-store-invite) - Store pending invitations to a user's 3pid.
- [`POST /_matrix/identity/v2/sign-ed25519`](https://matrix.org/docs/spec/identity_service/r0.3.0#post-matrix-identity-v2-sign-ed25519) - Sign invitation details.
There is only one exception: [`POST /_matrix/identity/v2/terms`](https://matrix.org/docs/spec/identity_service/r0.3.0#post-matrix-identity-v2-terms) which uses for accepting the terms and requires the authorization.
## [Hash lookup](https://github.com/matrix-org/matrix-doc/blob/hs/hash-identity/proposals/2134-identity-hash-lookup.md)
Hashes and the pepper updates together according to the `rotationPolicy`.
###### Requires: No.
In case the `none` algorithms ma1sd will be lookup using the v1 bulk API.
```.yaml
hashing:
enabled: true # enable or disable the hash lookup MSC2140 (default is false)
pepperLength: 20 # length of the pepper value (default is 20)
rotationPolicy: per_requests # or `per_seconds` how often the hashes will be updating
hashStorageType: sql # or `in_memory` where the hashes will be stored
algorithms:
- none # the same as v1 bulk lookup
- sha256 # hash the 3PID and pepper.
delay: 2m # how often hashes will be updated if rotation policy = per_seconds (default is 10s)
requests: 10 # how many lookup requests will be performed before updating hashes if rotation policy = per_requests (default is 10)
```
When enabled and client requests the `none` algorithms then hash lookups works as v1 bulk lookup.
Delay specified in the format: `2d 4h 12m 34s` - this means 2 days 4 hours 12 minutes and 34 seconds. Zero units may be omitted. For example:
- 12s - 12 seconds
- 3m - 3 minutes
- 5m 6s - 5 minutes and 6 seconds
- 6h 3s - 6 hours and 3 seconds
## Hash lookup
Sha256 algorithm supports only sql, memory and exec 3PID providers.
For sql provider (i.e. for the `synapseSql`):
```.yaml
synapseSql:
lookup:
query: 'select user_id as mxid, medium, address from user_threepids' # query for retrive 3PIDs for hashes.
```
For general sql provider:
```.yaml
sql:
lookup:
query: 'select user as mxid, field1 as medium, field2 as address from some_table' # query for retrive 3PIDs for hashes.
```
Each query should return the `mxid`, `medium` and `address` fields.
For memory providers:
```.yaml
memory:
hashEnabled: true # enable the hash lookup (defaults is false)
```
For exec providers:
```.yaml
exec:
identity:
hashEnabled: true # enable the hash lookup (defaults is false)
```
For ldap providers:
```.yaml
ldap:
lookup: true
```
NOTE: Federation requests work only with `none` algorithms.

View File

@@ -89,7 +89,7 @@ ldap:
#### 3PIDs
You can also change the attribute lists for 3PID, like email or phone numbers.
The following example would overwrite the [default list of attributes](../../src/main/java/io/kamax/ma1sd/config/ldap/LdapConfig.java#L64)
The following example would overwrite the [default list of attributes](../../src/main/java/io/kamax/mxisd/config/ldap/LdapConfig.java#L64)
for emails and phone number:
```yaml
ldap:

View File

@@ -1,5 +1,6 @@
#Thu Dec 05 22:39:36 MSK 2019
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0-all.zip
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

View File

@@ -21,6 +21,8 @@
#
matrix:
domain: ''
v1: true # deprecated
v2: false # MSC2140 API v2. Disabled by default in order to preserve backward compatibility.
################
@@ -109,3 +111,59 @@ threepid:
# Password for the account
password: "ThePassword"
#### MSC2134 (hash lookup)
#hashing:
# enabled: false # enable or disable the hash lookup MSC2140 (default is false)
# pepperLength: 20 # length of the pepper value (default is 20)
# rotationPolicy: per_requests # or `per_seconds` how often the hashes will be updating
# hashStorageType: sql # or `in_memory` where the hashes will be stored
# algorithms:
# - none # the same as v1 bulk lookup
# - sha256 # hash the 3PID and pepper.
# delay: 2m # how often hashes will be updated if rotation policy = per_seconds (default is 10s)
# requests: 10 # how many lookup requests will be performed before updating hashes if rotation policy = per_requests (default is 10)
### hash lookup for synapseSql provider.
# synapseSql:
# lookup:
# query: 'select user_id as mxid, medium, address from user_threepids' # query for retrive 3PIDs for hashes.
### hash lookup for ldap provider (with example of the ldap configuration)
# ldap:
# enabled: true
# lookup: true # hash lookup
# connection:
# host: 'ldap.domain.tld'
# port: 389
# bindDn: 'cn=admin,dc=domain,dc=tld'
# bindPassword: 'Secret'
# baseDNs:
# - 'dc=domain,dc=tld'
# attribute:
# uid:
# type: 'uid' # or mxid
# value: 'cn'
# name: 'displayName'
# identity:
# filter: '(objectClass=inetOrgPerson)'
#### MSC2140 (Terms)
#policy:
# policies:
# term_name: # term name
# version: 1.0 # version
# terms:
# en: # lang
# name: term name en # localized name
# url: https://ma1sd.host.tld/term_en.html # localized url
# fe: # lang
# name: term name fr # localized name
# url: https://ma1sd.host.tld/term_fr.html # localized url
# regexp:
# - '/_matrix/identity/v2/account.*'
# - '/_matrix/identity/v2/hash_details'
# - '/_matrix/identity/v2/lookup'
#

View File

@@ -104,30 +104,30 @@ public class HttpMxisd {
HttpHandler asNotFoundHandler = SaneHandler.around(new AsNotFoundHandler(m.getAs()));
final RoutingHandler handler = Handlers.routing()
.add("OPTIONS", "/**", SaneHandler.around(new OptionsHandler()))
.add("OPTIONS", "/**", sane(new OptionsHandler()))
// Status endpoints
.get(StatusHandler.Path, SaneHandler.around(new StatusHandler()))
.get(VersionHandler.Path, SaneHandler.around(new VersionHandler()))
.get(StatusHandler.Path, sane(new StatusHandler()))
.get(VersionHandler.Path, sane(new VersionHandler()))
// Authentication endpoints
.get(LoginHandler.Path, SaneHandler.around(new LoginGetHandler(m.getAuth(), m.getHttpClient())))
.post(LoginHandler.Path, SaneHandler.around(new LoginPostHandler(m.getAuth())))
.post(RestAuthHandler.Path, SaneHandler.around(new RestAuthHandler(m.getAuth())))
.get(LoginHandler.Path, sane(new LoginGetHandler(m.getAuth(), m.getHttpClient())))
.post(LoginHandler.Path, sane(new LoginPostHandler(m.getAuth())))
.post(RestAuthHandler.Path, sane(new RestAuthHandler(m.getAuth())))
// Directory endpoints
.post(UserDirectorySearchHandler.Path, SaneHandler.around(new UserDirectorySearchHandler(m.getDirectory())))
.post(UserDirectorySearchHandler.Path, sane(new UserDirectorySearchHandler(m.getDirectory())))
// Profile endpoints
.get(ProfileHandler.Path, SaneHandler.around(new ProfileHandler(m.getProfile())))
.get(InternalProfileHandler.Path, SaneHandler.around(new InternalProfileHandler(m.getProfile())))
.get(ProfileHandler.Path, sane(new ProfileHandler(m.getProfile())))
.get(InternalProfileHandler.Path, sane(new InternalProfileHandler(m.getProfile())))
// Registration endpoints
.post(Register3pidRequestTokenHandler.Path,
SaneHandler.around(new Register3pidRequestTokenHandler(m.getReg(), m.getClientDns(), m.getHttpClient())))
sane(new Register3pidRequestTokenHandler(m.getReg(), m.getClientDns(), m.getHttpClient())))
// Invite endpoints
.post(RoomInviteHandler.Path, SaneHandler.around(new RoomInviteHandler(m.getHttpClient(), m.getClientDns(), m.getInvite())))
.post(RoomInviteHandler.Path, sane(new RoomInviteHandler(m.getHttpClient(), m.getClientDns(), m.getInvite())))
// Application Service endpoints
.get(AsUserHandler.Path, asUserHandler)
@@ -139,7 +139,7 @@ public class HttpMxisd {
.put("/transactions/{" + AsTransactionHandler.ID + "}", asTxnHandler) // Legacy endpoint
// Banned endpoints
.get(InternalInfoHandler.Path, SaneHandler.around(new InternalInfoHandler()));
.get(InternalInfoHandler.Path, sane(new InternalInfoHandler()));
keyEndpoints(handler);
identityEndpoints(handler);
termsEndpoints(handler);
@@ -189,24 +189,32 @@ public class HttpMxisd {
}
private void accountEndpoints(RoutingHandler routingHandler) {
routingHandler.post(AccountRegisterHandler.Path, SaneHandler.around(new AccountRegisterHandler(m.getAccMgr())));
wrapWithTokenAndAuthorizationHandlers(routingHandler, Methods.GET, sane(new AccountGetUserInfoHandler(m.getAccMgr())),
AccountGetUserInfoHandler.Path, true);
wrapWithTokenAndAuthorizationHandlers(routingHandler, Methods.GET, sane(new AccountLogoutHandler(m.getAccMgr())),
AccountLogoutHandler.Path, true);
MatrixConfig matrixConfig = m.getConfig().getMatrix();
if (matrixConfig.isV2()) {
routingHandler.post(AccountRegisterHandler.Path, sane(new AccountRegisterHandler(m.getAccMgr())));
wrapWithTokenAndAuthorizationHandlers(routingHandler, Methods.GET, new AccountGetUserInfoHandler(m.getAccMgr()),
AccountGetUserInfoHandler.Path, true);
wrapWithTokenAndAuthorizationHandlers(routingHandler, Methods.GET, new AccountLogoutHandler(m.getAccMgr()),
AccountLogoutHandler.Path, true);
}
}
private void termsEndpoints(RoutingHandler routingHandler) {
routingHandler.get(GetTermsHandler.PATH, new GetTermsHandler(m.getConfig().getPolicy()));
wrapWithTokenAndAuthorizationHandlers(routingHandler, Methods.POST, sane(new AcceptTermsHandler(m.getAccMgr())),
AcceptTermsHandler.PATH, true);
MatrixConfig matrixConfig = m.getConfig().getMatrix();
if (matrixConfig.isV2()) {
routingHandler.get(GetTermsHandler.PATH, sane(new GetTermsHandler(m.getConfig().getPolicy())));
routingHandler.post(AcceptTermsHandler.PATH, sane(new AcceptTermsHandler(m.getAccMgr())));
}
}
private void hashEndpoints(RoutingHandler routingHandler) {
wrapWithTokenAndAuthorizationHandlers(routingHandler, Methods.GET, sane(new HashDetailsHandler(m.getHashManager())),
HashDetailsHandler.PATH, true);
wrapWithTokenAndAuthorizationHandlers(routingHandler, Methods.POST,
sane(new HashLookupHandler(m.getIdentity(), m.getHashManager())), HashLookupHandler.Path, true);
MatrixConfig matrixConfig = m.getConfig().getMatrix();
if (matrixConfig.isV2()) {
wrapWithTokenAndAuthorizationHandlers(routingHandler, Methods.GET, new HashDetailsHandler(m.getHashManager()),
HashDetailsHandler.PATH, true);
wrapWithTokenAndAuthorizationHandlers(routingHandler, Methods.POST,
new HashLookupHandler(m.getIdentity(), m.getHashManager()), HashLookupHandler.Path, true);
}
}
private void addEndpoints(RoutingHandler routingHandler, HttpString method, boolean useAuthorization, ApiHandler... handlers) {
@@ -219,11 +227,11 @@ public class HttpMxisd {
HttpHandler httpHandler) {
MatrixConfig matrixConfig = m.getConfig().getMatrix();
if (matrixConfig.isV1()) {
routingHandler.add(method, apiHandler.getPath(IdentityServiceAPI.V1), httpHandler);
routingHandler.add(method, apiHandler.getPath(IdentityServiceAPI.V1), sane(httpHandler));
}
if (matrixConfig.isV2()) {
String path = apiHandler.getPath(IdentityServiceAPI.V2);
wrapWithTokenAndAuthorizationHandlers(routingHandler, method, httpHandler, path, useAuthorization);
wrapWithTokenAndAuthorizationHandlers(routingHandler, method, httpHandler, apiHandler.getPath(IdentityServiceAPI.V2),
useAuthorization);
}
}
@@ -237,7 +245,7 @@ public class HttpMxisd {
} else {
wrappedHandler = httpHandler;
}
routingHandler.add(method, url, wrappedHandler);
routingHandler.add(method, url, sane(wrappedHandler));
}
@NotNull

View File

@@ -10,6 +10,7 @@ import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.exception.InvalidCredentialsException;
import io.kamax.mxisd.exception.NotFoundException;
import io.kamax.mxisd.matrix.HomeserverFederationResolver;
import io.kamax.mxisd.matrix.HomeserverVerifier;
import io.kamax.mxisd.storage.IStorage;
import io.kamax.mxisd.storage.ormlite.dao.AccountDao;
import org.apache.http.HttpStatus;
@@ -22,18 +23,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.security.cert.Certificate;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
public class AccountManager {
@@ -80,7 +73,7 @@ public class AccountManager {
homeserverURL + "/_matrix/federation/v1/openid/userinfo?access_token=" + openIdToken.getAccessToken());
String userId;
try (CloseableHttpClient httpClient = HttpClients.custom()
.setSSLHostnameVerifier(new MatrixHostnameVerifier(homeserverTarget.getDomain())).build()) {
.setSSLHostnameVerifier(new HomeserverVerifier(homeserverTarget.getDomain())).build()) {
try (CloseableHttpResponse response = httpClient.execute(getUserInfo)) {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == HttpStatus.SC_OK) {
@@ -170,74 +163,4 @@ public class AccountManager {
public MatrixConfig getMatrixConfig() {
return matrixConfig;
}
public static class MatrixHostnameVerifier implements HostnameVerifier {
private static final String ALT_DNS_NAME_TYPE = "2";
private static final String ALT_IP_ADDRESS_TYPE = "7";
private final String matrixHostname;
public MatrixHostnameVerifier(String matrixHostname) {
this.matrixHostname = matrixHostname;
}
@Override
public boolean verify(String hostname, SSLSession session) {
try {
Certificate peerCertificate = session.getPeerCertificates()[0];
if (peerCertificate instanceof X509Certificate) {
X509Certificate x509Certificate = (X509Certificate) peerCertificate;
if (x509Certificate.getSubjectAlternativeNames() == null) {
return false;
}
for (String altSubjectName : getAltSubjectNames(x509Certificate)) {
if (match(altSubjectName)) {
return true;
}
}
}
} catch (SSLPeerUnverifiedException | CertificateParsingException e) {
LOGGER.error("Unable to check remote host", e);
return false;
}
return false;
}
private List<String> getAltSubjectNames(X509Certificate x509Certificate) {
List<String> subjectNames = new ArrayList<>();
try {
for (List<?> subjectAlternativeNames : x509Certificate.getSubjectAlternativeNames()) {
if (subjectAlternativeNames == null
|| subjectAlternativeNames.size() < 2
|| subjectAlternativeNames.get(0) == null
|| subjectAlternativeNames.get(1) == null) {
continue;
}
String subjectType = subjectAlternativeNames.get(0).toString();
switch (subjectType) {
case ALT_DNS_NAME_TYPE:
case ALT_IP_ADDRESS_TYPE:
subjectNames.add(subjectAlternativeNames.get(1).toString());
break;
default:
LOGGER.trace("Unusable subject type: " + subjectType);
}
}
} catch (CertificateParsingException e) {
LOGGER.error("Unable to parse the certificate", e);
return Collections.emptyList();
}
return subjectNames;
}
private boolean match(String altSubjectName) {
if (altSubjectName.startsWith("*.")) {
return altSubjectName.toLowerCase().endsWith(matrixHostname.toLowerCase());
} else {
return matrixHostname.equalsIgnoreCase(altSubjectName);
}
}
}
}

View File

@@ -173,6 +173,10 @@ public class ExecIdentityStore extends ExecStore implements IThreePidProvider {
@Override
public Iterable<ThreePidMapping> populateHashes() {
if (!cfg.isHashLookup()) {
return Collections.emptyList();
}
Processor<List<ThreePidMapping>> p = new Processor<>();
p.withConfig(cfg.getLookup().getBulk());

View File

@@ -41,7 +41,10 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
public class LdapThreePidProvider extends LdapBackend implements IThreePidProvider {
@@ -137,4 +140,65 @@ public class LdapThreePidProvider extends LdapBackend implements IThreePidProvid
return mappingsFound;
}
private List<String> getAttributes() {
final List<String> attributes = getCfg().getAttribute().getThreepid().values().stream().flatMap(List::stream)
.collect(Collectors.toList());
attributes.add(getUidAtt());
return attributes;
}
private Optional<String> getAttributeValue(Entry entry, List<String> attributes) {
return attributes.stream()
.map(attr -> getAttribute(entry, attr))
.filter(Objects::nonNull)
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst();
}
@Override
public Iterable<ThreePidMapping> populateHashes() {
List<ThreePidMapping> result = new ArrayList<>();
if (!getCfg().getIdentity().isLookup()) {
return result;
}
String filter = getCfg().getIdentity().getFilter();
try (LdapConnection conn = getConn()) {
bind(conn);
log.debug("Query: {}", filter);
List<String> attributes = getAttributes();
log.debug("Attributes: {}", GsonUtil.build().toJson(attributes));
for (String baseDN : getBaseDNs()) {
log.debug("Base DN: {}", baseDN);
try (EntryCursor cursor = conn.search(baseDN, filter, SearchScope.SUBTREE, attributes.toArray(new String[0]))) {
while (cursor.next()) {
Entry entry = cursor.get();
log.info("Found possible match, DN: {}", entry.getDn().getName());
Optional<String> mxid = getAttribute(entry, getUidAtt());
if (!mxid.isPresent()) {
continue;
}
for (Map.Entry<String, List<String>> attributeEntry : getCfg().getAttribute().getThreepid().entrySet()) {
String medium = attributeEntry.getKey();
getAttributeValue(entry, attributeEntry.getValue())
.ifPresent(s -> result.add(new ThreePidMapping(medium, s, buildMatrixIdFromUid(mxid.get()))));
}
}
} catch (CursorLdapReferralException e) {
log.warn("3PID is only available via referral, skipping", e);
} catch (IOException | LdapException | CursorException e) {
log.error("Unable to fetch 3PID mappings", e);
}
}
} catch (LdapException | IOException e) {
log.error("Unable to fetch 3PID mappings", e);
}
return result;
}
}

View File

@@ -174,6 +174,10 @@ public class MemoryIdentityStore implements AuthenticatorProvider, DirectoryProv
@Override
public Iterable<ThreePidMapping> populateHashes() {
if (!cfg.isHashEnabled()) {
return Collections.emptyList();
}
return cfg.getIdentities().stream()
.map(mic -> mic.getThreepids().stream().map(mtp -> new ThreePidMapping(mtp.getMedium(), mtp.getAddress(), mic.getUsername())))
.flatMap(s -> s).collect(

View File

@@ -107,14 +107,16 @@ public abstract class SqlThreePidProvider implements IThreePidProvider {
@Override
public Iterable<ThreePidMapping> populateHashes() {
if (StringUtils.isBlank(cfg.getLookup().getQuery())) {
String query = cfg.getLookup().getQuery();
if (StringUtils.isBlank(query)) {
log.warn("Lookup query not configured, skip.");
return Collections.emptyList();
}
log.debug("Uses query to match users: {}", query);
List<ThreePidMapping> result = new ArrayList<>();
try (Connection connection = pool.get()) {
PreparedStatement statement = connection.prepareStatement(cfg.getLookup().getQuery());
PreparedStatement statement = connection.prepareStatement(query);
try (ResultSet resultSet = statement.executeQuery()) {
while (resultSet.next()) {
String mxid = resultSet.getString("mxid");

View File

@@ -0,0 +1,30 @@
package io.kamax.mxisd.config;
public class DurationDeserializer {
public long deserialize(String argument) {
long duration = 0L;
for (String part : argument.split(" ")) {
String unit = part.substring(part.length() - 1);
long value = Long.parseLong(part.substring(0, part.length() - 1));
switch (unit) {
case "s":
duration += value;
break;
case "m":
duration += value * 60;
break;
case "h":
duration += value * 60 * 60;
break;
case "d":
duration += value * 60 * 60 * 24;
break;
default:
throw new IllegalArgumentException(String.format("Unknown duration unit: %s", unit));
}
}
return duration;
}
}

View File

@@ -309,6 +309,7 @@ public class ExecConfig {
private Boolean enabled;
private int priority;
private Lookup lookup = new Lookup();
private boolean hashLookup = false;
public Boolean isEnabled() {
return enabled;
@@ -334,6 +335,13 @@ public class ExecConfig {
this.lookup = lookup;
}
public boolean isHashLookup() {
return hashLookup;
}
public void setHashLookup(boolean hashLookup) {
this.hashLookup = hashLookup;
}
}
public static class Profile {

View File

@@ -14,7 +14,9 @@ public class HashingConfig {
private int pepperLength = 20;
private RotationPolicyEnum rotationPolicy;
private HashStorageEnum hashStorageType;
private long delay = 10;
private String delay = "10s";
private transient long delayInSeconds = 10;
private int requests = 10;
private List<Algorithm> algorithms = new ArrayList<>();
public void build() {
@@ -23,10 +25,15 @@ public class HashingConfig {
LOGGER.info(" Pepper length: {}", getPepperLength());
LOGGER.info(" Rotation policy: {}", getRotationPolicy());
LOGGER.info(" Hash storage type: {}", getHashStorageType());
if (RotationPolicyEnum.per_seconds == rotationPolicy) {
LOGGER.info(" Rotation delay: {}", delay);
if (RotationPolicyEnum.per_seconds == getRotationPolicy()) {
setDelayInSeconds(new DurationDeserializer().deserialize(getDelay()));
LOGGER.info(" Rotation delay: {}", getDelay());
LOGGER.info(" Rotation delay in seconds: {}", getDelayInSeconds());
}
LOGGER.info(" Algorithms: {}", algorithms);
if (RotationPolicyEnum.per_requests == getRotationPolicy()) {
LOGGER.info(" Rotation after requests: {}", getRequests());
}
LOGGER.info(" Algorithms: {}", getAlgorithms());
} else {
LOGGER.info("Hash configuration disabled, used only `none` pepper.");
}
@@ -79,14 +86,30 @@ public class HashingConfig {
this.hashStorageType = hashStorageType;
}
public long getDelay() {
public String getDelay() {
return delay;
}
public void setDelay(long delay) {
public void setDelay(String delay) {
this.delay = delay;
}
public long getDelayInSeconds() {
return delayInSeconds;
}
public void setDelayInSeconds(long delayInSeconds) {
this.delayInSeconds = delayInSeconds;
}
public int getRequests() {
return requests;
}
public void setRequests(int requests) {
this.requests = requests;
}
public List<Algorithm> getAlgorithms() {
return algorithms;
}

View File

@@ -64,7 +64,7 @@ public class MatrixConfig {
private String domain;
private Identity identity = new Identity();
private boolean v1 = true;
private boolean v2 = true;
private boolean v2 = false;
public String getDomain() {
return domain;

View File

@@ -233,6 +233,7 @@ public abstract class LdapConfig {
private String filter;
private String token = "%3pid";
private Map<String, String> medium = new HashMap<>();
private boolean lookup = false;
public String getFilter() {
return filter;
@@ -262,6 +263,13 @@ public abstract class LdapConfig {
this.medium = medium;
}
public boolean isLookup() {
return lookup;
}
public void setLookup(boolean lookup) {
this.lookup = lookup;
}
}
public static class Profile {

View File

@@ -27,6 +27,7 @@ public class MemoryStoreConfig {
private boolean enabled;
private List<MemoryIdentityConfig> identities = new ArrayList<>();
private boolean hashEnabled = false;
public boolean isEnabled() {
return enabled;
@@ -44,6 +45,14 @@ public class MemoryStoreConfig {
this.identities = identities;
}
public boolean isHashEnabled() {
return hashEnabled;
}
public void setHashEnabled(boolean hashEnabled) {
this.hashEnabled = hashEnabled;
}
public void build() {
// no-op
}

View File

@@ -9,6 +9,7 @@ import org.apache.commons.lang3.RandomStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Base64;
import java.util.List;
public class HashEngine {
@@ -18,6 +19,7 @@ public class HashEngine {
private final List<? extends IThreePidProvider> providers;
private final HashStorage hashStorage;
private final HashingConfig config;
private final Base64.Encoder base64 = Base64.getUrlEncoder().withoutPadding();
private String pepper;
public HashEngine(List<? extends IThreePidProvider> providers, HashStorage hashStorage, HashingConfig config) {
@@ -33,7 +35,9 @@ public class HashEngine {
hashStorage.clear();
for (IThreePidProvider provider : providers) {
try {
LOGGER.info("Populate hashes from the handler: {}", provider.getClass().getCanonicalName());
for (ThreePidMapping pidMapping : provider.populateHashes()) {
LOGGER.debug("Found 3PID: {}", pidMapping);
hashStorage.add(pidMapping, hash(pidMapping));
}
} catch (Exception e) {
@@ -51,7 +55,7 @@ public class HashEngine {
}
protected String hash(ThreePidMapping pidMapping) {
return DigestUtils.sha256Hex(pidMapping.getValue() + " " + pidMapping.getMedium() + " " + getPepper());
return base64.encodeToString(DigestUtils.sha256(pidMapping.getValue() + " " + pidMapping.getMedium() + " " + getPepper()));
}
protected String newPepper() {

View File

@@ -58,10 +58,10 @@ public class HashManager {
if (config.isEnabled()) {
switch (config.getRotationPolicy()) {
case per_requests:
this.rotationStrategy = new RotationPerRequests();
this.rotationStrategy = new RotationPerRequests(config.getRequests());
break;
case per_seconds:
this.rotationStrategy = new TimeBasedRotation(config.getDelay());
this.rotationStrategy = new TimeBasedRotation(config.getDelayInSeconds());
break;
default:
throw new IllegalArgumentException("Unknown rotation type: " + config.getHashStorageType());

View File

@@ -8,6 +8,11 @@ public class RotationPerRequests implements HashRotationStrategy {
private HashEngine hashEngine;
private final AtomicInteger counter = new AtomicInteger(0);
private final int barrier;
public RotationPerRequests(int barrier) {
this.barrier = barrier;
}
@Override
public void register(HashEngine hashEngine) {
@@ -23,7 +28,7 @@ public class RotationPerRequests implements HashRotationStrategy {
@Override
public synchronized void newRequest() {
int newValue = counter.incrementAndGet();
if (newValue >= 10) {
if (newValue >= barrier) {
counter.set(0);
trigger();
}

View File

@@ -12,6 +12,7 @@ public class SqlHashStorage implements HashStorage {
public SqlHashStorage(IStorage storage) {
this.storage = storage;
Runtime.getRuntime().addShutdownHook(new Thread(storage::clearHashes));
}
@Override

View File

@@ -48,7 +48,7 @@ public class AccountRegisterHandler extends BasicHttpHandler {
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Registration from domain: {}, expired at {}", openIdToken.getMatrixServerName(),
new Date(openIdToken.getExpiresIn()));
new Date(System.currentTimeMillis() + openIdToken.getExpiresIn()));
}
String token = accountManager.register(openIdToken);

View File

@@ -31,6 +31,8 @@ import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.kamax.mxisd.http.undertow.handler.identity.share.LookupHandler;
import io.kamax.mxisd.lookup.BulkLookupRequest;
import io.kamax.mxisd.lookup.HashLookupRequest;
import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.lookup.SingleLookupRequest;
import io.kamax.mxisd.lookup.ThreePidMapping;
import io.kamax.mxisd.lookup.strategy.LookupStrategy;
import io.undertow.server.HttpServerExchange;
@@ -40,6 +42,7 @@ import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class HashLookupHandler extends LookupHandler implements ApiHandler {
@@ -82,6 +85,7 @@ public class HashLookupHandler extends LookupHandler implements ApiHandler {
default:
throw new InvalidParamException();
}
hashManager.getRotationStrategy().newRequest();
}
private void noneAlgorithm(HttpServerExchange exchange, HashLookupRequest request, ClientHashLookupRequest input) throws Exception {
@@ -89,6 +93,18 @@ public class HashLookupHandler extends LookupHandler implements ApiHandler {
throw new InvalidParamException();
}
ClientHashLookupAnswer answer = null;
if (input.getAddresses() != null && input.getAddresses().size() > 0) {
if (input.getAddresses().size() == 1) {
answer = noneSingleLookup(request, input);
} else {
answer = noneBulkLookup(request, input);
}
}
respondJson(exchange, answer != null ? answer : new ClientHashLookupAnswer());
}
private ClientHashLookupAnswer noneBulkLookup(HashLookupRequest request, ClientHashLookupRequest input) throws Exception {
BulkLookupRequest bulkLookupRequest = new BulkLookupRequest();
List<ThreePidMapping> mappings = new ArrayList<>();
for (String address : input.getAddresses()) {
@@ -107,7 +123,26 @@ public class HashLookupHandler extends LookupHandler implements ApiHandler {
}
log.info("Finished bulk lookup request from {}", request.getRequester());
respondJson(exchange, answer);
return answer;
}
private ClientHashLookupAnswer noneSingleLookup(HashLookupRequest request, ClientHashLookupRequest input) {
SingleLookupRequest singleLookupRequest = new SingleLookupRequest();
String address = input.getAddresses().get(0);
String[] parts = address.split(" ");
singleLookupRequest.setThreePid(parts[0]);
singleLookupRequest.setType(parts[1]);
ClientHashLookupAnswer answer = new ClientHashLookupAnswer();
Optional<SingleLookupReply> singleLookupReply = strategy.find(singleLookupRequest);
if (singleLookupReply.isPresent()) {
SingleLookupReply reply = singleLookupReply.get();
answer.getMappings().put(address, reply.getMxid().toString());
}
log.info("Finished single lookup request from {}", request.getRequester());
return answer;
}
private void sha256Algorithm(HttpServerExchange exchange, HashLookupRequest request, ClientHashLookupRequest input) {

View File

@@ -46,6 +46,4 @@ public interface LookupStrategy {
Optional<SingleLookupReply> findRecursive(SingleLookupRequest request);
CompletableFuture<List<ThreePidMapping>> find(BulkLookupRequest requests);
CompletableFuture<List<ThreePidMapping>> find(HashLookupRequest request);
}

View File

@@ -26,17 +26,23 @@ import io.kamax.matrix.json.MatrixJson;
import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.exception.ConfigurationException;
import io.kamax.mxisd.hash.HashManager;
import io.kamax.mxisd.hash.storage.HashStorage;
import io.kamax.mxisd.lookup.*;
import io.kamax.mxisd.lookup.ALookupRequest;
import io.kamax.mxisd.lookup.BulkLookupRequest;
import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.lookup.SingleLookupRequest;
import io.kamax.mxisd.lookup.ThreePidMapping;
import io.kamax.mxisd.lookup.fetcher.IBridgeFetcher;
import io.kamax.mxisd.lookup.provider.IThreePidProvider;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.UnknownHostException;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@@ -238,13 +244,4 @@ public class RecursivePriorityLookupStrategy implements LookupStrategy {
result.complete(mapFoundAll);
return bulkLookupInProgress.remove(payloadId);
}
@Override
public CompletableFuture<List<ThreePidMapping>> find(HashLookupRequest request) {
HashStorage hashStorage = hashManager.getHashStorage();
CompletableFuture<List<ThreePidMapping>> result = new CompletableFuture<>();
result.complete(hashStorage.find(request.getHashes()).stream().map(Pair::getValue).collect(Collectors.toList()));
hashManager.getRotationStrategy().newRequest();
return result;
}
}

View File

@@ -77,7 +77,8 @@ public class HomeserverVerifier implements HostnameVerifier {
private boolean match(String altSubjectName) {
if (altSubjectName.startsWith("*.")) {
return altSubjectName.toLowerCase().endsWith(matrixHostname.toLowerCase());
String subjectNameWithoutMask = altSubjectName.substring(1); // remove wildcard
return matrixHostname.toLowerCase().endsWith(subjectNameWithoutMask.toLowerCase());
} else {
return matrixHostname.equalsIgnoreCase(altSubjectName);
}

View File

@@ -39,6 +39,7 @@ import io.kamax.mxisd.storage.IStorage;
import io.kamax.mxisd.storage.dao.IThreePidSessionDao;
import io.kamax.mxisd.storage.ormlite.dao.ASTransactionDao;
import io.kamax.mxisd.storage.ormlite.dao.AccountDao;
import io.kamax.mxisd.storage.ormlite.dao.ChangelogDao;
import io.kamax.mxisd.storage.ormlite.dao.HashDao;
import io.kamax.mxisd.storage.ormlite.dao.HistoricalThreePidInviteIO;
import io.kamax.mxisd.storage.ormlite.dao.AcceptedDao;
@@ -46,12 +47,15 @@ import io.kamax.mxisd.storage.ormlite.dao.ThreePidInviteIO;
import io.kamax.mxisd.storage.ormlite.dao.ThreePidSessionDao;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.sql.SQLException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@@ -59,6 +63,8 @@ import java.util.stream.Collectors;
public class OrmLiteSqlStorage implements IStorage {
private static final Logger LOGGER = LoggerFactory.getLogger(OrmLiteSqlStorage.class);
@FunctionalInterface
private interface Getter<T> {
@@ -73,13 +79,18 @@ public class OrmLiteSqlStorage implements IStorage {
}
public static class Migrations {
public static final String FIX_ACCEPTED_DAO = "2019_12_09__2254__fix_accepted_dao";
}
private Dao<ThreePidInviteIO, String> invDao;
private Dao<HistoricalThreePidInviteIO, String> expInvDao;
private Dao<ThreePidSessionDao, String> sessionDao;
private Dao<ASTransactionDao, String> asTxnDao;
private Dao<AccountDao, String> accountDao;
private Dao<AcceptedDao, String> acceptedDao;
private Dao<AcceptedDao, Long> acceptedDao;
private Dao<HashDao, String> hashDao;
private Dao<ChangelogDao, String> changelogDao;
public OrmLiteSqlStorage(MxisdConfig cfg) {
this(cfg.getStorage().getBackend(), cfg.getStorage().getProvider().getSqlite().getDatabase());
@@ -96,6 +107,7 @@ public class OrmLiteSqlStorage implements IStorage {
withCatcher(() -> {
ConnectionSource connPool = new JdbcConnectionSource("jdbc:" + backend + ":" + path);
changelogDao = createDaoAndTable(connPool, ChangelogDao.class);
invDao = createDaoAndTable(connPool, ThreePidInviteIO.class);
expInvDao = createDaoAndTable(connPool, HistoricalThreePidInviteIO.class);
sessionDao = createDaoAndTable(connPool, ThreePidSessionDao.class);
@@ -103,10 +115,26 @@ public class OrmLiteSqlStorage implements IStorage {
accountDao = createDaoAndTable(connPool, AccountDao.class);
acceptedDao = createDaoAndTable(connPool, AcceptedDao.class);
hashDao = createDaoAndTable(connPool, HashDao.class);
runMigration(connPool);
});
}
private void runMigration(ConnectionSource connPol) throws SQLException {
ChangelogDao fixAcceptedDao = changelogDao.queryForId(Migrations.FIX_ACCEPTED_DAO);
if (fixAcceptedDao == null) {
fixAcceptedDao(connPol);
changelogDao.create(new ChangelogDao(Migrations.FIX_ACCEPTED_DAO, new Date(), "Recreate the accepted table."));
}
}
private void fixAcceptedDao(ConnectionSource connPool) throws SQLException {
LOGGER.info("Migration: {}", Migrations.FIX_ACCEPTED_DAO);
TableUtils.dropTable(acceptedDao, true);
TableUtils.createTableIfNotExists(connPool, AcceptedDao.class);
}
private <V, K> Dao<V, K> createDaoAndTable(ConnectionSource connPool, Class<V> c) throws SQLException {
LOGGER.info("Create the dao: {}", c.getSimpleName());
Dao<V, K> dao = DaoManager.createDao(connPool, c);
TableUtils.createTableIfNotExists(connPool, c);
return dao;

View File

@@ -26,7 +26,10 @@ import com.j256.ormlite.table.DatabaseTable;
@DatabaseTable(tableName = "accepted")
public class AcceptedDao {
@DatabaseField(canBeNull = false, id = true)
@DatabaseField(generatedId = true)
private Long id;
@DatabaseField(canBeNull = false)
private String url;
@DatabaseField(canBeNull = false)
@@ -45,6 +48,14 @@ public class AcceptedDao {
this.acceptedAt = acceptedAt;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUrl() {
return url;
}

View File

@@ -0,0 +1,52 @@
package io.kamax.mxisd.storage.ormlite.dao;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;
import java.util.Date;
@DatabaseTable(tableName = "changelog")
public class ChangelogDao {
@DatabaseField(id = true)
private String id;
@DatabaseField
private Date createdAt;
@DatabaseField
private String comment;
public ChangelogDao() {
}
public ChangelogDao(String id, Date createdAt, String comment) {
this.id = id;
this.createdAt = createdAt;
this.comment = comment;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Date getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Date createdAt) {
this.createdAt = createdAt;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
}

View File

@@ -0,0 +1,20 @@
package io.kamax.mxisd.test.config;
import static org.junit.Assert.assertEquals;
import io.kamax.mxisd.config.DurationDeserializer;
import org.junit.Test;
public class DurationDeserializerTest {
@Test
public void durationLoadTest() {
DurationDeserializer deserializer = new DurationDeserializer();
assertEquals(4, deserializer.deserialize("4s"));
assertEquals((60 * 60) + 4, deserializer.deserialize("1h 4s"));
assertEquals((2 * 60) + 4, deserializer.deserialize("2m 4s"));
assertEquals((2 * 60 * 60) + (7 * 60) + 4, deserializer.deserialize("2h 7m 4s"));
assertEquals((60 * 60 * 24) + (2 * 60 * 60) + (7 * 60) + 4, deserializer.deserialize("1d 2h 7m 4s"));
}
}

View File

@@ -5,11 +5,14 @@ import static org.junit.Assert.assertEquals;
import org.apache.commons.codec.digest.DigestUtils;
import org.junit.Test;
import java.util.Base64;
public class HashEngineTest {
@Test
public void sha256test() {
assertEquals("a26de61ae3055f84b33ac1a179b9ad5301f9109024f4db1ae653ea525d2136f4",
DigestUtils.sha256Hex("user2@mail.homeserver.tld email I9x4vpcWjqp9X8iiOY4a"));
Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();
assertEquals("rujYzy1w0JxulN_rVlErGUmkdXT5znL0sjSF_IWreko",
encoder.encodeToString(DigestUtils.sha256("user@mail.homeserver.tld email I9x4vpcWjqp9X8iiOY4a")));
}
}