Skip to content

Commit a105c22

Browse files
committed
OF-3327: Refactor SCRAM key derivation into ScramUtils
Extract the common SCRAM key derivation logic from DefaultAuthProvider into ScramUtils, eliminating duplicated code between password verification and password storage.
1 parent 69287ec commit a105c22

3 files changed

Lines changed: 82 additions & 14 deletions

File tree

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

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package org.jivesoftware.openfire.auth;
1818

19-
import java.security.MessageDigest;
2019
import java.security.NoSuchAlgorithmException;
2120
import java.security.SecureRandom;
2221
import java.sql.Connection;
@@ -345,17 +344,16 @@ public boolean checkPassword(String username, String testPassword) throws UserNo
345344
Log.debug("No available SCRAM-SHA-1 credentials for checkPassword for user {}", username);
346345
return false;
347346
}
348-
final byte[] testStoredKey;
349347
try {
350348
final byte[] saltShaker = DatatypeConverter.parseBase64Binary(credential.salt);
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);
349+
final ScramUtils.ScramKeys keys = ScramUtils.deriveScramKeys(
350+
saltShaker, testPassword, credential.iterations,
351+
ScramSha1SaslServer.HMAC_ALGORITHM_NAME, ScramSha1SaslServer.DIGEST_ALGORITHM_NAME);
352+
return DatatypeConverter.printBase64Binary(keys.storedKey).equals(credential.storedKey);
354353
} catch(SaslException | NoSuchAlgorithmException | IllegalArgumentException e) {
355354
Log.warn("Unable to check SCRAM values for PLAIN authentication for user '{}'", username, e);
356355
return false;
357356
}
358-
return DatatypeConverter.printBase64Binary(testStoredKey).equals(credential.storedKey);
359357
}
360358
catch (SQLException sqle) {
361359
Log.warn("A database error occurred while authenticating user {}:", username, sqle);
@@ -388,15 +386,15 @@ public void setPassword(String username, String password) throws UserNotFoundExc
388386
byte[] saltShaker = new byte[SALT_LENGTH];
389387
random.nextBytes(saltShaker);
390388
final String salt = DatatypeConverter.printBase64Binary(saltShaker);
389+
ScramUtils.ScramKeys keys = null;
391390
final int iterations = ScramSha1SaslServer.ITERATION_COUNT.getValue();
392-
byte[] saltedPassword = null, clientKey = null, storedKey = null, serverKey = null;
393391
try {
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);
392+
keys = ScramUtils.deriveScramKeys(
393+
saltShaker, password, iterations,
394+
ScramSha1SaslServer.HMAC_ALGORITHM_NAME,
395+
ScramSha1SaslServer.DIGEST_ALGORITHM_NAME);
398396
} catch (SaslException | NoSuchAlgorithmException e) {
399-
Log.warn("Unable to persist values for SCRAM authentication.");
397+
Log.warn("Unable to derive SCRAM credential while trying to persist values for SCRAM authentication", e);
400398
}
401399

402400
String plaintextToStore = password;
@@ -453,8 +451,8 @@ public void setPassword(String username, String password) throws UserNotFoundExc
453451
ScramSha1SaslServer.MECHANISM_NAME,
454452
salt,
455453
iterations,
456-
storedKey == null ? null : DatatypeConverter.printBase64Binary(storedKey),
457-
serverKey == null ? null : DatatypeConverter.printBase64Binary(serverKey)
454+
keys == null ? null : DatatypeConverter.printBase64Binary(keys.storedKey),
455+
keys == null ? null : DatatypeConverter.printBase64Binary(keys.serverKey)
458456
)
459457
);
460458
}

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import java.nio.charset.StandardCharsets;
2222
import java.security.InvalidKeyException;
23+
import java.security.MessageDigest;
2324
import java.security.NoSuchAlgorithmException;
2425

