Commit 44826566 authored by Richard Midwinter's avatar Richard Midwinter Committed by Dave Cridland

OF-631: Implement SCRAM support

This implements the SCRAM-SHA1 mechanism, and includes extending the existing
DefaultAuthProvider to store the Scram hashes for faster authentication.

If user.scramHashedOnly is set to true, then only these non-reversable hashes
are stored (and thus security is increased in exchanged for removing support
for DIGEST-MD5 et al).
parent 5e3f4161
...@@ -3,6 +3,10 @@ ...@@ -3,6 +3,10 @@
CREATE TABLE ofUser ( CREATE TABLE ofUser (
username VARCHAR(64) NOT NULL, username VARCHAR(64) NOT NULL,
storedKey VARCHAR(32),
serverKey VARCHAR(32),
salt VARCHAR(32),
iterations INTEGER,
plainPassword VARCHAR(32), plainPassword VARCHAR(32),
encryptedPassword VARCHAR(255), encryptedPassword VARCHAR(255),
name VARCHAR(100), name VARCHAR(100),
......
...@@ -3,6 +3,10 @@ ...@@ -3,6 +3,10 @@
CREATE TABLE ofUser ( CREATE TABLE ofUser (
username VARCHAR(64) NOT NULL, username VARCHAR(64) NOT NULL,
storedKey VARCHAR(32),
serverKey VARCHAR(32),
salt VARCHAR(32),
iterations INTEGER,
plainPassword VARCHAR(32), plainPassword VARCHAR(32),
encryptedPassword VARCHAR(255), encryptedPassword VARCHAR(255),
name VARCHAR(100), name VARCHAR(100),
......
...@@ -3,6 +3,10 @@ ...@@ -3,6 +3,10 @@
CREATE TABLE ofUser ( CREATE TABLE ofUser (
username VARCHAR(64) NOT NULL, username VARCHAR(64) NOT NULL,
storedKey VARCHAR(32),
serverKey VARCHAR(32),
salt VARCHAR(32),
iterations INTEGER,
plainPassword VARCHAR(32), plainPassword VARCHAR(32),
encryptedPassword VARCHAR(255), encryptedPassword VARCHAR(255),
name VARCHAR(100), name VARCHAR(100),
......
...@@ -3,6 +3,10 @@ ...@@ -3,6 +3,10 @@
CREATE TABLE ofUser ( CREATE TABLE ofUser (
username VARCHAR2(64) NOT NULL, username VARCHAR2(64) NOT NULL,
storedKey VARCHAR(32),
serverKey VARCHAR(32),
salt VARCHAR(32),
iterations INTEGER,
plainPassword VARCHAR2(32), plainPassword VARCHAR2(32),
encryptedPassword VARCHAR2(255), encryptedPassword VARCHAR2(255),
name VARCHAR2(100), name VARCHAR2(100),
......
...@@ -5,6 +5,10 @@ ...@@ -5,6 +5,10 @@
CREATE TABLE ofUser ( CREATE TABLE ofUser (
username VARCHAR(64) NOT NULL, username VARCHAR(64) NOT NULL,
storedKey VARCHAR(32),
serverKey VARCHAR(32),
salt VARCHAR(32),
iterations INTEGER,
plainPassword VARCHAR(32), plainPassword VARCHAR(32),
encryptedPassword VARCHAR(255), encryptedPassword VARCHAR(255),
name VARCHAR(100), name VARCHAR(100),
......
...@@ -3,6 +3,10 @@ ...@@ -3,6 +3,10 @@
CREATE TABLE ofUser ( CREATE TABLE ofUser (
username NVARCHAR(64) NOT NULL, username NVARCHAR(64) NOT NULL,
storedKey VARCHAR(32),
serverKey VARCHAR(32),
salt VARCHAR(32),
iterations INTEGER,
plainPassword NVARCHAR(32), plainPassword NVARCHAR(32),
encryptedPassword NVARCHAR(255), encryptedPassword NVARCHAR(255),
name NVARCHAR(100), name NVARCHAR(100),
......
...@@ -3,6 +3,10 @@ ...@@ -3,6 +3,10 @@
CREATE TABLE ofUser ( CREATE TABLE ofUser (
username NVARCHAR(64) NOT NULL, username NVARCHAR(64) NOT NULL,
storedKey VARCHAR(32),
serverKey VARCHAR(32),
salt VARCHAR(32),
iterations INTEGER,
plainPassword NVARCHAR(32) NULL, plainPassword NVARCHAR(32) NULL,
encryptedPassword NVARCHAR(255) NULL, encryptedPassword NVARCHAR(255) NULL,
name NVARCHAR(100) NULL, name NVARCHAR(100) NULL,
......
// add columns for SASL SCRAM-SHA-1
ALTER TABLE ofUser ADD COLUMN storedKey VARCHAR(32);
ALTER TABLE ofUser ADD COLUMN serverKey VARCHAR(32);
ALTER TABLE ofUser ADD COLUMN salt VARCHAR(32);
ALTER TABLE ofUser ADD COLUMN iterations INTEGER;
UPDATE ofVersion SET version = 22 WHERE name = 'openfire';
// add columns for SASL SCRAM-SHA-1
ALTER TABLE ofUser ADD COLUMN storedKey VARCHAR(32);
ALTER TABLE ofUser ADD COLUMN serverKey VARCHAR(32);
ALTER TABLE ofUser ADD COLUMN salt VARCHAR(32);
ALTER TABLE ofUser ADD COLUMN iterations INTEGER;
UPDATE ofVersion SET version = 22 WHERE name = 'openfire';
// add columns for SASL SCRAM-SHA-1
ALTER TABLE ofUser ADD COLUMN storedKey VARCHAR(32);
ALTER TABLE ofUser ADD COLUMN serverKey VARCHAR(32);
ALTER TABLE ofUser ADD COLUMN salt VARCHAR(32);
ALTER TABLE ofUser ADD COLUMN iterations INTEGER;
UPDATE ofVersion SET version = 22 WHERE name = 'openfire';
// add columns for SASL SCRAM-SHA-1
ALTER TABLE ofUser ADD COLUMN storedKey VARCHAR(32);
ALTER TABLE ofUser ADD COLUMN serverKey VARCHAR(32);
ALTER TABLE ofUser ADD COLUMN salt VARCHAR(32);
ALTER TABLE ofUser ADD COLUMN iterations INTEGER;
UPDATE ofVersion SET version = 22 WHERE name = 'openfire';
COMMIT;
// add columns for SASL SCRAM-SHA-1
ALTER TABLE ofUser ADD COLUMN storedKey VARCHAR(32);
ALTER TABLE ofUser ADD COLUMN serverKey VARCHAR(32);
ALTER TABLE ofUser ADD COLUMN salt VARCHAR(32);
ALTER TABLE ofUser ADD COLUMN iterations INTEGER;
UPDATE ofVersion SET version = 22 WHERE name = 'openfire';
// add columns for SASL SCRAM-SHA-1
ALTER TABLE ofUser ADD COLUMN storedKey VARCHAR(32);
ALTER TABLE ofUser ADD COLUMN serverKey VARCHAR(32);
ALTER TABLE ofUser ADD COLUMN salt VARCHAR(32);
ALTER TABLE ofUser ADD COLUMN iterations INTEGER;
UPDATE ofVersion SET version = 22 WHERE name = 'openfire';
// add columns for SASL SCRAM-SHA-1
ALTER TABLE ofUser ADD COLUMN storedKey VARCHAR(32);
ALTER TABLE ofUser ADD COLUMN serverKey VARCHAR(32);
ALTER TABLE ofUser ADD COLUMN salt VARCHAR(32);
ALTER TABLE ofUser ADD COLUMN iterations INTEGER;
UPDATE ofVersion SET version = 22 WHERE name = 'openfire';
...@@ -1959,6 +1959,9 @@ setup.profile.description=Choose the user and group system to use with the serve ...@@ -1959,6 +1959,9 @@ setup.profile.description=Choose the user and group system to use with the serve
setup.profile.default=Default setup.profile.default=Default
setup.profile.default_description=Store users and groups in the server database. This is the \ setup.profile.default_description=Store users and groups in the server database. This is the \
best option for simple deployments. best option for simple deployments.
setup.profile.default.scramOnly=Only Hashed Passwords
setup.profile.default.scramOnly_description=Store only non-reversible hashes of passwords in the database. \
This only supports PLAIN and SCRAM-SHA-1 capable clients.
setup.profile.ldap=Directory Server (LDAP) setup.profile.ldap=Directory Server (LDAP)
setup.profile.ldap_description=Integrate with a directory server such as Active Directory or \ setup.profile.ldap_description=Integrate with a directory server such as Active Directory or \
OpenLDAP using the LDAP protocol. Users and groups are stored in the directory and treated \ OpenLDAP using the LDAP protocol. Users and groups are stored in the directory and treated \
......
...@@ -68,7 +68,7 @@ public class SchemaManager { ...@@ -68,7 +68,7 @@ public class SchemaManager {
/** /**
* Current Openfire database schema version. * Current Openfire database schema version.
*/ */
private static final int DATABASE_VERSION = 21; private static final int DATABASE_VERSION = 22;
/** /**
* Creates a new Schema manager. * Creates a new Schema manager.
......
...@@ -49,6 +49,7 @@ import org.jivesoftware.database.DbConnectionManager; ...@@ -49,6 +49,7 @@ import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.openfire.admin.AdminManager; import org.jivesoftware.openfire.admin.AdminManager;
import org.jivesoftware.openfire.audit.AuditManager; import org.jivesoftware.openfire.audit.AuditManager;
import org.jivesoftware.openfire.audit.spi.AuditManagerImpl; import org.jivesoftware.openfire.audit.spi.AuditManagerImpl;
import org.jivesoftware.openfire.auth.ScramUtils;
import org.jivesoftware.openfire.clearspace.ClearspaceManager; import org.jivesoftware.openfire.clearspace.ClearspaceManager;
import org.jivesoftware.openfire.cluster.ClusterManager; import org.jivesoftware.openfire.cluster.ClusterManager;
import org.jivesoftware.openfire.cluster.NodeID; import org.jivesoftware.openfire.cluster.NodeID;
...@@ -420,6 +421,9 @@ public class XMPPServer { ...@@ -420,6 +421,9 @@ public class XMPPServer {
JiveGlobals.setProperty(propName, JiveGlobals.getXMLProperty(propName)); JiveGlobals.setProperty(propName, JiveGlobals.getXMLProperty(propName));
} }
} }
// Set default SASL SCRAM-SHA-1 iteration count
JiveGlobals.setProperty("sasl.scram-sha-1.iteration-count", Integer.toString(ScramUtils.DEFAULT_ITERATION_COUNT));
// Update certificates (if required) // Update certificates (if required)
try { try {
......
...@@ -328,4 +328,9 @@ public class AuthFactory { ...@@ -328,4 +328,9 @@ public class AuthFactory {
} }
return cipher; return cipher;
} }
public static boolean supportsScram() {
// TODO Auto-generated method stub
return authProvider.isScramSupported();
}
} }
\ No newline at end of file
...@@ -129,4 +129,6 @@ public interface AuthProvider { ...@@ -129,4 +129,6 @@ public interface AuthProvider {
* backend user store. * backend user store.
*/ */
public boolean supportsPasswordRetrieval(); public boolean supportsPasswordRetrieval();
boolean isScramSupported();
} }
\ No newline at end of file
...@@ -20,12 +20,19 @@ ...@@ -20,12 +20,19 @@
package org.jivesoftware.openfire.auth; package org.jivesoftware.openfire.auth;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.sql.Connection; import java.sql.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Types; import java.sql.Types;
import javax.security.sasl.SaslException;
import javax.xml.bind.DatatypeConverter;
import org.jivesoftware.database.DbConnectionManager; import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.openfire.XMPPServer; import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.user.UserNotFoundException; import org.jivesoftware.openfire.user.UserNotFoundException;
...@@ -46,10 +53,14 @@ public class DefaultAuthProvider implements AuthProvider { ...@@ -46,10 +53,14 @@ public class DefaultAuthProvider implements AuthProvider {
private static final Logger Log = LoggerFactory.getLogger(DefaultAuthProvider.class); private static final Logger Log = LoggerFactory.getLogger(DefaultAuthProvider.class);
private static final String LOAD_PASSWORD = private static final String LOAD_PASSWORD =
"SELECT plainPassword,encryptedPassword FROM ofUser WHERE username=?"; "SELECT plainPassword,encryptedPassword FROM ofUser WHERE username=?";
private static final String TEST_PASSWORD =
"SELECT plainPassword,encryptedPassword,iterations,salt,storedKey FROM ofUser WHERE username=?";
private static final String UPDATE_PASSWORD = private static final String UPDATE_PASSWORD =
"UPDATE ofUser SET plainPassword=?, encryptedPassword=? WHERE username=?"; "UPDATE ofUser SET plainPassword=?, encryptedPassword=?, storedKey=?, serverKey=?, salt=?, iterations=? WHERE username=?";
private static final SecureRandom random = new SecureRandom();
/** /**
* Constructs a new DefaultAuthProvider. * Constructs a new DefaultAuthProvider.
...@@ -75,7 +86,7 @@ public class DefaultAuthProvider implements AuthProvider { ...@@ -75,7 +86,7 @@ public class DefaultAuthProvider implements AuthProvider {
} }
} }
try { try {
if (!password.equals(getPassword(username))) { if (!checkPassword(username, password)) {
throw new UnauthorizedException(); throw new UnauthorizedException();
} }
} }
...@@ -119,7 +130,8 @@ public class DefaultAuthProvider implements AuthProvider { ...@@ -119,7 +130,8 @@ public class DefaultAuthProvider implements AuthProvider {
} }
public boolean isDigestSupported() { public boolean isDigestSupported() {
return true; boolean scramOnly = JiveGlobals.getBooleanProperty("user.scramHashedPasswordOnly");
return !scramOnly;
} }
public String getPassword(String username) throws UserNotFoundException { public String getPassword(String username) throws UserNotFoundException {
...@@ -159,6 +171,9 @@ public class DefaultAuthProvider implements AuthProvider { ...@@ -159,6 +171,9 @@ public class DefaultAuthProvider implements AuthProvider {
// Ignore and return plain password instead. // Ignore and return plain password instead.
} }
} }
if (plainText == null) {
throw new UnsupportedOperationException();
}
return plainText; return plainText;
} }
catch (SQLException sqle) { catch (SQLException sqle) {
...@@ -169,9 +184,80 @@ public class DefaultAuthProvider implements AuthProvider { ...@@ -169,9 +184,80 @@ public class DefaultAuthProvider implements AuthProvider {
} }
} }
public boolean checkPassword(String username, String testPassword) throws UserNotFoundException {
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
if (username.contains("@")) {
// Check that the specified domain matches the server's domain
int index = username.indexOf("@");
String domain = username.substring(index + 1);
if (domain.equals(XMPPServer.getInstance().getServerInfo().getXMPPDomain())) {
username = username.substring(0, index);
} else {
// Unknown domain.
throw new UserNotFoundException();
}
}
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(TEST_PASSWORD);
pstmt.setString(1, username);
rs = pstmt.executeQuery();
if (!rs.next()) {
throw new UserNotFoundException(username);
}
String plainText = rs.getString(1);
String encrypted = rs.getString(2);
int iterations = rs.getInt(3);
String salt = rs.getString(4);
String storedKey = rs.getString(5);
if (encrypted != null) {
try {
plainText = AuthFactory.decryptPassword(encrypted);
}
catch (UnsupportedOperationException uoe) {
// Ignore and return plain password instead.
}
}
if (plainText != null) {
boolean scramOnly = JiveGlobals.getBooleanProperty("user.scramHashedPasswordOnly");
if (scramOnly) {
// If we have a password here, but we're meant to be scramOnly, we should reset it.
setPassword(username, plainText);
}
return testPassword.equals(plainText);
}
// Don't have either plain or encrypted, so test SCRAM hash.
if (salt == null || iterations == 0 || storedKey == null) {
Log.warn("No available credentials for checkPassword.");
return false;
}
byte[] saltShaker = DatatypeConverter.parseBase64Binary(salt);
byte[] saltedPassword = null, clientKey = null, testStoredKey = null;
try {
saltedPassword = ScramUtils.createSaltedPassword(saltShaker, testPassword, iterations);
clientKey = ScramUtils.computeHmac(saltedPassword, "Client Key");
testStoredKey = MessageDigest.getInstance("SHA-1").digest(clientKey);
} catch(SaslException | NoSuchAlgorithmException | UnsupportedEncodingException e) {
Log.warn("Unable to check SCRAM values for PLAIN authentication.");
return false;
}
return DatatypeConverter.printBase64Binary(testStoredKey).equals(storedKey);
}
catch (SQLException sqle) {
Log.error("User SQL failure:", sqle);
throw new UserNotFoundException(sqle);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
}
public void setPassword(String username, String password) throws UserNotFoundException { public void setPassword(String username, String password) throws UserNotFoundException {
// Determine if the password should be stored as plain text or encrypted. // Determine if the password should be stored as plain text or encrypted.
boolean usePlainPassword = JiveGlobals.getBooleanProperty("user.usePlainPassword"); boolean usePlainPassword = JiveGlobals.getBooleanProperty("user.usePlainPassword");
boolean scramOnly = JiveGlobals.getBooleanProperty("user.scramHashedPasswordOnly");
String encryptedPassword = null; String encryptedPassword = null;
if (username.contains("@")) { if (username.contains("@")) {
// Check that the specified domain matches the server's domain // Check that the specified domain matches the server's domain
...@@ -184,7 +270,26 @@ public class DefaultAuthProvider implements AuthProvider { ...@@ -184,7 +270,26 @@ public class DefaultAuthProvider implements AuthProvider {
throw new UserNotFoundException(); throw new UserNotFoundException();
} }
} }
if (!usePlainPassword) {
// Store the salt and salted password so SCRAM-SHA-1 SASL auth can be used later.
byte[] saltShaker = new byte[32];
random.nextBytes(saltShaker);
String salt = DatatypeConverter.printBase64Binary(saltShaker);
int iterations = JiveGlobals.getIntProperty("sasl.scram-sha-1.iteration-count",
ScramUtils.DEFAULT_ITERATION_COUNT);
byte[] saltedPassword = null, clientKey = null, storedKey = null, serverKey = null;
try {
saltedPassword = ScramUtils.createSaltedPassword(saltShaker, password, iterations);
clientKey = ScramUtils.computeHmac(saltedPassword, "Client Key");
storedKey = MessageDigest.getInstance("SHA-1").digest(clientKey);
serverKey = ScramUtils.computeHmac(saltedPassword, "Server Key");
} catch (SaslException | NoSuchAlgorithmException | UnsupportedEncodingException e) {
Log.warn("Unable to persist values for SCRAM authentication.");
}
if (!scramOnly && !usePlainPassword) {
try { try {
encryptedPassword = AuthFactory.encryptPassword(password); encryptedPassword = AuthFactory.encryptPassword(password);
// Set password to null so that it's inserted that way. // Set password to null so that it's inserted that way.
...@@ -195,6 +300,10 @@ public class DefaultAuthProvider implements AuthProvider { ...@@ -195,6 +300,10 @@ public class DefaultAuthProvider implements AuthProvider {
// the plain password will be stored. // the plain password will be stored.
} }
} }
if (scramOnly) {
encryptedPassword = null;
password = null;
}
Connection con = null; Connection con = null;
PreparedStatement pstmt = null; PreparedStatement pstmt = null;
...@@ -213,7 +322,21 @@ public class DefaultAuthProvider implements AuthProvider { ...@@ -213,7 +322,21 @@ public class DefaultAuthProvider implements AuthProvider {
else { else {
pstmt.setString(2, encryptedPassword); pstmt.setString(2, encryptedPassword);
} }
pstmt.setString(3, username); if (storedKey == null) {
pstmt.setNull(3, Types.VARCHAR);
}
else {
pstmt.setString(3, DatatypeConverter.printBase64Binary(storedKey));
}
if (serverKey == null) {
pstmt.setNull(4, Types.VARCHAR);
}
else {
pstmt.setString(4, DatatypeConverter.printBase64Binary(serverKey));
}
pstmt.setString(5, salt);
pstmt.setInt(6, iterations);
pstmt.setString(7, username);
pstmt.executeUpdate(); pstmt.executeUpdate();
} }
catch (SQLException sqle) { catch (SQLException sqle) {
...@@ -225,6 +348,12 @@ public class DefaultAuthProvider implements AuthProvider { ...@@ -225,6 +348,12 @@ public class DefaultAuthProvider implements AuthProvider {
} }
public boolean supportsPasswordRetrieval() { public boolean supportsPasswordRetrieval() {
boolean scramOnly = JiveGlobals.getBooleanProperty("user.scramHashedPasswordOnly");
return !scramOnly;
}
@Override
public boolean isScramSupported() {
return true; return true;
} }
} }
\ No newline at end of file
...@@ -257,4 +257,10 @@ public class HybridAuthProvider implements AuthProvider { ...@@ -257,4 +257,10 @@ public class HybridAuthProvider implements AuthProvider {
public boolean supportsPasswordRetrieval() { public boolean supportsPasswordRetrieval() {
return false; return false;
} }
@Override
public boolean isScramSupported() {
// TODO Auto-generated method stub
return false;
}
} }
\ No newline at end of file
...@@ -409,4 +409,10 @@ public class JDBCAuthProvider implements AuthProvider { ...@@ -409,4 +409,10 @@ public class JDBCAuthProvider implements AuthProvider {
} }
} }
} }
@Override
public boolean isScramSupported() {
// TODO Auto-generated method stub
return false;
}
} }
...@@ -204,4 +204,10 @@ public class NativeAuthProvider implements AuthProvider { ...@@ -204,4 +204,10 @@ public class NativeAuthProvider implements AuthProvider {
public boolean supportsPasswordRetrieval() { public boolean supportsPasswordRetrieval() {
return false; return false;
} }
@Override
public boolean isScramSupported() {
// TODO Auto-generated method stub
return false;
}
} }
...@@ -245,4 +245,8 @@ public class POP3AuthProvider implements AuthProvider { ...@@ -245,4 +245,8 @@ public class POP3AuthProvider implements AuthProvider {
public boolean supportsPasswordRetrieval() { public boolean supportsPasswordRetrieval() {
return false; return false;
} }
public boolean isScramSupported() {
return false;
}
} }
\ No newline at end of file
/**
* $RCSfile$
* $Revision: $
* $Date: $
*
* Copyright 2015 Surevine Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.openfire.auth;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.security.sasl.SaslException;
import org.jivesoftware.util.JiveGlobals;
/**
* A utility class that provides methods that are useful for dealing with
* Salted Challenge Response Authentication Mechanism (SCRAM).
*
* @author Richard Midwinter
*/
public class ScramUtils {
public static final int DEFAULT_ITERATION_COUNT = 4096;
private ScramUtils() {}
public static byte[] createSaltedPassword(byte[] salt, String password, int iters) throws SaslException {
Mac mac = createSha1Hmac(password.getBytes(StandardCharsets.US_ASCII));
mac.update(salt);
mac.update(new byte[]{0, 0, 0, 1});
byte[] result = mac.doFinal();
byte[] previous = null;
for (int i = 1; i < iters; i++) {
mac.update(previous != null ? previous : result);
previous = mac.doFinal();
for (int x = 0; x < result.length; x++) {
result[x] ^= previous[x];
}
}
return result;
}
public static byte[] computeHmac(final byte[] key, final String string)
throws SaslException, UnsupportedEncodingException {
Mac mac = createSha1Hmac(key);
mac.update(string.getBytes(StandardCharsets.US_ASCII));
return mac.doFinal();
}
public static Mac createSha1Hmac(final byte[] keyBytes)
throws SaslException {
try {
SecretKeySpec key = new SecretKeySpec(keyBytes, "HmacSHA1");
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(key);
return mac;
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new SaslException(e.getMessage(), e);
}
}
}
...@@ -131,4 +131,9 @@ public class ClearspaceAuthProvider implements AuthProvider { ...@@ -131,4 +131,9 @@ public class ClearspaceAuthProvider implements AuthProvider {
public boolean supportsPasswordRetrieval() { public boolean supportsPasswordRetrieval() {
return false; return false;
} }
@Override
public boolean isScramSupported() {
return false;
}
} }
...@@ -109,4 +109,10 @@ public class CrowdAuthProvider implements AuthProvider { ...@@ -109,4 +109,10 @@ public class CrowdAuthProvider implements AuthProvider {
return false; return false;
} }
@Override
public boolean isScramSupported() {
// TODO Auto-generated method stub
return false;
}
} }
...@@ -160,4 +160,9 @@ public class LdapAuthProvider implements AuthProvider { ...@@ -160,4 +160,9 @@ public class LdapAuthProvider implements AuthProvider {
public boolean supportsPasswordRetrieval() { public boolean supportsPasswordRetrieval() {
return false; return false;
} }
@Override
public boolean isScramSupported() {
return false;
}
} }
\ No newline at end of file
...@@ -32,6 +32,7 @@ import java.util.Set; ...@@ -32,6 +32,7 @@ import java.util.Set;
import java.util.StringTokenizer; import java.util.StringTokenizer;
import java.util.TimeZone; import java.util.TimeZone;
import javax.naming.NamingEnumeration;
import javax.naming.directory.Attribute; import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes; import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext; import javax.naming.directory.DirContext;
...@@ -133,7 +134,39 @@ public class LdapUserProvider implements UserProvider { ...@@ -133,7 +134,39 @@ public class LdapUserProvider implements UserProvider {
} }
// Escape the username so that it can be used as a JID. // Escape the username so that it can be used as a JID.
username = JID.escapeNode(username); username = JID.escapeNode(username);
return new User(username, name, email, creationDate, modificationDate);
// As defined by RFC5803.
Attribute authPassword = attrs.get("authPassword");
User user = new User(username, name, email, creationDate, modificationDate);
if (authPassword != null) {
// The authPassword attribute can be multivalued.
// Not sure if this is the right API to loop through them.
NamingEnumeration values = authPassword.getAll();
while (values.hasMore()) {
Attribute authPasswordValue = (Attribute) values.next();
String[] parts = ((String) authPasswordValue.get()).split("$");
String[] authInfo = parts[1].split(":");
String[] authValue = parts[2].split(":");
String scheme = parts[0].trim();
// We only support SCRAM-SHA-1 at the moment.
if ("SCRAM-SHA-1".equals(scheme)) {
int iterations = Integer.valueOf(authInfo[0].trim());
String salt = authInfo[1].trim();
String storedKey = authValue[0].trim();
String serverKey = authValue[1].trim();
user.setSalt(salt);
user.setStoredKey(storedKey);
user.setServerKey(serverKey);
user.setIterations(iterations);
break;
}
}
}
return user;
} }
catch (Exception e) { catch (Exception e) {
throw new UserNotFoundException(e); throw new UserNotFoundException(e);
......
...@@ -802,6 +802,11 @@ public class SASLAuthentication { ...@@ -802,6 +802,11 @@ public class SASLAuthentication {
it.remove(); it.remove();
} }
} }
else if (mech.equals("SCRAM-SHA-1")) {
if (!AuthFactory.supportsPasswordRetrieval() && !AuthFactory.supportsScram()) {
it.remove();
}
}
else if (mech.equals("ANONYMOUS")) { else if (mech.equals("ANONYMOUS")) {
// Check anonymous is supported // Check anonymous is supported
if (!XMPPServer.getInstance().getIQAuthHandler().isAnonymousAllowed()) { if (!XMPPServer.getInstance().getIQAuthHandler().isAnonymousAllowed()) {
...@@ -832,6 +837,7 @@ public class SASLAuthentication { ...@@ -832,6 +837,7 @@ public class SASLAuthentication {
mechanisms.add("PLAIN"); mechanisms.add("PLAIN");
mechanisms.add("DIGEST-MD5"); mechanisms.add("DIGEST-MD5");
mechanisms.add("CRAM-MD5"); mechanisms.add("CRAM-MD5");
mechanisms.add("SCRAM-SHA-1");
mechanisms.add("JIVE-SHAREDSECRET"); mechanisms.add("JIVE-SHAREDSECRET");
} }
else { else {
...@@ -843,6 +849,7 @@ public class SASLAuthentication { ...@@ -843,6 +849,7 @@ public class SASLAuthentication {
mech.equals("PLAIN") || mech.equals("PLAIN") ||
mech.equals("DIGEST-MD5") || mech.equals("DIGEST-MD5") ||
mech.equals("CRAM-MD5") || mech.equals("CRAM-MD5") ||
mech.equals("SCRAM-SHA-1") ||
mech.equals("GSSAPI") || mech.equals("GSSAPI") ||
mech.equals("EXTERNAL") || mech.equals("EXTERNAL") ||
mech.equals("JIVE-SHAREDSECRET")) mech.equals("JIVE-SHAREDSECRET"))
......
...@@ -34,10 +34,12 @@ public class SaslProvider extends Provider { ...@@ -34,10 +34,12 @@ public class SaslProvider extends Provider {
* Constructs a the JiveSoftware SASL provider. * Constructs a the JiveSoftware SASL provider.
*/ */
public SaslProvider() { public SaslProvider() {
super("JiveSoftware", 1.0, "JiveSoftware SASL provider v1.0, implementing server mechanisms for: PLAIN, CLEARSPACE"); super("JiveSoftware", 1.0, "JiveSoftware SASL provider v1.0, implementing server mechanisms for: PLAIN, CLEARSPACE, SCRAM-SHA-1");
// Add SaslServer supporting the PLAIN SASL mechanism // Add SaslServer supporting the PLAIN SASL mechanism
put("SaslServerFactory.PLAIN", "org.jivesoftware.openfire.sasl.SaslServerFactoryImpl"); put("SaslServerFactory.PLAIN", "org.jivesoftware.openfire.sasl.SaslServerFactoryImpl");
// Add SaslServer supporting the Clearspace SASL mechanism // Add SaslServer supporting the Clearspace SASL mechanism
put("SaslServerFactory.CLEARSPACE", "org.jivesoftware.openfire.sasl.SaslServerFactoryImpl"); put("SaslServerFactory.CLEARSPACE", "org.jivesoftware.openfire.sasl.SaslServerFactoryImpl");
// Add SaslServer supporting the SCRAM-SHA-1 SASL mechanism
put("SaslServerFactory.SCRAM-SHA-1", "org.jivesoftware.openfire.sasl.SaslServerFactoryImpl");
} }
} }
\ No newline at end of file
...@@ -38,9 +38,10 @@ import org.jivesoftware.openfire.clearspace.ClearspaceSaslServer; ...@@ -38,9 +38,10 @@ import org.jivesoftware.openfire.clearspace.ClearspaceSaslServer;
public class SaslServerFactoryImpl implements SaslServerFactory { public class SaslServerFactoryImpl implements SaslServerFactory {
private static final String myMechs[] = { "PLAIN", "CLEARSPACE" }; private static final String myMechs[] = { "PLAIN", "CLEARSPACE", "SCRAM-SHA-1" };
private static final int PLAIN = 0; private static final int PLAIN = 0;
private static final int CLEARSPACE = 1; private static final int CLEARSPACE = 1;
private static final int SCRAM_SHA_1 = 2;
public SaslServerFactoryImpl() { public SaslServerFactoryImpl() {
} }
...@@ -70,6 +71,12 @@ public class SaslServerFactoryImpl implements SaslServerFactory { ...@@ -70,6 +71,12 @@ public class SaslServerFactoryImpl implements SaslServerFactory {
} }
return new ClearspaceSaslServer(); return new ClearspaceSaslServer();
} }
else if (mechanism.equals(myMechs[SCRAM_SHA_1])) {
if (cbh == null) {
throw new SaslException("CallbackHandler with support for AuthorizeCallback required");
}
return new ScramSha1SaslServer();
}
return null; return null;
} }
......
...@@ -62,14 +62,14 @@ public class DefaultUserProvider implements UserProvider { ...@@ -62,14 +62,14 @@ public class DefaultUserProvider implements UserProvider {
private static final Logger Log = LoggerFactory.getLogger(DefaultUserProvider.class); private static final Logger Log = LoggerFactory.getLogger(DefaultUserProvider.class);
private static final String LOAD_USER = private static final String LOAD_USER =
"SELECT name, email, creationDate, modificationDate FROM ofUser WHERE username=?"; "SELECT salt, serverKey, storedKey, iterations, name, email, creationDate, modificationDate FROM ofUser WHERE username=?";
private static final String USER_COUNT = private static final String USER_COUNT =
"SELECT count(*) FROM ofUser"; "SELECT count(*) FROM ofUser";
private static final String ALL_USERS = private static final String ALL_USERS =
"SELECT username FROM ofUser ORDER BY username"; "SELECT username FROM ofUser ORDER BY username";
private static final String INSERT_USER = private static final String INSERT_USER =
"INSERT INTO ofUser (username,plainPassword,encryptedPassword,name,email,creationDate,modificationDate) " + "INSERT INTO ofUser (username,name,email,creationDate,modificationDate) " +
"VALUES (?,?,?,?,?,?,?)"; "VALUES (?,?,?,?,?)";
private static final String DELETE_USER_FLAGS = private static final String DELETE_USER_FLAGS =
"DELETE FROM ofUserFlag WHERE username=?"; "DELETE FROM ofUserFlag WHERE username=?";
private static final String DELETE_USER_PROPS = private static final String DELETE_USER_PROPS =
...@@ -85,7 +85,7 @@ public class DefaultUserProvider implements UserProvider { ...@@ -85,7 +85,7 @@ public class DefaultUserProvider implements UserProvider {
private static final String UPDATE_MODIFICATION_DATE = private static final String UPDATE_MODIFICATION_DATE =
"UPDATE ofUser SET modificationDate=? WHERE username=?"; "UPDATE ofUser SET modificationDate=? WHERE username=?";
private static final boolean IS_READ_ONLY = false; private static final boolean IS_READ_ONLY = false;
public User loadUser(String username) throws UserNotFoundException { public User loadUser(String username) throws UserNotFoundException {
if(username.contains("@")) { if(username.contains("@")) {
if (!XMPPServer.getInstance().isLocal(new JID(username))) { if (!XMPPServer.getInstance().isLocal(new JID(username))) {
...@@ -104,12 +104,21 @@ public class DefaultUserProvider implements UserProvider { ...@@ -104,12 +104,21 @@ public class DefaultUserProvider implements UserProvider {
if (!rs.next()) { if (!rs.next()) {
throw new UserNotFoundException(); throw new UserNotFoundException();
} }
String name = rs.getString(1); String salt = rs.getString(1);
String email = rs.getString(2); String serverKey = rs.getString(2);
Date creationDate = new Date(Long.parseLong(rs.getString(3).trim())); String storedKey = rs.getString(3);
Date modificationDate = new Date(Long.parseLong(rs.getString(4).trim())); int iterations = rs.getInt(4);
String name = rs.getString(5);
String email = rs.getString(6);
Date creationDate = new Date(Long.parseLong(rs.getString(7).trim()));
Date modificationDate = new Date(Long.parseLong(rs.getString(8).trim()));
return new User(username, name, email, creationDate, modificationDate); User user = new User(username, name, email, creationDate, modificationDate);
user.setSalt(salt);
user.setServerKey(serverKey);
user.setStoredKey(storedKey);
user.setIterations(iterations);
return user;
} }
catch (Exception e) { catch (Exception e) {
throw new UserNotFoundException(e); throw new UserNotFoundException(e);
...@@ -129,22 +138,6 @@ public class DefaultUserProvider implements UserProvider { ...@@ -129,22 +138,6 @@ public class DefaultUserProvider implements UserProvider {
} }
catch (UserNotFoundException unfe) { catch (UserNotFoundException unfe) {
// The user doesn't already exist so we can create a new user // The user doesn't already exist so we can create a new user
// Determine if the password should be stored as plain text or encrypted.
boolean usePlainPassword = JiveGlobals.getBooleanProperty("user.usePlainPassword");
String encryptedPassword = null;
if (!usePlainPassword) {
try {
encryptedPassword = AuthFactory.encryptPassword(password);
// Set password to null so that it's inserted that way.
password = null;
}
catch (UnsupportedOperationException uoe) {
// Encrypting the password may have failed if in setup mode. Therefore,
// use the plain password.
}
}
Date now = new Date(); Date now = new Date();
Connection con = null; Connection con = null;
PreparedStatement pstmt = null; PreparedStatement pstmt = null;
...@@ -152,32 +145,20 @@ public class DefaultUserProvider implements UserProvider { ...@@ -152,32 +145,20 @@ public class DefaultUserProvider implements UserProvider {
con = DbConnectionManager.getConnection(); con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(INSERT_USER); pstmt = con.prepareStatement(INSERT_USER);
pstmt.setString(1, username); pstmt.setString(1, username);
if (password == null) {
pstmt.setNull(2, Types.VARCHAR);
}
else {
pstmt.setString(2, password);
}
if (encryptedPassword == null) {
pstmt.setNull(3, Types.VARCHAR);
}
else {
pstmt.setString(3, encryptedPassword);
}
if (name == null || name.matches("\\s*")) { if (name == null || name.matches("\\s*")) {
pstmt.setNull(4, Types.VARCHAR); pstmt.setNull(2, Types.VARCHAR);
} }
else { else {
pstmt.setString(4, name); pstmt.setString(2, name);
} }
if (email == null || email.matches("\\s*")) { if (email == null || email.matches("\\s*")) {
pstmt.setNull(5, Types.VARCHAR); pstmt.setNull(3, Types.VARCHAR);
} }
else { else {
pstmt.setString(5, email); pstmt.setString(3, email);
} }
pstmt.setString(6, StringUtils.dateToMillis(now)); pstmt.setString(4, StringUtils.dateToMillis(now));
pstmt.setString(7, StringUtils.dateToMillis(now)); pstmt.setString(5, StringUtils.dateToMillis(now));
pstmt.execute(); pstmt.execute();
} }
catch (Exception e) { catch (Exception e) {
...@@ -186,6 +167,12 @@ public class DefaultUserProvider implements UserProvider { ...@@ -186,6 +167,12 @@ public class DefaultUserProvider implements UserProvider {
finally { finally {
DbConnectionManager.closeConnection(pstmt, con); DbConnectionManager.closeConnection(pstmt, con);
} }
try {
AuthFactory.setPassword(username, password);
} catch(Exception e) {
Log.error("User pasword not set", e);
}
return new User(username, name, email, now, now); return new User(username, name, email, now, now);
} }
} }
......
...@@ -84,6 +84,10 @@ public class User implements Cacheable, Externalizable, Result { ...@@ -84,6 +84,10 @@ public class User implements Cacheable, Externalizable, Result {
private static final String EMAIL_VISIBLE_PROPERTY = "email.visible"; private static final String EMAIL_VISIBLE_PROPERTY = "email.visible";
private String username; private String username;
private String salt;
private String storedKey;
private String serverKey;
private int iterations;
private String name; private String name;
private String email; private String email;
private Date creationDate; private Date creationDate;
...@@ -199,6 +203,38 @@ public class User implements Cacheable, Externalizable, Result { ...@@ -199,6 +203,38 @@ public class User implements Cacheable, Externalizable, Result {
Log.error(e.getMessage(), e); Log.error(e.getMessage(), e);
} }
} }
public String getStoredKey() {
return storedKey;
}
public void setStoredKey(String storedKey) {
this.storedKey = storedKey;
}
public String getServerKey() {
return serverKey;
}
public void setServerKey(String serverKey) {
this.serverKey = serverKey;
}
public String getSalt() {
return salt;
}
public void setSalt(String salt) {
this.salt = salt;
}
public int getIterations() {
return iterations;
}
public void setIterations(int iterations) {
this.iterations = iterations;
}
public String getName() { public String getName() {
return name == null ? "" : name; return name == null ? "" : name;
......
...@@ -71,6 +71,11 @@ ...@@ -71,6 +71,11 @@
if (password == null) { if (password == null) {
errors.put("password", "password"); errors.put("password", "password");
} }
try {
AuthFactory.authenticate("admin", "admin");
} catch (Exception e) {
errors.put("password", "password");
}
if (email == null) { if (email == null) {
errors.put("email", "email"); errors.put("email", "email");
} }
...@@ -248,14 +253,15 @@ function checkClick() { ...@@ -248,14 +253,15 @@ function checkClick() {
<% <%
// If the current password is "admin", don't show the text box for them to type // If the current password is "admin", don't show the text box for them to type
// the current password. This makes setup simpler for first-time users. // the current password. This makes setup simpler for first-time users.
String currentPass = null; boolean defaultPassword = false;
try { try {
currentPass = AuthFactory.getPassword("admin"); AuthFactory.authenticate("admin", "admin");
defaultPassword = true;
} }
catch (Exception e) { catch (Exception e) {
// Ignore. // Ignore.
} }
if ("admin".equals(currentPass)) { if (defaultPassword) {
%> %>
<input type="hidden" name="password" value="admin"> <input type="hidden" name="password" value="admin">
<% <%
......
...@@ -25,6 +25,8 @@ ...@@ -25,6 +25,8 @@
JiveGlobals.getProperty("provider.auth.className")); JiveGlobals.getProperty("provider.auth.className"));
boolean isCLEARSPACE = "org.jivesoftware.openfire.clearspace.ClearspaceAuthProvider".equals( boolean isCLEARSPACE = "org.jivesoftware.openfire.clearspace.ClearspaceAuthProvider".equals(
JiveGlobals.getProperty("provider.auth.className")); JiveGlobals.getProperty("provider.auth.className"));
boolean scramOnly = JiveGlobals.getBooleanProperty("user.scramHashedPasswordOnly");
boolean requestedScramOnly = (request.getParameter("scramOnly") != null);
boolean next = request.getParameter("continue") != null; boolean next = request.getParameter("continue") != null;
if (next) { if (next) {
// Figure out where to send the user. // Figure out where to send the user.
...@@ -49,6 +51,9 @@ ...@@ -49,6 +51,9 @@
org.jivesoftware.openfire.security.DefaultSecurityAuditProvider.class.getName())); org.jivesoftware.openfire.security.DefaultSecurityAuditProvider.class.getName()));
xmppSettings.put("provider.admin.className", JiveGlobals.getXMLProperty("provider.admin.className", xmppSettings.put("provider.admin.className", JiveGlobals.getXMLProperty("provider.admin.className",
org.jivesoftware.openfire.admin.DefaultAdminProvider.class.getName())); org.jivesoftware.openfire.admin.DefaultAdminProvider.class.getName()));
if (requestedScramOnly) {
JiveGlobals.setProperty("user.scramHashedPasswordOnly", "true");
}
// Redirect // Redirect
response.sendRedirect("setup-admin-settings.jsp"); response.sendRedirect("setup-admin-settings.jsp");
...@@ -93,6 +98,15 @@ ...@@ -93,6 +98,15 @@
<fmt:message key="setup.profile.default_description" /> <fmt:message key="setup.profile.default_description" />
</td> </td>
</tr> </tr>
<tr>
<td align="center" valign="top">
<input type="checkbox" name="scramOnly" value="scramOnly" id="rb01-0" <% if (scramOnly) { %>checked<% } %>>
</td>
<td>
<label for="rb01-0"><b><fmt:message key="setup.profile.default.scramOnly" /></b></label><br>
<fmt:message key="setup.profile.default.scramOnly_description" />
</td>
</tr>
<tr> <tr>
<td align="center" valign="top"> <td align="center" valign="top">
<input type="radio" name="mode" value="ldap" id="rb02" <% if (isLDAP) { %>checked<% } %>> <input type="radio" name="mode" value="ldap" id="rb02" <% if (isLDAP) { %>checked<% } %>>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment