blob: 2a22ff4ac070b4daffcee94a35caa8945604a1ae [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.kafka.common.security.scram.internals;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.kafka.common.KafkaException;
import org.apache.kafka.common.security.scram.ScramCredential;
import org.apache.kafka.common.security.scram.internals.ScramMessages.ClientFinalMessage;
import org.apache.kafka.common.security.scram.internals.ScramMessages.ClientFirstMessage;
import org.apache.kafka.common.security.scram.internals.ScramMessages.ServerFirstMessage;
/**
* Scram message salt and hash functions defined in <a href="https://tools.ietf.org/html/rfc5802">RFC 5802</a>.
*/
public class ScramFormatter {
private static final Pattern EQUAL = Pattern.compile("=", Pattern.LITERAL);
private static final Pattern COMMA = Pattern.compile(",", Pattern.LITERAL);
private static final Pattern EQUAL_TWO_C = Pattern.compile("=2C", Pattern.LITERAL);
private static final Pattern EQUAL_THREE_D = Pattern.compile("=3D", Pattern.LITERAL);
private final MessageDigest messageDigest;
private final Mac mac;
private final SecureRandom random;
public ScramFormatter(ScramMechanism mechanism) throws NoSuchAlgorithmException {
this.messageDigest = MessageDigest.getInstance(mechanism.hashAlgorithm());
this.mac = Mac.getInstance(mechanism.macAlgorithm());
this.random = new SecureRandom();
}
public byte[] hmac(byte[] key, byte[] bytes) throws InvalidKeyException {
mac.init(new SecretKeySpec(key, mac.getAlgorithm()));
return mac.doFinal(bytes);
}
public byte[] hash(byte[] str) {
return messageDigest.digest(str);
}
public byte[] xor(byte[] first, byte[] second) {
if (first.length != second.length)
throw new IllegalArgumentException("Argument arrays must be of the same length");
byte[] result = new byte[first.length];
for (int i = 0; i < result.length; i++)
result[i] = (byte) (first[i] ^ second[i]);
return result;
}
public byte[] hi(byte[] str, byte[] salt, int iterations) throws InvalidKeyException {
mac.init(new SecretKeySpec(str, mac.getAlgorithm()));
mac.update(salt);
byte[] u1 = mac.doFinal(new byte[]{0, 0, 0, 1});
byte[] prev = u1;
byte[] result = u1;
for (int i = 2; i <= iterations; i++) {
byte[] ui = hmac(str, prev);
result = xor(result, ui);
prev = ui;
}
return result;
}
public byte[] normalize(String str) {
return toBytes(str);
}
public byte[] saltedPassword(String password, byte[] salt, int iterations) throws InvalidKeyException {
return hi(normalize(password), salt, iterations);
}
public byte[] clientKey(byte[] saltedPassword) throws InvalidKeyException {
return hmac(saltedPassword, toBytes("Client Key"));
}
public byte[] storedKey(byte[] clientKey) {
return hash(clientKey);
}
public String saslName(String username) {
String replace1 = EQUAL.matcher(username).replaceAll(Matcher.quoteReplacement("=3D"));
return COMMA.matcher(replace1).replaceAll(Matcher.quoteReplacement("=2C"));
}
public String username(String saslName) {
String username = EQUAL_TWO_C.matcher(saslName).replaceAll(Matcher.quoteReplacement(","));
if (EQUAL_THREE_D.matcher(username).replaceAll(Matcher.quoteReplacement("")).indexOf('=') >= 0) {
throw new IllegalArgumentException("Invalid username: " + saslName);
}
return EQUAL_THREE_D.matcher(username).replaceAll(Matcher.quoteReplacement("="));
}
public String authMessage(String clientFirstMessageBare, String serverFirstMessage, String clientFinalMessageWithoutProof) {
return clientFirstMessageBare + "," + serverFirstMessage + "," + clientFinalMessageWithoutProof;
}
public byte[] clientSignature(byte[] storedKey, ClientFirstMessage clientFirstMessage, ServerFirstMessage serverFirstMessage, ClientFinalMessage clientFinalMessage) throws InvalidKeyException {
byte[] authMessage = authMessage(clientFirstMessage, serverFirstMessage, clientFinalMessage);
return hmac(storedKey, authMessage);
}
public byte[] clientProof(byte[] saltedPassword, ClientFirstMessage clientFirstMessage, ServerFirstMessage serverFirstMessage, ClientFinalMessage clientFinalMessage) throws InvalidKeyException {
byte[] clientKey = clientKey(saltedPassword);
byte[] storedKey = hash(clientKey);
byte[] clientSignature = hmac(storedKey, authMessage(clientFirstMessage, serverFirstMessage, clientFinalMessage));
return xor(clientKey, clientSignature);
}
private byte[] authMessage(ClientFirstMessage clientFirstMessage, ServerFirstMessage serverFirstMessage, ClientFinalMessage clientFinalMessage) {
return toBytes(authMessage(clientFirstMessage.clientFirstMessageBare(),
serverFirstMessage.toMessage(),
clientFinalMessage.clientFinalMessageWithoutProof()));
}
public byte[] storedKey(byte[] clientSignature, byte[] clientProof) {
return hash(xor(clientSignature, clientProof));
}
public byte[] serverKey(byte[] saltedPassword) throws InvalidKeyException {
return hmac(saltedPassword, toBytes("Server Key"));
}
public byte[] serverSignature(byte[] serverKey, ClientFirstMessage clientFirstMessage, ServerFirstMessage serverFirstMessage, ClientFinalMessage clientFinalMessage) throws InvalidKeyException {
byte[] authMessage = authMessage(clientFirstMessage, serverFirstMessage, clientFinalMessage);
return hmac(serverKey, authMessage);
}
public String secureRandomString() {
return new BigInteger(130, random).toString(Character.MAX_RADIX);
}
public byte[] secureRandomBytes() {
return toBytes(secureRandomString());
}
public byte[] toBytes(String str) {
return str.getBytes(StandardCharsets.UTF_8);
}
public ScramCredential generateCredential(String password, int iterations) {
try {
byte[] salt = secureRandomBytes();
byte[] saltedPassword = saltedPassword(password, salt, iterations);
byte[] clientKey = clientKey(saltedPassword);
byte[] storedKey = storedKey(clientKey);
byte[] serverKey = serverKey(saltedPassword);
return new ScramCredential(salt, storedKey, serverKey, iterations);
} catch (InvalidKeyException e) {
throw new KafkaException("Could not create credential", e);
}
}
}