2526
import javax.crypto.Mac;
@@ -41,6 +42,32 @@ public class ScramUtils {
4142

4243
private ScramUtils() {}
4344

45+
/**
46+
* Derives the stored key and server key for a SCRAM credential.
47+
*
48+
* The keys are derived from the supplied password using the specified salt, iteration count, HMAC algorithm and
49+
* digest algorithm, as defined by the SCRAM specification.
50+
*
51+
* @param salt the salt.
52+
* @param password the plaintext password.
53+
* @param iterations the SCRAM iteration count.
54+
* @param hmacAlgorithm the HMAC algorithm (for example {@code HmacSHA1} or {@code HmacSHA256}).
55+
* @param digestAlgorithm the digest algorithm corresponding to the HMAC algorithm (for example {@code SHA-1} or {@code SHA-256}).
56+
* @return the derived stored key and server key.
57+
* @throws SaslException if the salted password or HMAC values cannot be derived.
58+
* @throws NoSuchAlgorithmException if the requested digest algorithm is unavailable.
59+
*/
60+
public static ScramKeys deriveScramKeys(final byte[] salt, final String password, final int iterations, final String hmacAlgorithm, final String digestAlgorithm) throws SaslException, NoSuchAlgorithmException
61+
{
62+
final byte[] saltedPassword = createSaltedPassword(salt, password, iterations, hmacAlgorithm);
63+
final byte[] clientKey = computeHmac(saltedPassword, "Client Key", hmacAlgorithm);
64+
final byte[] storedKey = MessageDigest.getInstance(digestAlgorithm).digest(clientKey);
65+
66+
final byte[] serverKey = computeHmac(saltedPassword, "Server Key", hmacAlgorithm);
67+
68+
return new ScramKeys(storedKey, serverKey);
69+
}
70+
4471
/**
4572
* Computes a salted password ({@code Hi(password, salt, iterations)} as defined in RFC 5802), using the provided
4673
* HMAC algorithm.
@@ -151,4 +178,23 @@ public static Mac createSha1Hmac(final byte[] keyBytes) throws SaslException
151178
{
152179
return createHmac(keyBytes, ScramSha1SaslServer.HMAC_ALGORITHM_NAME);
153180
}
181+
182+
/**
183+
* The derived SCRAM keys for a password.
184+
*
185+
* These keys are derived from a password, salt and iteration count as defined by the SCRAM specification. The
186+
* stored key is persisted and used to verify client authentication, while the server key is used by the server when
187+
* constructing SCRAM protocol messages.
188+
*/
189+
public static final class ScramKeys
190+
{
191+
public final byte[] storedKey;
192+
public final byte[] serverKey;
193+
194+
private ScramKeys(byte[] storedKey, byte[] serverKey)
195+
{
196+
this.storedKey = storedKey;
197+
this.serverKey = serverKey;
198+
}
199+
}
154200
}

xmppserver/src/test/java/org/jivesoftware/openfire/auth/ScramUtilsTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,4 +165,28 @@ public void testScramSha1ComputeHmac2() throws Exception
165165
// Verify results.
166166
assertArrayEquals(StringUtils.decodeHex("0fe09258b3ac852ba502cc62ba903eaacdbf7d31"), result); // test against 'server key' from the test vectors.
167167
}
168+
169+
/**
170+
* Verifies the implementation of {@link ScramUtils#deriveScramKeys(byte[], String, int, String, String)}
171+
* by using SCRAM-SHA-1(-PLUS) test vectors that are provided by the XSF.
172+
*
173+
* @see <a href="https://wiki.xmpp.org/web/SASL_Authentication_and_SCRAM">XSF SCRAM test vectors.</a>
174+
*/
175+
@Test
176+
public void testScramSha1DeriveScramKeys() throws Exception
177+
{
178+
// Setup test fixture.
179+
final byte[] salt = StringUtils.decodeHex("4125c247e43ab1e93c6dff76");
180+
final String password = "pencil";
181+
final int iterations = 4096;
182+
final String hmacAlgorithm = "HmacSHA1";
183+
final String digestAlgorithm = "SHA-1";
184+
185+
// Execute system under test.
186+
final ScramUtils.ScramKeys result = ScramUtils.deriveScramKeys(salt, password, iterations, hmacAlgorithm, digestAlgorithm);
187+
188+
// Verify results.
189+
assertArrayEquals(StringUtils.decodeHex("e9d94660c39d65c38fbad91c358f14da0eef2bd6"), result.storedKey);
190+
assertArrayEquals(StringUtils.decodeHex("0fe09258b3ac852ba502cc62ba903eaacdbf7d31"), result.serverKey);
191+
}
168192
}

0 commit comments

Comments
 (0)