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 @@
CREATE TABLE ofUser (
username VARCHAR(64) NOT NULL,
storedKey VARCHAR(32),
serverKey VARCHAR(32),
salt VARCHAR(32),
iterations INTEGER,
plainPassword VARCHAR(32),
encryptedPassword VARCHAR(255),
name VARCHAR(100),
......
......@@ -3,6 +3,10 @@
CREATE TABLE ofUser (
username VARCHAR(64) NOT NULL,
storedKey VARCHAR(32),
serverKey VARCHAR(32),
salt VARCHAR(32),
iterations INTEGER,
plainPassword VARCHAR(32),
encryptedPassword VARCHAR(255),
name VARCHAR(100),
......
......@@ -3,6 +3,10 @@
CREATE TABLE ofUser (
username VARCHAR(64) NOT NULL,
storedKey VARCHAR(32),
serverKey VARCHAR(32),
salt VARCHAR(32),
iterations INTEGER,
plainPassword VARCHAR(32),
encryptedPassword VARCHAR(255),
name VARCHAR(100),
......
......@@ -3,6 +3,10 @@
CREATE TABLE ofUser (
username VARCHAR2(64) NOT NULL,
storedKey VARCHAR(32),
serverKey VARCHAR(32),
salt VARCHAR(32),
iterations INTEGER,
plainPassword VARCHAR2(32),
encryptedPassword VARCHAR2(255),
name VARCHAR2(100),
......
......@@ -5,6 +5,10 @@
CREATE TABLE ofUser (
username VARCHAR(64) NOT NULL,
storedKey VARCHAR(32),
serverKey VARCHAR(32),
salt VARCHAR(32),
iterations INTEGER,
plainPassword VARCHAR(32),
encryptedPassword VARCHAR(255),
name VARCHAR(100),
......
......@@ -3,6 +3,10 @@
CREATE TABLE ofUser (
username NVARCHAR(64) NOT NULL,
storedKey VARCHAR(32),
serverKey VARCHAR(32),
salt VARCHAR(32),
iterations INTEGER,
plainPassword NVARCHAR(32),
encryptedPassword NVARCHAR(255),
name NVARCHAR(100),
......
......@@ -3,6 +3,10 @@
CREATE TABLE ofUser (
username NVARCHAR(64) NOT NULL,
storedKey VARCHAR(32),
serverKey VARCHAR(32),
salt VARCHAR(32),
iterations INTEGER,
plainPassword NVARCHAR(32) NULL,
encryptedPassword NVARCHAR(255) 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
setup.profile.default=Default
setup.profile.default_description=Store users and groups in the server database. This is the \
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_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 \
......
......@@ -68,7 +68,7 @@ public class SchemaManager {
/**
* Current Openfire database schema version.
*/
private static final int DATABASE_VERSION = 21;
private static final int DATABASE_VERSION = 22;
/**
* Creates a new Schema manager.
......
......@@ -49,6 +49,7 @@ import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.openfire.admin.AdminManager;
import org.jivesoftware.openfire.audit.AuditManager;
import org.jivesoftware.openfire.audit.spi.AuditManagerImpl;
import org.jivesoftware.openfire.auth.ScramUtils;
import org.jivesoftware.openfire.clearspace.ClearspaceManager;
import org.jivesoftware.openfire.cluster.ClusterManager;
import org.jivesoftware.openfire.cluster.NodeID;
......@@ -420,6 +421,9 @@ public class XMPPServer {
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)
try {
......
......@@ -328,4 +328,9 @@ public class AuthFactory {
}
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 {
* backend user store.
*/
public boolean supportsPasswordRetrieval();
boolean isScramSupported();
}
\ No newline at end of file
......@@ -20,12 +20,19 @@
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.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import javax.security.sasl.SaslException;
import javax.xml.bind.DatatypeConverter;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.user.UserNotFoundException;
......@@ -46,10 +53,14 @@ public class DefaultAuthProvider implements AuthProvider {
private static final Logger Log = LoggerFactory.getLogger(DefaultAuthProvider.class);
private static final String LOAD_PASSWORD =
"SELECT plainPassword,encryptedPassword FROM ofUser WHERE username=?";
private static final String LOAD_PASSWORD =
"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 =
"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.
......@@ -75,7 +86,7 @@ public class DefaultAuthProvider implements AuthProvider {
}
}
try {
if (!password.equals(getPassword(username))) {
if (!checkPassword(username, password)) {
throw new UnauthorizedException();
}
}
......@@ -119,7 +130,8 @@ public class DefaultAuthProvider implements AuthProvider {
}
public boolean isDigestSupported() {
return true;
boolean scramOnly = JiveGlobals.getBooleanProperty("user.scramHashedPasswordOnly");
return !scramOnly;
}
public String getPassword(String username) throws UserNotFoundException {
......@@ -159,6 +171,9 @@ public class DefaultAuthProvider implements AuthProvider {
// Ignore and return plain password instead.
}
}
if (plainText == null) {
throw new UnsupportedOperationException();
}
return plainText;
}
catch (SQLException sqle) {
......@@ -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 {
// Determine if the password should be stored as plain text or encrypted.
boolean usePlainPassword = JiveGlobals.getBooleanProperty("user.usePlainPassword");
boolean scramOnly = JiveGlobals.getBooleanProperty("user.scramHashedPasswordOnly");
String encryptedPassword = null;
if (username.contains("@")) {
// Check that the specified domain matches the server's domain
......@@ -184,7 +270,26 @@ public class DefaultAuthProvider implements AuthProvider {
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 {
encryptedPassword = AuthFactory.encryptPassword(password);
// Set password to null so that it's inserted that way.
......@@ -195,6 +300,10 @@ public class DefaultAuthProvider implements AuthProvider {
// the plain password will be stored.
}
}
if (scramOnly) {
encryptedPassword = null;
password = null;
}
Connection con = null;
PreparedStatement pstmt = null;
......@@ -213,7 +322,21 @@ public class DefaultAuthProvider implements AuthProvider {
else {
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();
}
catch (SQLException sqle) {
......@@ -225,6 +348,12 @@ public class DefaultAuthProvider implements AuthProvider {
}
public boolean supportsPasswordRetrieval() {
boolean scramOnly = JiveGlobals.getBooleanProperty("user.scramHashedPasswordOnly");
return !scramOnly;
}
@Override
public boolean isScramSupported() {
return true;
}
}
\ No newline at end of file
......@@ -257,4 +257,10 @@ public class HybridAuthProvider implements AuthProvider {
public boolean supportsPasswordRetrieval() {
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 {
}
}
}
@Override
public boolean isScramSupported() {
// TODO Auto-generated method stub
return false;
}
}
......@@ -204,4 +204,10 @@ public class NativeAuthProvider implements AuthProvider {
public boolean supportsPasswordRetrieval() {
return false;
}
@Override
public boolean isScramSupported() {
// TODO Auto-generated method stub
return false;
}
}
......@@ -245,4 +245,8 @@ public class POP3AuthProvider implements AuthProvider {
public boolean supportsPasswordRetrieval() {
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 {
public boolean supportsPasswordRetrieval() {
return false;
}
@Override
public boolean isScramSupported() {
return false;
}
}
......@@ -109,4 +109,10 @@ public class CrowdAuthProvider implements AuthProvider {
return false;
}
@Override
public boolean isScramSupported() {
// TODO Auto-generated method stub
return false;
}
}
......@@ -160,4 +160,9 @@ public class LdapAuthProvider implements AuthProvider {
public boolean supportsPasswordRetrieval() {
return false;
}
@Override
public boolean isScramSupported() {
return false;
}
}
\ No newline at end of file
......@@ -32,6 +32,7 @@ import java.util.Set;
import java.util.StringTokenizer;
import java.util.TimeZone;
import javax.naming.NamingEnumeration;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
......@@ -133,7 +134,39 @@ public class LdapUserProvider implements UserProvider {
}
// Escape the username so that it can be used as a JID.
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) {
throw new UserNotFoundException(e);
......
......@@ -802,6 +802,11 @@ public class SASLAuthentication {
it.remove();
}
}
else if (mech.equals("SCRAM-SHA-1")) {
if (!AuthFactory.supportsPasswordRetrieval() && !AuthFactory.supportsScram()) {
it.remove();
}
}
else if (mech.equals("ANONYMOUS")) {
// Check anonymous is supported
if (!XMPPServer.getInstance().getIQAuthHandler().isAnonymousAllowed()) {
......@@ -832,6 +837,7 @@ public class SASLAuthentication {
mechanisms.add("PLAIN");
mechanisms.add("DIGEST-MD5");
mechanisms.add("CRAM-MD5");
mechanisms.add("SCRAM-SHA-1");
mechanisms.add("JIVE-SHAREDSECRET");
}
else {
......@@ -843,6 +849,7 @@ public class SASLAuthentication {
mech.equals("PLAIN") ||
mech.equals("DIGEST-MD5") ||
mech.equals("CRAM-MD5") ||
mech.equals("SCRAM-SHA-1") ||
mech.equals("GSSAPI") ||
mech.equals("EXTERNAL") ||
mech.equals("JIVE-SHAREDSECRET"))
......
......@@ -34,10 +34,12 @@ public class SaslProvider extends Provider {
* Constructs a the JiveSoftware SASL provider.
*/
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
put("SaslServerFactory.PLAIN", "org.jivesoftware.openfire.sasl.SaslServerFactoryImpl");
// Add SaslServer supporting the Clearspace SASL mechanism
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;
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 CLEARSPACE = 1;
private static final int SCRAM_SHA_1 = 2;
public SaslServerFactoryImpl() {
}
......@@ -70,6 +71,12 @@ public class SaslServerFactoryImpl implements SaslServerFactory {
}
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;
}
......
/**
* $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.sasl;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;
import javax.xml.bind.DatatypeConverter;
import org.jivesoftware.openfire.auth.AuthFactory;
import org.jivesoftware.openfire.auth.ConnectionException;
import org.jivesoftware.openfire.auth.InternalUnauthenticatedException;
import org.jivesoftware.openfire.auth.ScramUtils;
import org.jivesoftware.openfire.user.UserManager;
import org.jivesoftware.openfire.user.UserNotFoundException;
/**
* Implements the SCRAM-SHA-1 server-side mechanism.
*
* @author Richard Midwinter
*/
public class ScramSha1SaslServer implements SaslServer {
private static final Pattern
CLIENT_FIRST_MESSAGE = Pattern.compile("^(([pny])=?([^,]*),([^,]*),)(m?=?[^,]*,?n=([^,]*),r=([^,]*),?.*)$"),
CLIENT_FINAL_MESSAGE = Pattern.compile("(c=([^,]*),r=([^,]*)),p=(.*)$");
private String username;
private State state = State.INITIAL;
private String nonce;
private String serverFirstMessage;
private String clientFirstMessageBare;
private SecureRandom random = new SecureRandom();
private enum State {
INITIAL,
IN_PROGRESS,
COMPLETE;
}
public ScramSha1SaslServer() {
}
/**
* Returns the IANA-registered mechanism name of this SASL server.
* ("SCRAM-SHA-1").
* @return A non-null string representing the IANA-registered mechanism name.
*/
public String getMechanismName() {
return "SCRAM-SHA-1";
}
/**
* Evaluates the response data and generates a challenge.
*
* If a response is received from the client during the authentication
* process, this method is called to prepare an appropriate next
* challenge to submit to the client. The challenge is null if the
* authentication has succeeded and no more challenge data is to be sent
* to the client. It is non-null if the authentication must be continued
* by sending a challenge to the client, or if the authentication has
* succeeded but challenge data needs to be processed by the client.
* <tt>isComplete()</tt> should be called
* after each call to <tt>evaluateResponse()</tt>,to determine if any further
* response is needed from the client.
*
* @param response The non-null (but possibly empty) response sent
* by the client.
*
* @return The possibly null challenge to send to the client.
* It is null if the authentication has succeeded and there is
* no more challenge data to be sent to the client.
* @exception SaslException If an error occurred while processing
* the response or generating a challenge.
*/
@Override
public byte[] evaluateResponse(final byte[] response) throws SaslException {
byte[] challenge;
switch (state) {
case INITIAL:
challenge = generateServerFirstMessage(response);
state = State.IN_PROGRESS;
break;
case IN_PROGRESS:
challenge = generateServerFinalMessage(response);
state = State.COMPLETE;
break;
case COMPLETE:
if (response == null || response.length == 0) {
challenge = new byte[0];
break;
}
default:
throw new SaslException("No response expected in state " + state);
}
return challenge;
}
/**
* First response returns:
* - the nonce (client nonce appended with our own random UUID)
* - the salt
* - the number of iterations
*/
private byte[] generateServerFirstMessage(final byte[] response) throws SaslException {
String clientFirstMessage = new String(response, StandardCharsets.US_ASCII);
Matcher m = CLIENT_FIRST_MESSAGE.matcher(clientFirstMessage);
if (!m.matches()) {
throw new SaslException("Invalid first client message");
}
// String gs2Header = m.group(1);
// String gs2CbindFlag = m.group(2);
// String gs2CbindName = m.group(3);
// String authzId = m.group(4);
clientFirstMessageBare = m.group(5);
username = m.group(6);
String clientNonce = m.group(7);
nonce = clientNonce + UUID.randomUUID().toString();
try {
serverFirstMessage = String.format("r=%s,s=%s,i=%d", nonce, DatatypeConverter.printBase64Binary(getSalt(username)),
getIterations(username));
} catch (UserNotFoundException e) {
throw new SaslException(e.getMessage(), e);
}
return serverFirstMessage.getBytes(StandardCharsets.US_ASCII);
}
/**
* Final response returns the server signature.
*/
private byte[] generateServerFinalMessage(final byte[] response) throws SaslException {
String clientFinalMessage = new String(response, StandardCharsets.US_ASCII);
Matcher m = CLIENT_FINAL_MESSAGE.matcher(clientFinalMessage);
if (!m.matches()) {
throw new SaslException("Invalid client final message");
}
String clientFinalMessageWithoutProof = m.group(1);
// String channelBinding = m.group(2);
String clientNonce = m.group(3);
String proof = m.group(4);
if (!nonce.equals(clientNonce)) {
throw new SaslException("Client final message has incorrect nonce value");
}
try {
String authMessage = clientFirstMessageBare + "," + serverFirstMessage + "," + clientFinalMessageWithoutProof;
byte[] storedKey = getStoredKey(username);
byte[] serverKey = getServerKey(username);
byte[] clientSignature = ScramUtils.computeHmac(storedKey, authMessage);
byte[] serverSignature = ScramUtils.computeHmac(serverKey, authMessage);
byte[] clientKey = clientSignature.clone();
byte[] decodedProof = DatatypeConverter.parseBase64Binary(proof);
for (int i = 0; i < clientKey.length; i++) {
clientKey[i] ^= decodedProof[i];
}
if (!Arrays.equals(storedKey, MessageDigest.getInstance("SHA-1").digest(clientKey))) {
throw new SaslException("Authentication failed");
}
return ("v=" + DatatypeConverter.printBase64Binary(serverSignature))
.getBytes(StandardCharsets.US_ASCII);
} catch (UnsupportedEncodingException | UserNotFoundException | NoSuchAlgorithmException e) {
throw new SaslException(e.getMessage(), e);
}
}
/**
* Determines whether the authentication exchange has completed.
* This method is typically called after each invocation of
* <tt>evaluateResponse()</tt> to determine whether the
* authentication has completed successfully or should be continued.
* @return true if the authentication exchange has completed; false otherwise.
*/
public boolean isComplete() {
return state == State.COMPLETE;
}
/**
* Reports the authorization ID in effect for the client of this
* session.
* This method can only be called if isComplete() returns true.
* @return The authorization ID of the client.
* @exception IllegalStateException if this authentication session has not completed
*/
public String getAuthorizationID() {
if (isComplete()) {
return username;
} else {
throw new IllegalStateException("SCRAM-SHA-1 authentication not completed");
}
}
/**
* Unwraps a byte array received from the client. SCRAM-SHA-1 supports no security layer.
*
* @throws SaslException if attempted to use this method.
*/
public byte[] unwrap(byte[] incoming, int offset, int len)
throws SaslException {
if (isComplete()) {
throw new IllegalStateException("SCRAM-SHA-1 does not support integrity or privacy");
} else {
throw new IllegalStateException("SCRAM-SHA-1 authentication not completed");
}
}
/**
* Wraps a byte array to be sent to the client. SCRAM-SHA-1 supports no security layer.
*
* @throws SaslException if attempted to use this method.
*/
public byte[] wrap(byte[] outgoing, int offset, int len)
throws SaslException {
if (isComplete()) {
throw new IllegalStateException("SCRAM-SHA-1 does not support integrity or privacy");
} else {
throw new IllegalStateException("SCRAM-SHA-1 authentication not completed");
}
}
/**
* Retrieves the negotiated property.
* This method can be called only after the authentication exchange has
* completed (i.e., when <tt>isComplete()</tt> returns true); otherwise, an
* <tt>IllegalStateException</tt> is thrown.
*
* @param propName the property
* @return The value of the negotiated property. If null, the property was
* not negotiated or is not applicable to this mechanism.
* @exception IllegalStateException if this authentication exchange has not completed
*/
public Object getNegotiatedProperty(String propName) {
if (isComplete()) {
if (propName.equals(Sasl.QOP)) {
return "auth";
} else {
return null;
}
} else {
throw new IllegalStateException("SCRAM-SHA-1 authentication not completed");
}
}
/**
* Disposes of any system resources or security-sensitive information
* the SaslServer might be using. Invoking this method invalidates
* the SaslServer instance. This method is idempotent.
* @throws SaslException If a problem was encountered while disposing
* the resources.
*/
public void dispose() throws SaslException {
username = null;
state = State.INITIAL;
}
/**
* Retrieve the salt from the database for a given username.
*
* Returns a random salt if the user doesn't exist to mimic an invalid password.
*/
private byte[] getSalt(final String username) {
try {
String saltshaker = UserManager.getUserProvider().loadUser(username).getSalt();
byte[] salt;
if (saltshaker == null) {
String password = AuthFactory.getPassword(username);
AuthFactory.setPassword(username, password);
salt = DatatypeConverter.parseBase64Binary(UserManager.getUserProvider().loadUser(username).getSalt());
} else {
salt = DatatypeConverter.parseBase64Binary(saltshaker);
}
return salt;
} catch (UserNotFoundException | UnsupportedOperationException | ConnectionException | InternalUnauthenticatedException e) {
byte[] salt = new byte[32];
random.nextBytes(salt);
return salt;
}
}
/**
* Retrieve the iteration count from the database for a given username.
*/
private int getIterations(final String username) throws UserNotFoundException {
return UserManager.getUserProvider().loadUser(username).getIterations();
}
/**
* Retrieve the server key from the database for a given username.
*/
private byte[] getServerKey(final String username) throws UserNotFoundException {
return DatatypeConverter.parseBase64Binary(
UserManager.getUserProvider().loadUser(username).getServerKey());
}
/**
* Retrieve the stored key from the database for a given username.
*/
private byte[] getStoredKey(final String username) throws UserNotFoundException {
return DatatypeConverter.parseBase64Binary(
UserManager.getUserProvider().loadUser(username).getStoredKey());
}
}
......@@ -62,14 +62,14 @@ public class DefaultUserProvider implements UserProvider {
private static final Logger Log = LoggerFactory.getLogger(DefaultUserProvider.class);
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 =
"SELECT count(*) FROM ofUser";
private static final String ALL_USERS =
"SELECT username FROM ofUser ORDER BY username";
private static final String INSERT_USER =
"INSERT INTO ofUser (username,plainPassword,encryptedPassword,name,email,creationDate,modificationDate) " +
"VALUES (?,?,?,?,?,?,?)";
"INSERT INTO ofUser (username,name,email,creationDate,modificationDate) " +
"VALUES (?,?,?,?,?)";
private static final String DELETE_USER_FLAGS =
"DELETE FROM ofUserFlag WHERE username=?";
private static final String DELETE_USER_PROPS =
......@@ -85,7 +85,7 @@ public class DefaultUserProvider implements UserProvider {
private static final String UPDATE_MODIFICATION_DATE =
"UPDATE ofUser SET modificationDate=? WHERE username=?";
private static final boolean IS_READ_ONLY = false;
public User loadUser(String username) throws UserNotFoundException {
if(username.contains("@")) {
if (!XMPPServer.getInstance().isLocal(new JID(username))) {
......@@ -104,12 +104,21 @@ public class DefaultUserProvider implements UserProvider {
if (!rs.next()) {
throw new UserNotFoundException();
}
String name = rs.getString(1);
String email = rs.getString(2);
Date creationDate = new Date(Long.parseLong(rs.getString(3).trim()));
Date modificationDate = new Date(Long.parseLong(rs.getString(4).trim()));
String salt = rs.getString(1);
String serverKey = rs.getString(2);
String storedKey = rs.getString(3);
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) {
throw new UserNotFoundException(e);
......@@ -129,22 +138,6 @@ public class DefaultUserProvider implements UserProvider {
}
catch (UserNotFoundException unfe) {
// 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();
Connection con = null;
PreparedStatement pstmt = null;
......@@ -152,32 +145,20 @@ public class DefaultUserProvider implements UserProvider {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(INSERT_USER);
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*")) {
pstmt.setNull(4, Types.VARCHAR);
pstmt.setNull(2, Types.VARCHAR);
}
else {
pstmt.setString(4, name);
pstmt.setString(2, name);
}
if (email == null || email.matches("\\s*")) {
pstmt.setNull(5, Types.VARCHAR);
pstmt.setNull(3, Types.VARCHAR);
}
else {
pstmt.setString(5, email);
pstmt.setString(3, email);
}
pstmt.setString(6, StringUtils.dateToMillis(now));
pstmt.setString(7, StringUtils.dateToMillis(now));
pstmt.setString(4, StringUtils.dateToMillis(now));
pstmt.setString(5, StringUtils.dateToMillis(now));
pstmt.execute();
}
catch (Exception e) {
......@@ -186,6 +167,12 @@ public class DefaultUserProvider implements UserProvider {
finally {
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);
}
}
......
......@@ -84,6 +84,10 @@ public class User implements Cacheable, Externalizable, Result {
private static final String EMAIL_VISIBLE_PROPERTY = "email.visible";
private String username;
private String salt;
private String storedKey;
private String serverKey;
private int iterations;
private String name;
private String email;
private Date creationDate;
......@@ -199,6 +203,38 @@ public class User implements Cacheable, Externalizable, Result {
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() {
return name == null ? "" : name;
......
......@@ -71,6 +71,11 @@
if (password == null) {
errors.put("password", "password");
}
try {
AuthFactory.authenticate("admin", "admin");
} catch (Exception e) {
errors.put("password", "password");
}
if (email == null) {
errors.put("email", "email");
}
......@@ -248,14 +253,15 @@ function checkClick() {
<%
// 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.
String currentPass = null;
boolean defaultPassword = false;
try {
currentPass = AuthFactory.getPassword("admin");
AuthFactory.authenticate("admin", "admin");
defaultPassword = true;
}
catch (Exception e) {
// Ignore.
}
if ("admin".equals(currentPass)) {
if (defaultPassword) {
%>
<input type="hidden" name="password" value="admin">
<%
......
......@@ -25,6 +25,8 @@
JiveGlobals.getProperty("provider.auth.className"));
boolean isCLEARSPACE = "org.jivesoftware.openfire.clearspace.ClearspaceAuthProvider".equals(
JiveGlobals.getProperty("provider.auth.className"));
boolean scramOnly = JiveGlobals.getBooleanProperty("user.scramHashedPasswordOnly");
boolean requestedScramOnly = (request.getParameter("scramOnly") != null);
boolean next = request.getParameter("continue") != null;
if (next) {
// Figure out where to send the user.
......@@ -49,6 +51,9 @@
org.jivesoftware.openfire.security.DefaultSecurityAuditProvider.class.getName()));
xmppSettings.put("provider.admin.className", JiveGlobals.getXMLProperty("provider.admin.className",
org.jivesoftware.openfire.admin.DefaultAdminProvider.class.getName()));
if (requestedScramOnly) {
JiveGlobals.setProperty("user.scramHashedPasswordOnly", "true");
}
// Redirect
response.sendRedirect("setup-admin-settings.jsp");
......@@ -93,6 +98,15 @@
<fmt:message key="setup.profile.default_description" />
</td>
</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>
<td align="center" valign="top">
<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