
org.postgresql.adba.util.scram.client.ScramSession Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of pgadba Show documentation
Show all versions of pgadba Show documentation
ADBA implementation for PostgreSQL
The newest version!
/*
* Copyright 2017, OnGres.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
* following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
* disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
*/
package org.postgresql.adba.util.scram.client;
import static org.postgresql.adba.util.scram.common.util.Preconditions.checkNotEmpty;
import static org.postgresql.adba.util.scram.common.util.Preconditions.checkNotNull;
import java.util.Base64;
import java.util.Optional;
import org.postgresql.adba.util.scram.common.ScramFunctions;
import org.postgresql.adba.util.scram.common.ScramMechanism;
import org.postgresql.adba.util.scram.common.exception.ScramInvalidServerSignatureException;
import org.postgresql.adba.util.scram.common.exception.ScramParseException;
import org.postgresql.adba.util.scram.common.exception.ScramServerErrorException;
import org.postgresql.adba.util.scram.common.gssapi.Gs2CbindFlag;
import org.postgresql.adba.util.scram.common.message.ClientFinalMessage;
import org.postgresql.adba.util.scram.common.message.ClientFirstMessage;
import org.postgresql.adba.util.scram.common.message.ServerFinalMessage;
import org.postgresql.adba.util.scram.common.message.ServerFirstMessage;
import org.postgresql.adba.util.scram.common.stringprep.StringPreparation;
/**
* A class that represents a SCRAM client. Use this class to perform a SCRAM negotiation with a SCRAM server. This class performs
* an authentication execution for a given user, and has state related to it. Thus, it cannot be shared across users or
* authentication executions.
*/
public class ScramSession {
private final ScramMechanism scramMechanism;
private final StringPreparation stringPreparation;
private final String user;
private final String nonce;
private ClientFirstMessage clientFirstMessage;
private String serverFirstMessageString;
/**
* Constructs a SCRAM client, to perform an authentication for a given user. This class can be instantiated directly, but it is
* recommended that a {@link ScramClient} is used instead.
*
* @param scramMechanism The SCRAM mechanism that will be using this client
* @param stringPreparation a string normalizer object
* @param user the user name to use
* @param nonce the nonce
*/
public ScramSession(ScramMechanism scramMechanism, StringPreparation stringPreparation, String user, String nonce) {
this.scramMechanism = checkNotNull(scramMechanism, "scramMechanism");
this.stringPreparation = checkNotNull(stringPreparation, "stringPreparation");
this.user = checkNotEmpty(user, "user");
this.nonce = checkNotEmpty(nonce, "nonce");
}
private String setAndReturnClientFirstMessage(ClientFirstMessage clientFirstMessage) {
this.clientFirstMessage = clientFirstMessage;
return clientFirstMessage.toString();
}
/**
* Returns the text representation of a SCRAM client-first-message, with the GSS-API header values indicated.
*
* @param gs2CbindFlag The channel binding flag
* @param cbindName The channel binding algorithm name, if channel binding is supported, or null
* @param authzid The optional
* @return The message
*/
public String clientFirstMessage(Gs2CbindFlag gs2CbindFlag, String cbindName, String authzid) {
return setAndReturnClientFirstMessage(new ClientFirstMessage(gs2CbindFlag, authzid, cbindName, user, nonce));
}
/**
* Returns the text representation of a SCRAM client-first-message, with no channel binding nor authzid.
*
* @return The message
*/
public String clientFirstMessage() {
return setAndReturnClientFirstMessage(new ClientFirstMessage(user, nonce));
}
/**
* Process a received server-first-message. Generate by calling {@link #receiveServerFirstMessage(String)}.
*/
public class ServerFirstProcessor {
private final ServerFirstMessage serverFirstMessage;
private ServerFirstProcessor(String receivedServerFirstMessage) throws ScramParseException {
serverFirstMessageString = receivedServerFirstMessage;
serverFirstMessage = ServerFirstMessage.parseFrom(receivedServerFirstMessage, nonce);
}
public String getSalt() {
return serverFirstMessage.getSalt();
}
public int getIteration() {
return serverFirstMessage.getIteration();
}
/**
* Generates a {@link ClientFinalProcessor}, that allows to generate the client-final-message and also receive and parse the
* server-first-message. It is based on the user's password.
*
* @param password The user's password
* @return The handler
* @throws IllegalArgumentException If the message is null or empty
*/
public ClientFinalProcessor clientFinalProcessor(String password) throws IllegalArgumentException {
return new ClientFinalProcessor(
serverFirstMessage.getNonce(),
checkNotEmpty(password, "password"),
getSalt(),
getIteration()
);
}
/**
* Generates a {@link ClientFinalProcessor}, that allows to generate the client-final-message and also receive and parse the
* server-first-message. It is based on the clientKey and storedKey, which, if available, provide an optimized path versus
* providing the original user's password.
*
* @param clientKey The client key, as per the SCRAM algorithm. It can be generated with: {@link
* ScramFunctions#clientKey(ScramMechanism, StringPreparation, String, byte[], int)}
* @param storedKey The stored key, as per the SCRAM algorithm. It can be generated from the client key with: {@link
* ScramFunctions#storedKey(ScramMechanism, byte[])}
* @return The handler
* @throws IllegalArgumentException If the message is null or empty
*/
public ClientFinalProcessor clientFinalProcessor(byte[] clientKey, byte[] storedKey)
throws IllegalArgumentException {
return new ClientFinalProcessor(
serverFirstMessage.getNonce(),
checkNotNull(clientKey, "clientKey"),
checkNotNull(storedKey, "storedKey")
);
}
}
/**
* Processor that allows to generate the client-final-message, as well as process the server-final-message and verify server's
* signature. Generate the processor by calling either {@link ServerFirstProcessor#clientFinalProcessor(String)} or {@link
* ServerFirstProcessor#clientFinalProcessor(byte[], byte[])}.
*/
public class ClientFinalProcessor {
private final String nonce;
private final byte[] clientKey;
private final byte[] storedKey;
private final byte[] serverKey;
private String authMessage;
private ClientFinalProcessor(String nonce, byte[] clientKey, byte[] storedKey, byte[] serverKey) {
assert null != clientKey : "clientKey";
assert null != storedKey : "storedKey";
assert null != serverKey : "serverKey";
this.nonce = nonce;
this.clientKey = clientKey;
this.storedKey = storedKey;
this.serverKey = serverKey;
}
private ClientFinalProcessor(String nonce, byte[] clientKey, byte[] serverKey) {
this(nonce, clientKey, ScramFunctions.storedKey(scramMechanism, clientKey), serverKey);
}
private ClientFinalProcessor(String nonce, byte[] saltedPassword) {
this(
nonce,
ScramFunctions.clientKey(scramMechanism, saltedPassword),
ScramFunctions.serverKey(scramMechanism, saltedPassword)
);
}
private ClientFinalProcessor(String nonce, String password, String salt, int iteration) {
this(
nonce,
ScramFunctions.saltedPassword(
scramMechanism, stringPreparation, password, Base64.getDecoder().decode(salt), iteration
)
);
}
private synchronized void generateAndCacheAuthMessage(Optional cbindData) {
if (null != authMessage) {
return;
}
authMessage = clientFirstMessage.writeToWithoutGs2Header(new StringBuffer())
.append(",")
.append(serverFirstMessageString)
.append(",")
.append(ClientFinalMessage.writeToWithoutProof(clientFirstMessage.getGs2Header(), cbindData, nonce))
.toString();
}
private String clientFinalMessage(Optional cbindData) {
if (null == authMessage) {
generateAndCacheAuthMessage(cbindData);
}
ClientFinalMessage clientFinalMessage = new ClientFinalMessage(
clientFirstMessage.getGs2Header(),
cbindData,
nonce,
ScramFunctions.clientProof(
clientKey,
ScramFunctions.clientSignature(scramMechanism, storedKey, authMessage)
)
);
return clientFinalMessage.toString();
}
/**
* Generates the SCRAM representation of the client-final-message, including the given channel-binding data.
*
* @param cbindData The bytes of the channel-binding data
* @return The message
* @throws IllegalArgumentException If the channel binding data is null
*/
public String clientFinalMessage(byte[] cbindData) throws IllegalArgumentException {
return clientFinalMessage(Optional.of(checkNotNull(cbindData, "cbindData")));
}
/**
* Generates the SCRAM representation of the client-final-message.
*
* @return The message
*/
public String clientFinalMessage() {
return clientFinalMessage(Optional.empty());
}
/**
* Receive and process the server-final-message. Server SCRAM signatures is verified.
*
* @param serverFinalMessage The received server-final-message
* @throws ScramParseException If the message is not a valid server-final-message
* @throws ScramServerErrorException If the server-final-message contained an error
* @throws ScramInvalidServerSignatureException if the server signature can't be verified
* @throws IllegalArgumentException If the message is null or empty
*/
public void receiveServerFinalMessage(String serverFinalMessage)
throws ScramParseException, ScramServerErrorException, ScramInvalidServerSignatureException,
IllegalArgumentException {
checkNotEmpty(serverFinalMessage, "serverFinalMessage");
ServerFinalMessage message = ServerFinalMessage.parseFrom(serverFinalMessage);
if (message.isError()) {
throw new ScramServerErrorException(message.getError().get());
}
if (!ScramFunctions.verifyServerSignature(
scramMechanism, serverKey, authMessage, message.getVerifier().get()
)) {
throw new ScramInvalidServerSignatureException("Invalid server SCRAM signature");
}
}
}
/**
* Constructs a handler for the server-first-message, from its String representation.
*
* @param serverFirstMessage The message
* @return The handler
* @throws ScramParseException If the message is not a valid server-first-message
* @throws IllegalArgumentException If the message is null or empty
*/
public ServerFirstProcessor receiveServerFirstMessage(String serverFirstMessage)
throws ScramParseException, IllegalArgumentException {
return new ServerFirstProcessor(checkNotEmpty(serverFirstMessage, "serverFirstMessage"));
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy