Skip to content

Commit 69287ec

Browse files
committed
OF-3327: Parameterize existing SCRAM implementation
This prepares the codebase to have additional SCRAM-based mechanisms (such as `SCRAM-SHA-256`). It does so by moving generic bits of the implementation that is currently specific to the implementation of `SCRAM-SHA-` into a base class that was introduced earlier for this purpose. The implementation in `ScramUtils` now no longer is specific the `SCRAM-SHA-1`: overloaded methods (that take an algorithm reference) have been introduced, the old methods that are implicitly specific to `SCRAM-SHA-1` have been deprecated. This all allows the `ScramSha1SaslServer` implementation to shrink significantly, to only this `SHA-1` bindings. Future implementations of algorithms like `SHA-256` should follow suit and be equally small/short.
1 parent 6e9d1d3 commit 69287ec

13 files changed

Lines changed: 890 additions & 717 deletions

xmppserver/src/main/java/org/jivesoftware/openfire/auth/DefaultAuthProvider.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -348,9 +348,9 @@ public boolean checkPassword(String username, String testPassword) throws UserNo
348348
final byte[] testStoredKey;
349349
try {
350350
final byte[] saltShaker = DatatypeConverter.parseBase64Binary(credential.salt);
351-
final byte[] saltedPassword = ScramUtils.createSaltedPassword(saltShaker, testPassword, credential.iterations);
352-
final byte[] clientKey = ScramUtils.computeHmac(saltedPassword, "Client Key");
353-
testStoredKey = MessageDigest.getInstance("SHA-1").digest(clientKey);
351+
final byte[] saltedPassword = ScramUtils.createSaltedPassword(saltShaker, testPassword, credential.iterations, ScramSha1SaslServer.HMAC_ALGORITHM_NAME);
352+
final byte[] clientKey = ScramUtils.computeHmac(saltedPassword, "Client Key", ScramSha1SaslServer.HMAC_ALGORITHM_NAME);
353+
testStoredKey = MessageDigest.getInstance(ScramSha1SaslServer.DIGEST_ALGORITHM_NAME).digest(clientKey);
354354
} catch(SaslException | NoSuchAlgorithmException | IllegalArgumentException e) {
355355
Log.warn("Unable to check SCRAM values for PLAIN authentication for user '{}'", username, e);
356356
return false;
@@ -391,10 +391,10 @@ public void setPassword(String username, String password) throws UserNotFoundExc
391391
final int iterations = ScramSha1SaslServer.ITERATION_COUNT.getValue();
392392
byte[] saltedPassword = null, clientKey = null, storedKey = null, serverKey = null;
393393
try {
394-
saltedPassword = ScramUtils.createSaltedPassword(saltShaker, password, iterations);
395-
clientKey = ScramUtils.computeHmac(saltedPassword, "Client Key");
396-
storedKey = MessageDigest.getInstance("SHA-1").digest(clientKey);
397-
serverKey = ScramUtils.computeHmac(saltedPassword, "Server Key");
394+
saltedPassword = ScramUtils.createSaltedPassword(saltShaker, password, iterations, ScramSha1SaslServer.HMAC_ALGORITHM_NAME);
395+
clientKey = ScramUtils.computeHmac(saltedPassword, "Client Key", ScramSha1SaslServer.HMAC_ALGORITHM_NAME);
396+
storedKey = MessageDigest.getInstance(ScramSha1SaslServer.DIGEST_ALGORITHM_NAME).digest(clientKey);
397+
serverKey = ScramUtils.computeHmac(saltedPassword, "Server Key", ScramSha1SaslServer.HMAC_ALGORITHM_NAME);
398398
} catch (SaslException | NoSuchAlgorithmException e) {
399399
Log.warn("Unable to persist values for SCRAM authentication.");
400400
}

xmppserver/src/main/java/org/jivesoftware/openfire/auth/HybridAuthProvider.java

Lines changed: 21 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2005-2008 Jive Software, 2016-2024 Ignite Realtime Foundation. All rights reserved.
2+
* Copyright (C) 2005-2008 Jive Software, 2016-2026 Ignite Realtime Foundation. All rights reserved.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717
package org.jivesoftware.openfire.auth;
1818

1919
import org.jivesoftware.openfire.user.UserNotFoundException;
20+
import org.jivesoftware.openfire.sasl.ScramSha1SaslServer;
2021
import org.jivesoftware.util.JiveGlobals;
2122
import org.jivesoftware.util.SystemProperty;
2223
import org.slf4j.Logger;
@@ -290,99 +291,46 @@ public void setPassword(String username, String password)
290291
}
291292

292293
@Override
294+
@Deprecated(forRemoval = true) // Remove in or after Openfire 5.3.0
293295
public String getSalt(String username) throws UnsupportedOperationException, UserNotFoundException
294296
{
295-
if (!isScramSupported()) {
296-
throw new UnsupportedOperationException();
297-
}
298-
299-
// Check overrides first.
300-
try {
301-
return super.getSalt(username);
302-
} catch (UserNotFoundException e) {
303-
if (hasOverride(username)) {
304-
// An override was used. Must not try other providers.
305-
throw e;
306-
}
307-
}
308-
309-
// When there's no override, try all providers in order.
310-
for (final AuthProvider provider: getAuthProviders()) {
311-
try {
312-
provider.getSalt(username);
313-
} catch (UserNotFoundException | UnsupportedOperationException e) {
314-
Log.trace("Could get salt for user {} with auth provider {}. Will try remaining providers (if any)", username, provider.getClass().getName(), e);
315-
}
316-
}
317-
throw new UserNotFoundException();
297+
return getScramCredential(username, ScramSha1SaslServer.MECHANISM_NAME).salt;
318298
}
319299

320300
@Override
301+
@Deprecated(forRemoval = true) // Remove in or after Openfire 5.3.0
321302
public int getIterations(String username) throws UnsupportedOperationException, UserNotFoundException
322303
{
323-
if (!isScramSupported()) {
324-
throw new UnsupportedOperationException();
325-
}
326-
327-
// Check overrides first.
328-
try {
329-
return super.getIterations(username);
330-
} catch (UserNotFoundException e) {
331-
if (hasOverride(username)) {
332-
// An override was used. Must not try other providers.
333-
throw e;
334-
}
335-
}
336-
337-
// When there's no override, try all providers in order.
338-
for (final AuthProvider provider: getAuthProviders()) {
339-
try {
340-
provider.getIterations(username);
341-
} catch (UserNotFoundException | UnsupportedOperationException e) {
342-
Log.trace("Could get iterations for user {} with auth provider {}. Will try remaining providers (if any)", username, provider.getClass().getName(), e);
343-
}
344-
}
345-
throw new UserNotFoundException();
304+
return getScramCredential(username, ScramSha1SaslServer.MECHANISM_NAME).iterations;
346305
}
347306

348307
@Override
308+
@Deprecated(forRemoval = true) // Remove in or after Openfire 5.3.0
349309
public String getServerKey(String username) throws UnsupportedOperationException, UserNotFoundException
350310
{
351-
if (!isScramSupported()) {
352-
throw new UnsupportedOperationException();
353-
}
354-
355-
// Check overrides first.
356-
try {
357-
return super.getServerKey(username);
358-
} catch (UserNotFoundException e) {
359-
if (hasOverride(username)) {
360-
// An override was used. Must not try other providers.
361-
throw e;
362-
}
363-
}
364-
365-
// When there's no override, try all providers in order.
366-
for (final AuthProvider provider: getAuthProviders()) {
367-
try {
368-
provider.getServerKey(username);
369-
} catch (UserNotFoundException | UnsupportedOperationException e) {
370-
Log.trace("Could get serverkey for user {} with auth provider {}. Will try remaining providers (if any)", username, provider.getClass().getName(), e);
371-
}
372-
}
373-
throw new UserNotFoundException();
311+
return getScramCredential(username, ScramSha1SaslServer.MECHANISM_NAME).serverKey;
374312
}
375313

376314
@Override
315+
@Deprecated(forRemoval = true) // Remove in or after Openfire 5.3.0
377316
public String getStoredKey(String username) throws UnsupportedOperationException, UserNotFoundException
317+
{
318+
return getScramCredential(username, ScramSha1SaslServer.MECHANISM_NAME).storedKey;
319+
}
320+
321+
/**
322+
* Returns SCRAM credentials for a user and mechanism.
323+
*/
324+
@Override
325+
public ScramCredentialData getScramCredential(final String username, final String mechanism) throws UnsupportedOperationException, UserNotFoundException
378326
{
379327
if (!isScramSupported()) {
380328
throw new UnsupportedOperationException();
381329
}
382330

383331
// Check overrides first.
384332
try {
385-
return super.getStoredKey(username);
333+
return super.getScramCredential(username, mechanism);
386334
} catch (UserNotFoundException e) {
387335
if (hasOverride(username)) {
388336
// An override was used. Must not try other providers.
@@ -393,9 +341,9 @@ public String getStoredKey(String username) throws UnsupportedOperationException
393341
// When there's no override, try all providers in order.
394342
for (final AuthProvider provider: getAuthProviders()) {
395343
try {
396-
provider.getStoredKey(username);
344+
return provider.getScramCredential(username, mechanism);
397345
} catch (UserNotFoundException | UnsupportedOperationException e) {
398-
Log.trace("Could get storedkey for user {} with auth provider {}. Will try remaining providers (if any)", username, provider.getClass().getName(), e);
346+
Log.trace("Could get SCRAM credential for user {} with auth provider {} and mechanism {}. Will try remaining providers (if any)", username, provider.getClass().getName(), mechanism, e);
399347
}
400348
}
401349
throw new UserNotFoundException();

xmppserver/src/main/java/org/jivesoftware/openfire/auth/ScramUtils.java

Lines changed: 95 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2015 Surevine Ltd, 2016-2018 Ignite Realtime Foundation. All rights reserved
2+
* Copyright (C) 2015 Surevine Ltd, 2016-2026 Ignite Realtime Foundation. All rights reserved
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
1616

1717
package org.jivesoftware.openfire.auth;
1818

19+
import org.jivesoftware.openfire.sasl.ScramSha1SaslServer;
20+
1921
import java.nio.charset.StandardCharsets;
2022
import java.security.InvalidKeyException;
2123
import java.security.NoSuchAlgorithmException;
@@ -25,19 +27,34 @@
2527
import javax.security.sasl.SaslException;
2628

2729
/**
28-
* A utility class that provides methods that are useful for dealing with
29-
* Salted Challenge Response Authentication Mechanism (SCRAM).
30-
*
30+
* A utility class that provides methods that are useful for dealing with Salted Challenge Response Authentication
31+
* Mechanism (SCRAM).
32+
*
33+
* The HMAC algorithm to be used is provided as an argument (in the form of a JCA standard name, such as
34+
* {@code HmacSHA1} or {@code HmacSHA256}), allowing these utilities to serve any SCRAM mechanism.
35+
*
3136
* @author Richard Midwinter
3237
*/
3338
public class ScramUtils {
34-
39+
3540
public static final int DEFAULT_ITERATION_COUNT = 4096;
3641

3742
private ScramUtils() {}
3843

39-
public static byte[] createSaltedPassword(byte[] salt, String password, int iters) throws SaslException {
40-
Mac mac = createSha1Hmac(password.getBytes(StandardCharsets.UTF_8));
44+
/**
45+
* Computes a salted password ({@code Hi(password, salt, iterations)} as defined in RFC 5802), using the provided
46+
* HMAC algorithm.
47+
*
48+
* @param salt the salt.
49+
* @param password the password.
50+
* @param iters the iteration count.
51+
* @param hmacAlgorithm the JCA name of the HMAC algorithm to use (for example: {@code HmacSHA1}).
52+
* @return the salted password.
53+
* @throws SaslException if the HMAC could not be initialized.
54+
*/
55+
public static byte[] createSaltedPassword(byte[] salt, String password, int iters, String hmacAlgorithm) throws SaslException
56+
{
57+
final Mac mac = createHmac(password.getBytes(StandardCharsets.UTF_8), hmacAlgorithm);
4158
mac.update(salt);
4259
mac.update(new byte[]{0, 0, 0, 1});
4360
byte[] result = mac.doFinal();
@@ -53,23 +70,85 @@ public static byte[] createSaltedPassword(byte[] salt, String password, int iter
5370

5471
return result;
5572
}
56-
57-
public static byte[] computeHmac(final byte[] key, final String string)
58-
throws SaslException {
59-
Mac mac = createSha1Hmac(key);
73+
74+
/**
75+
* Computes an HMAC over the UTF-8 bytes of the provided string, using the provided HMAC algorithm.
76+
*
77+
* @param key the key.
78+
* @param string the value to compute the HMAC over.
79+
* @param hmacAlgorithm the JCA name of the HMAC algorithm to use (for example: {@code HmacSHA1}).
80+
* @return the computed HMAC.
81+
* @throws SaslException if the HMAC could not be initialized.
82+
*/
83+
public static byte[] computeHmac(final byte[] key, final String string, final String hmacAlgorithm) throws SaslException
84+
{
85+
final Mac mac = createHmac(key, hmacAlgorithm);
6086
mac.update(string.getBytes(StandardCharsets.UTF_8));
6187
return mac.doFinal();
6288
}
6389

64-
public static Mac createSha1Hmac(final byte[] keyBytes)
65-
throws SaslException {
90+
/**
91+
* Creates an initialized {@link Mac} instance for the provided HMAC algorithm.
92+
*
93+
* @param keyBytes the key.
94+
* @param hmacAlgorithm the JCA name of the HMAC algorithm to use (for example: {@code HmacSHA1}).
95+
* @return an initialized Mac.
96+
* @throws SaslException if the HMAC could not be initialized.
97+
*/
98+
public static Mac createHmac(final byte[] keyBytes, final String hmacAlgorithm) throws SaslException
99+
{
66100
try {
67-
SecretKeySpec key = new SecretKeySpec(keyBytes, "HmacSHA1");
68-
Mac mac = Mac.getInstance("HmacSHA1");
101+
final SecretKeySpec key = new SecretKeySpec(keyBytes, hmacAlgorithm);
102+
final Mac mac = Mac.getInstance(hmacAlgorithm);
69103
mac.init(key);
70104
return mac;
71105
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
72-
throw new SaslException(e.getMessage(), e);
106+
throw new SaslException("Unable to create an initialized Mac instance for algorithm '" + hmacAlgorithm + "',", e);
73107
}
74108
}
109+
110+
/**
111+
* Computes a SHA-1 salted password ({@code Hi(password, salt, iterations)} as defined in RFC 5802).
112+
*
113+
* @param salt the salt.
114+
* @param password the password.
115+
* @param iters the iteration count.
116+
* @return the salted password.
117+
* @throws SaslException if the HMAC could not be initialized.
118+
* @deprecated Use {@link #createSaltedPassword(byte[], String, int, String)}, providing an explicit HMAC algorithm.
119+
*/
120+
@Deprecated(forRemoval = true) // Remove in or after Openfire 5.3.0
121+
public static byte[] createSaltedPassword(byte[] salt, String password, int iters) throws SaslException
122+
{
123+
return createSaltedPassword(salt, password, iters, ScramSha1SaslServer.HMAC_ALGORITHM_NAME);
124+
}
125+
126+
/**
127+
* Computes an HMAC-SHA-1 over the UTF-8 bytes of the provided string.
128+
*
129+
* @param key the key.
130+
* @param string the value to compute the HMAC over.
131+
* @return the computed HMAC.
132+
* @throws SaslException if the HMAC could not be initialized.
133+
* @deprecated Use {@link #computeHmac(byte[], String, String)}, providing an explicit HMAC algorithm.
134+
*/
135+
@Deprecated(forRemoval = true) // Remove in or after Openfire 5.3.0
136+
public static byte[] computeHmac(final byte[] key, final String string) throws SaslException
137+
{
138+
return computeHmac(key, string, ScramSha1SaslServer.HMAC_ALGORITHM_NAME);
139+
}
140+
141+
/**
142+
* Creates an initialized {@link Mac} instance for HMAC-SHA-1.
143+
*
144+
* @param keyBytes the key.
145+
* @return an initialized Mac.
146+
* @throws SaslException if the HMAC could not be initialized.
147+
* @deprecated Use {@link #createHmac(byte[], String)}, providing an explicit HMAC algorithm.
148+
*/
149+
@Deprecated(forRemoval = true) // Remove in or after Openfire 5.3.0
150+
public static Mac createSha1Hmac(final byte[] keyBytes) throws SaslException
151+
{
152+
return createHmac(keyBytes, ScramSha1SaslServer.HMAC_ALGORITHM_NAME);
153+
}
75154
}

xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapUserProvider.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.jivesoftware.openfire.XMPPServer;
2121
import org.jivesoftware.openfire.group.Group;
2222
import org.jivesoftware.openfire.group.GroupManager;
23+
import org.jivesoftware.openfire.sasl.ScramSha1SaslServer;
2324
import org.jivesoftware.openfire.user.*;
2425
import org.jivesoftware.util.JiveGlobals;
2526
import org.slf4j.Logger;
@@ -155,7 +156,7 @@ public User loadUser(String username) throws UserNotFoundException {
155156
String scheme = parts[0].trim();
156157

157158
// We only support SCRAM-SHA-1 at the moment.
158-
if ("SCRAM-SHA-1".equals(scheme)) {
159+
if (ScramSha1SaslServer.MECHANISM_NAME.equals(scheme)) {
159160
int iterations = Integer.parseInt(authInfo[0].trim());
160161
String salt = authInfo[1].trim();
161162
String storedKey = authValue[0].trim();

xmppserver/src/main/java/org/jivesoftware/openfire/muc/MUCRoom.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.jivesoftware.openfire.event.UserEventListener;
3030
import org.jivesoftware.openfire.group.*;
3131
import org.jivesoftware.openfire.muc.spi.*;
32+
import org.jivesoftware.openfire.sasl.ScramSha1SaslServer;
3233
import org.jivesoftware.openfire.user.User;
3334
import org.jivesoftware.openfire.user.UserAlreadyExistsException;
3435
import org.jivesoftware.openfire.user.UserNotFoundException;
@@ -4203,9 +4204,9 @@ public String generateOccupantId(@Nonnull final JID userJid)
42034204

42044205
try {
42054206
// Keep the room's identifier with they key, rather than with the data, which should have slightly improved security properties.
4206-
final byte[] roomKey = ScramUtils.computeHmac(siteKey.getBytes(StandardCharsets.UTF_8), this.getJID().toBareJID());
4207+
final byte[] roomKey = ScramUtils.computeHmac(siteKey.getBytes(StandardCharsets.UTF_8), this.getJID().toBareJID(), "HmacSHA1");
42074208

4208-
return StringUtils.encodeHex(ScramUtils.computeHmac(roomKey, userJid.toBareJID()));
4209+
return StringUtils.encodeHex(ScramUtils.computeHmac(roomKey, userJid.toBareJID(), "HmacSHA1"));
42094210
} catch (SaslException e) {
42104211
throw new RuntimeException("Unable to compute HMAC for user '" + userJid + "' in room '" + this.getJID() + "' while generating an Occupant ID.");
42114212
}

xmppserver/src/main/java/org/jivesoftware/openfire/net/SASLAuthentication.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.jivesoftware.openfire.sasl.Failure;
3434
import org.jivesoftware.openfire.sasl.JiveSharedSecretSaslServer;
3535
import org.jivesoftware.openfire.sasl.SaslFailureException;
36+
import org.jivesoftware.openfire.sasl.ScramSaslServer;
3637
import org.jivesoftware.openfire.sasl.ScramSha1SaslServer;
3738
import org.jivesoftware.openfire.session.*;
3839
import org.jivesoftware.openfire.spi.ConnectionType;
@@ -464,7 +465,7 @@ else if (encoded.equals("="))
464465
session.removeSessionData( "SaslServer" );
465466
session.setSessionData("SaslMechanism", saslServer.getMechanismName());
466467
if (saslServer.getMechanismName().endsWith("-PLUS")) {
467-
session.setSessionData("ChannelBindingType", saslServer.getNegotiatedProperty(ScramSha1SaslServer.PROPNAME_CHANNELBINDINGTYPE));
468+
session.setSessionData("ChannelBindingType", saslServer.getNegotiatedProperty(ScramSaslServer.PROPNAME_CHANNELBINDINGTYPE));
468469
}
469470
return Status.authenticated;
470471

@@ -672,8 +673,8 @@ public static Set<String> getSupportedMechanisms()
672673
}
673674
break;
674675

675-
case "SCRAM-SHA-1": // intended fall-through
676-
case "SCRAM-SHA-1-PLUS":
676+
case ScramSha1SaslServer.MECHANISM_NAME: // intended fall-through
677+
case ScramSha1SaslServer.MECHANISM_NAME+"-PLUS":
677678
if ( !AuthFactory.supportsScram() )
678679
{
679680
Log.trace( "Cannot support '{}' as the AuthProvider that's in use does not support SCRAM.", mechanism );
@@ -773,7 +774,7 @@ else if ( session instanceof LocalIncomingServerSession )
773774
*/
774775
public static List<String> getEnabledMechanisms()
775776
{
776-
return JiveGlobals.getListProperty("sasl.mechs", Arrays.asList( "ANONYMOUS","PLAIN","DIGEST-MD5","CRAM-MD5","SCRAM-SHA-1","SCRAM-SHA-1-PLUS","JIVE-SHAREDSECRET","GSSAPI","EXTERNAL" ) );
777+
return JiveGlobals.getListProperty("sasl.mechs", Arrays.asList( "ANONYMOUS","PLAIN","DIGEST-MD5","CRAM-MD5",ScramSha1SaslServer.MECHANISM_NAME,ScramSha1SaslServer.MECHANISM_NAME+"-PLUS","JIVE-SHAREDSECRET","GSSAPI","EXTERNAL" ) );
777778
}
778779

779780
/**

0 commit comments

Comments
 (0)