From 416efaa418eb48795132f9f94d70b2a5e55a9b31 Mon Sep 17 00:00:00 2001
From: "deepin-community-bot[bot]"
<156989552+deepin-community-bot[bot]@users.noreply.github.com>
Date: Thu, 7 May 2026 12:34:14 +0000
Subject: [PATCH] feat: update libscram-java to 3.2-1
---
.editorconfig | 9 +
.gitignore | 105 ++-
.gitlab-ci.yml | 8 +-
.mvn/maven.config | 7 +
.mvn/wrapper/MavenWrapperDownloader.java | 155 ++--
.mvn/wrapper/maven-wrapper.properties | 9 +-
CHANGELOG | 33 -
CHANGELOG.md | 50 ++
LICENSE | 2 +-
NOTICE | 25 -
README.md | 116 +--
SECURITY.md | 25 +
checks/checkstyle-header.txt | 4 +
checks/checkstyle-suppressions.xml | 8 +
checks/checkstyle.xml | 310 ++++++++
checks/forbiddenapis.txt | 2 +
checks/pmd-ruleset.xml | 41 +
checks/spotbugs-exclude.xml | 39 +
client/README.md | 103 ---
client/pom.xml | 94 ---
.../ongres/scram/client/NonceSupplier.java | 27 -
.../com/ongres/scram/client/ScramClient.java | 381 ---------
.../com/ongres/scram/client/ScramSession.java | 283 -------
.../ongres/scram/client/ScramClientTest.java | 201 -----
.../ongres/scram/client/ScramSessionTest.java | 67 --
common/pom.xml | 48 --
common/spotbugs-exclude.xml | 7 -
.../scram/common/ScramAttributeValue.java | 59 --
.../ongres/scram/common/ScramAttributes.java | 158 ----
.../ongres/scram/common/ScramFunctions.java | 250 ------
.../ongres/scram/common/ScramMechanism.java | 80 --
.../ongres/scram/common/ScramMechanisms.java | 220 ------
.../scram/common/ScramStringFormatting.java | 149 ----
.../common/bouncycastle/base64/Base64.java | 196 -----
.../bouncycastle/base64/Base64Encoder.java | 399 ----------
.../bouncycastle/base64/DecoderException.java | 44 --
.../common/bouncycastle/base64/Encoder.java | 39 -
.../bouncycastle/base64/EncoderException.java | 44 --
.../common/bouncycastle/pbkdf2/Arrays.java | 75 --
.../bouncycastle/pbkdf2/CipherParameters.java | 30 -
.../pbkdf2/DataLengthException.java | 51 --
.../common/bouncycastle/pbkdf2/Digest.java | 73 --
.../bouncycastle/pbkdf2/DigestFactory.java | 34 -
.../bouncycastle/pbkdf2/EncodableDigest.java | 39 -
.../bouncycastle/pbkdf2/ExtendedDigest.java | 35 -
.../bouncycastle/pbkdf2/GeneralDigest.java | 178 -----
.../common/bouncycastle/pbkdf2/HMac.java | 245 ------
.../common/bouncycastle/pbkdf2/Integers.java | 34 -
.../bouncycastle/pbkdf2/KeyParameter.java | 50 --
.../scram/common/bouncycastle/pbkdf2/Mac.java | 93 ---
.../common/bouncycastle/pbkdf2/Memoable.java | 48 --
.../pbkdf2/PBEParametersGenerator.java | 116 ---
.../pbkdf2/PKCS5S2ParametersGenerator.java | 124 ---
.../common/bouncycastle/pbkdf2/Pack.java | 82 --
.../pbkdf2/RuntimeCryptoException.java | 48 --
.../bouncycastle/pbkdf2/SHA256Digest.java | 382 ---------
.../common/bouncycastle/pbkdf2/Strings.java | 133 ----
.../common/exception/ScramException.java | 52 --
.../ScramInvalidServerSignatureException.java | 47 --
.../common/exception/ScramParseException.java | 47 --
.../exception/ScramServerErrorException.java | 62 --
.../common/gssapi/Gs2AttributeValue.java | 61 --
.../scram/common/gssapi/Gs2Attributes.java | 83 --
.../scram/common/gssapi/Gs2CbindFlag.java | 73 --
.../ongres/scram/common/gssapi/Gs2Header.java | 132 ----
.../common/message/ClientFinalMessage.java | 136 ----
.../common/message/ClientFirstMessage.java | 200 -----
.../common/message/ServerFinalMessage.java | 219 ------
.../common/message/ServerFirstMessage.java | 161 ----
.../common/stringprep/StringPreparation.java | 38 -
.../common/stringprep/StringPreparations.java | 73 --
.../util/AbstractCharAttributeValue.java | 67 --
.../common/util/AbstractStringWritable.java | 34 -
.../scram/common/util/CharAttribute.java | 36 -
.../scram/common/util/CharAttributeValue.java | 36 -
.../ongres/scram/common/util/CryptoUtil.java | 180 -----
.../scram/common/util/Preconditions.java | 88 ---
.../scram/common/util/StringWritable.java | 37 -
.../scram/common/util/StringWritableCsv.java | 123 ---
.../scram/common/util/UsAsciiUtils.java | 53 --
.../ongres/scram/common/RfcExampleSha1.java | 49 --
.../ongres/scram/common/RfcExampleSha256.java | 49 --
.../scram/common/ScramAttributeValueTest.java | 107 ---
.../scram/common/ScramFunctionsTest.java | 255 ------
.../scram/common/ScramMechanismsTest.java | 133 ----
.../common/ScramStringFormattingTest.java | 84 --
.../common/gssapi/Gs2AttributeValueTest.java | 85 --
.../scram/common/gssapi/Gs2HeaderTest.java | 100 ---
.../message/ClientFinalMessageTest.java | 44 --
.../message/ClientFirstMessageTest.java | 137 ----
.../message/ServerFinalMessageTest.java | 73 --
.../message/ServerFirstMessageTest.java | 54 --
.../scram/common/stringprep/SaslPrepTest.java | 87 ---
.../stringprep/StringPreparationTest.java | 155 ----
.../util/AbstractCharAttributeValueTest.java | 101 ---
.../ongres/scram/common/util/Base64Test.java | 51 --
.../scram/common/util/CryptoUtilTest.java | 63 --
.../common/util/StringWritableCsvTest.java | 146 ----
.../scram/common/util/UsAsciiUtilsTest.java | 94 ---
coverage-report/pom.xml | 50 ++
debian/changelog | 24 +
debian/control | 13 +-
debian/libscram-java.poms | 5 +-
debian/maven.ignoreRules | 1 +
debian/maven.properties | 2 -
debian/patches/01-multi-release-jar.patch | 18 +
debian/patches/02-reactor-dependencies.patch | 13 +
debian/patches/series | 2 +
debian/rules | 3 -
mvnw | 363 +++++----
mvnw.cmd | 68 +-
pom.xml | 288 ++-----
scram-client/pom.xml | 52 ++
.../it/jpms-scram-client/invoker.properties | 3 +
scram-client/src/it/jpms-scram-client/pom.xml | 49 ++
.../src/test/java/module-info.java | 12 +
.../test/java/test/scram/ScramClientTest.java | 48 ++
scram-client/src/it/settings.xml | 35 +
.../scram/client/ClientFinalProcessor.java | 127 +++
.../com/ongres/scram/client/MessageFlow.java | 35 +
.../com/ongres/scram/client/ScramClient.java | 549 +++++++++++++
.../scram/client/ServerFirstProcessor.java | 103 +++
.../com/ongres/scram/client/package-info.java | 12 +
scram-client/src/main/java9/module-info.java | 9 +
.../ChannelBindingNegotiationTest.java | 95 +++
.../java/com/example/ScramClientTest.java | 111 +++
.../java/com/ongres/scram/JarFileCheckIT.java | 72 ++
.../ongres/scram/client/RfcExampleSha1.java | 35 +
.../ongres/scram/client/ScramBuilderTest.java | 125 +++
.../src/test/resources/SHA3-512withECDSA.pem | 16 +
.../src/test/resources/SHA512withRSA.pem | 54 ++
scram-common/pom.xml | 52 ++
.../common/AbstractCharAttributeValue.java | 48 ++
.../scram/common/AbstractScramMessage.java | 23 +
.../com/ongres/scram/common/CharSupplier.java | 20 +
.../scram/common/ClientFinalMessage.java | 161 ++++
.../scram/common/ClientFirstMessage.java | 197 +++++
.../com/ongres/scram/common/CryptoUtil.java | 151 ++++
.../scram/common/Gs2AttributeValue.java | 43 ++
.../ongres/scram/common/Gs2Attributes.java | 75 ++
.../com/ongres/scram/common/Gs2CbindFlag.java | 57 ++
.../com/ongres/scram/common/Gs2Header.java | 197 +++++
.../scram/common/ScramAttributeValue.java | 41 +
.../ongres/scram/common/ScramAttributes.java | 141 ++++
.../ongres/scram/common/ScramFunctions.java | 268 +++++++
.../ongres/scram/common/ScramMechanism.java | 272 +++++++
.../scram/common/ScramStringFormatting.java | 149 ++++
.../scram/common/ServerFinalMessage.java | 163 ++++
.../scram/common/ServerFirstMessage.java | 188 +++++
.../scram/common/StringPreparation.java | 87 +++
.../ongres/scram/common/StringWritable.java | 23 +
.../scram/common/StringWritableCsv.java | 115 +++
.../com/ongres/scram/common/UsAsciiUtils.java | 57 ++
.../common/exception/ScramException.java | 33 +
.../ScramInvalidServerSignatureException.java | 38 +
.../common/exception/ScramParseException.java | 33 +
.../exception/ScramRuntimeException.java | 33 +
.../exception/ScramServerErrorException.java | 54 ++
.../common/exception/ServerErrorValue.java | 53 ++
.../scram/common/exception/package-info.java | 11 +
.../com/ongres/scram/common/package-info.java | 13 +
.../scram/common/util/Preconditions.java | 128 +++
.../scram/common/util/TlsServerEndpoint.java | 89 +++
scram-common/src/main/java9/module-info.java | 11 +
.../java/com/ongres/scram/JarFileCheckIT.java | 72 ++
.../scram/common/ClientFinalMessageTest.java | 22 +
.../scram/common/ClientFirstMessageTest.java | 125 +++
.../scram/common/Gs2AttributeValueTest.java | 77 ++
.../ongres/scram/common/Gs2HeaderTest.java | 85 ++
.../ongres/scram/common/RfcExampleSha1.java | 35 +
.../ongres/scram/common/RfcExampleSha256.java | 37 +
.../com/ongres/scram/common/SaslPrepTest.java | 42 +
.../scram/common/ScramAttributeValueTest.java | 85 ++
.../scram/common/ScramFunctionsTest.java | 270 +++++++
.../scram/common/ScramMechanismTest.java | 89 +++
.../common/ScramStringFormattingTest.java | 67 ++
.../scram/common/ServerFinalMessageTest.java | 63 ++
.../scram/common/ServerFirstMessageTest.java | 34 +
.../scram/common/StringPreparationTest.java | 145 ++++
.../scram/common/StringWritableCsvTest.java | 117 +++
.../ongres/scram/common/UsAsciiUtilsTest.java | 59 ++
.../ongres/scram/common/util/NonceTest.java | 48 ++
scram-parent/javadoc/style.css | 40 +
scram-parent/pom.xml | 728 ++++++++++++++++++
thirdparty/bouncycastle-LICENSE | 7 -
thirdparty/passlib_lambda-LICENSE | 22 -
thirdparty/saslprep-LICENSE | 22 -
187 files changed, 7952 insertions(+), 9327 deletions(-)
create mode 100644 .editorconfig
create mode 100644 .mvn/maven.config
delete mode 100644 CHANGELOG
create mode 100644 CHANGELOG.md
delete mode 100644 NOTICE
create mode 100644 SECURITY.md
create mode 100644 checks/checkstyle-header.txt
create mode 100644 checks/checkstyle-suppressions.xml
create mode 100644 checks/checkstyle.xml
create mode 100644 checks/forbiddenapis.txt
create mode 100644 checks/pmd-ruleset.xml
create mode 100644 checks/spotbugs-exclude.xml
delete mode 100644 client/README.md
delete mode 100644 client/pom.xml
delete mode 100644 client/src/main/java/com/ongres/scram/client/NonceSupplier.java
delete mode 100644 client/src/main/java/com/ongres/scram/client/ScramClient.java
delete mode 100644 client/src/main/java/com/ongres/scram/client/ScramSession.java
delete mode 100644 client/src/test/java/com/ongres/scram/client/ScramClientTest.java
delete mode 100644 client/src/test/java/com/ongres/scram/client/ScramSessionTest.java
delete mode 100644 common/pom.xml
delete mode 100644 common/spotbugs-exclude.xml
delete mode 100644 common/src/main/java/com/ongres/scram/common/ScramAttributeValue.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/ScramAttributes.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/ScramFunctions.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/ScramMechanism.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/ScramMechanisms.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/ScramStringFormatting.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/bouncycastle/base64/Base64.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/bouncycastle/base64/Base64Encoder.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/bouncycastle/base64/DecoderException.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/bouncycastle/base64/Encoder.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/bouncycastle/base64/EncoderException.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/Arrays.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/CipherParameters.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/DataLengthException.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/Digest.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/DigestFactory.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/EncodableDigest.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/ExtendedDigest.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/GeneralDigest.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/HMac.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/Integers.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/KeyParameter.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/Mac.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/Memoable.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/PBEParametersGenerator.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/PKCS5S2ParametersGenerator.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/Pack.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/RuntimeCryptoException.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/SHA256Digest.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/Strings.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/exception/ScramException.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/exception/ScramInvalidServerSignatureException.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/exception/ScramParseException.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/exception/ScramServerErrorException.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/gssapi/Gs2AttributeValue.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/gssapi/Gs2Attributes.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/gssapi/Gs2CbindFlag.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/gssapi/Gs2Header.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/message/ClientFinalMessage.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/message/ClientFirstMessage.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/message/ServerFinalMessage.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/message/ServerFirstMessage.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/stringprep/StringPreparation.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/stringprep/StringPreparations.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/util/AbstractCharAttributeValue.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/util/AbstractStringWritable.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/util/CharAttribute.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/util/CharAttributeValue.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/util/CryptoUtil.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/util/Preconditions.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/util/StringWritable.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/util/StringWritableCsv.java
delete mode 100644 common/src/main/java/com/ongres/scram/common/util/UsAsciiUtils.java
delete mode 100644 common/src/test/java/com/ongres/scram/common/RfcExampleSha1.java
delete mode 100644 common/src/test/java/com/ongres/scram/common/RfcExampleSha256.java
delete mode 100644 common/src/test/java/com/ongres/scram/common/ScramAttributeValueTest.java
delete mode 100644 common/src/test/java/com/ongres/scram/common/ScramFunctionsTest.java
delete mode 100644 common/src/test/java/com/ongres/scram/common/ScramMechanismsTest.java
delete mode 100644 common/src/test/java/com/ongres/scram/common/ScramStringFormattingTest.java
delete mode 100644 common/src/test/java/com/ongres/scram/common/gssapi/Gs2AttributeValueTest.java
delete mode 100644 common/src/test/java/com/ongres/scram/common/gssapi/Gs2HeaderTest.java
delete mode 100644 common/src/test/java/com/ongres/scram/common/message/ClientFinalMessageTest.java
delete mode 100644 common/src/test/java/com/ongres/scram/common/message/ClientFirstMessageTest.java
delete mode 100644 common/src/test/java/com/ongres/scram/common/message/ServerFinalMessageTest.java
delete mode 100644 common/src/test/java/com/ongres/scram/common/message/ServerFirstMessageTest.java
delete mode 100644 common/src/test/java/com/ongres/scram/common/stringprep/SaslPrepTest.java
delete mode 100644 common/src/test/java/com/ongres/scram/common/stringprep/StringPreparationTest.java
delete mode 100644 common/src/test/java/com/ongres/scram/common/util/AbstractCharAttributeValueTest.java
delete mode 100644 common/src/test/java/com/ongres/scram/common/util/Base64Test.java
delete mode 100644 common/src/test/java/com/ongres/scram/common/util/CryptoUtilTest.java
delete mode 100644 common/src/test/java/com/ongres/scram/common/util/StringWritableCsvTest.java
delete mode 100644 common/src/test/java/com/ongres/scram/common/util/UsAsciiUtilsTest.java
create mode 100644 coverage-report/pom.xml
create mode 100644 debian/patches/01-multi-release-jar.patch
create mode 100644 debian/patches/02-reactor-dependencies.patch
create mode 100644 debian/patches/series
create mode 100644 scram-client/pom.xml
create mode 100644 scram-client/src/it/jpms-scram-client/invoker.properties
create mode 100644 scram-client/src/it/jpms-scram-client/pom.xml
create mode 100644 scram-client/src/it/jpms-scram-client/src/test/java/module-info.java
create mode 100644 scram-client/src/it/jpms-scram-client/src/test/java/test/scram/ScramClientTest.java
create mode 100644 scram-client/src/it/settings.xml
create mode 100644 scram-client/src/main/java/com/ongres/scram/client/ClientFinalProcessor.java
create mode 100644 scram-client/src/main/java/com/ongres/scram/client/MessageFlow.java
create mode 100644 scram-client/src/main/java/com/ongres/scram/client/ScramClient.java
create mode 100644 scram-client/src/main/java/com/ongres/scram/client/ServerFirstProcessor.java
create mode 100644 scram-client/src/main/java/com/ongres/scram/client/package-info.java
create mode 100644 scram-client/src/main/java9/module-info.java
create mode 100644 scram-client/src/test/java/com/example/ChannelBindingNegotiationTest.java
create mode 100644 scram-client/src/test/java/com/example/ScramClientTest.java
create mode 100644 scram-client/src/test/java/com/ongres/scram/JarFileCheckIT.java
create mode 100644 scram-client/src/test/java/com/ongres/scram/client/RfcExampleSha1.java
create mode 100644 scram-client/src/test/java/com/ongres/scram/client/ScramBuilderTest.java
create mode 100644 scram-client/src/test/resources/SHA3-512withECDSA.pem
create mode 100644 scram-client/src/test/resources/SHA512withRSA.pem
create mode 100644 scram-common/pom.xml
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/AbstractCharAttributeValue.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/AbstractScramMessage.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/CharSupplier.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/ClientFinalMessage.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/ClientFirstMessage.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/CryptoUtil.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/Gs2AttributeValue.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/Gs2Attributes.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/Gs2CbindFlag.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/Gs2Header.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/ScramAttributeValue.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/ScramAttributes.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/ScramFunctions.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/ScramMechanism.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/ScramStringFormatting.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/ServerFinalMessage.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/ServerFirstMessage.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/StringPreparation.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/StringWritable.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/StringWritableCsv.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/UsAsciiUtils.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/exception/ScramException.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/exception/ScramInvalidServerSignatureException.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/exception/ScramParseException.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/exception/ScramRuntimeException.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/exception/ScramServerErrorException.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/exception/ServerErrorValue.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/exception/package-info.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/package-info.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/util/Preconditions.java
create mode 100644 scram-common/src/main/java/com/ongres/scram/common/util/TlsServerEndpoint.java
create mode 100644 scram-common/src/main/java9/module-info.java
create mode 100644 scram-common/src/test/java/com/ongres/scram/JarFileCheckIT.java
create mode 100644 scram-common/src/test/java/com/ongres/scram/common/ClientFinalMessageTest.java
create mode 100644 scram-common/src/test/java/com/ongres/scram/common/ClientFirstMessageTest.java
create mode 100644 scram-common/src/test/java/com/ongres/scram/common/Gs2AttributeValueTest.java
create mode 100644 scram-common/src/test/java/com/ongres/scram/common/Gs2HeaderTest.java
create mode 100644 scram-common/src/test/java/com/ongres/scram/common/RfcExampleSha1.java
create mode 100644 scram-common/src/test/java/com/ongres/scram/common/RfcExampleSha256.java
create mode 100644 scram-common/src/test/java/com/ongres/scram/common/SaslPrepTest.java
create mode 100644 scram-common/src/test/java/com/ongres/scram/common/ScramAttributeValueTest.java
create mode 100644 scram-common/src/test/java/com/ongres/scram/common/ScramFunctionsTest.java
create mode 100644 scram-common/src/test/java/com/ongres/scram/common/ScramMechanismTest.java
create mode 100644 scram-common/src/test/java/com/ongres/scram/common/ScramStringFormattingTest.java
create mode 100644 scram-common/src/test/java/com/ongres/scram/common/ServerFinalMessageTest.java
create mode 100644 scram-common/src/test/java/com/ongres/scram/common/ServerFirstMessageTest.java
create mode 100644 scram-common/src/test/java/com/ongres/scram/common/StringPreparationTest.java
create mode 100644 scram-common/src/test/java/com/ongres/scram/common/StringWritableCsvTest.java
create mode 100644 scram-common/src/test/java/com/ongres/scram/common/UsAsciiUtilsTest.java
create mode 100644 scram-common/src/test/java/com/ongres/scram/common/util/NonceTest.java
create mode 100644 scram-parent/javadoc/style.css
create mode 100644 scram-parent/pom.xml
delete mode 100644 thirdparty/bouncycastle-LICENSE
delete mode 100644 thirdparty/passlib_lambda-LICENSE
delete mode 100644 thirdparty/saslprep-LICENSE
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..fff7c5f
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,9 @@
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+trim_trailing_whitespace = true
+end_of_line = lf
+insert_final_newline = true
diff --git a/.gitignore b/.gitignore
index 224e7f0..ab5ee6d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,104 @@
-.pc/
+### Eclipse ###
+.metadata
+bin/
+tmp/
+*.tmp
+*.bak
+*.swp
+*~.nib
+local.properties
+.settings/
+.loadpath
+.recommenders
+.pmd
+
+# External tool builders
+.externalToolBuilders/
+
+# Locally stored "Eclipse launch configurations"
+*.launch
+
+# PyDev specific (Python IDE for Eclipse)
+*.pydevproject
+
+# CDT-specific (C/C++ Development Tooling)
+.cproject
+
+# CDT- autotools
+.autotools
+
+# Java annotation processor (APT)
+.factorypath
+
+# PDT-specific (PHP Development Tools)
+.buildpath
+
+# sbteclipse plugin
+.target
+
+# Tern plugin
+.tern-project
+
+# TeXlipse plugin
+.texlipse
+
+# STS (Spring Tool Suite)
+.springBeans
+
+# Code Recommenders
+.recommenders/
+
+# Annotation Processing
+.apt_generated/
+
+# Scala IDE specific (Scala & Java development for Eclipse)
+.cache-main
+.scala_dependencies
+.worksheet
+
+### Eclipse Patch ###
+# Eclipse Core
+.project
+
+# JDT-specific (Eclipse Java Development Tools)
+.classpath
+
+# Annotation Processing
+.apt_generated
+
+.sts4-cache/
+
+# VSCode
+.vscode/
+
+### Java ###
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+/target
+/*/target
+
+.flattened-pom.xml
+
+*pom.xml.versionsBackup
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 7738d4e..c33cb1d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,4 @@
-image: openjdk:8-stretch
+image: eclipse-temurin:21-jdk
stages:
- build
@@ -13,8 +13,4 @@ build:
paths:
- .m2/
script:
- - bash mvnw clean verify -P safer
-
-build-oracle-jdk-7:
- extends: build
- image: registry.gitlab.com/ongresinc/scram/oracle-jdk:7
+ - ./mvnw clean verify
diff --git a/.mvn/maven.config b/.mvn/maven.config
new file mode 100644
index 0000000..b080f98
--- /dev/null
+++ b/.mvn/maven.config
@@ -0,0 +1,7 @@
+--strict-checksums
+--show-version
+--errors
+--fail-fast
+-DinstallAtEnd=true
+-DdeployAtEnd=true
+-DrootDirectory=${session.rootDirectory}
diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java
index 066726d..7e9c1e3 100644
--- a/.mvn/wrapper/MavenWrapperDownloader.java
+++ b/.mvn/wrapper/MavenWrapperDownloader.java
@@ -1,100 +1,69 @@
/*
- * Copyright 2007-present the original author or authors.
+ * 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
*
- * 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
*
- * 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.
+ * 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.
*/
-import java.net.*;
-import java.io.*;
-import java.nio.channels.*;
-import java.util.Properties;
-
-public class MavenWrapperDownloader {
- private static final String WRAPPER_VERSION = "0.5.5";
- /**
- * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
- */
- private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
- + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.Authenticator;
+import java.net.PasswordAuthentication;
+import java.net.URI;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.util.concurrent.ThreadLocalRandom;
- /**
- * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
- * use instead of the default one.
- */
- private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
- ".mvn/wrapper/maven-wrapper.properties";
+public final class MavenWrapperDownloader {
+ private static final String WRAPPER_VERSION = "3.3.4";
- /**
- * Path where the maven-wrapper.jar will be saved to.
- */
- private static final String MAVEN_WRAPPER_JAR_PATH =
- ".mvn/wrapper/maven-wrapper.jar";
+ private static final boolean VERBOSE = Boolean.parseBoolean(System.getenv("MVNW_VERBOSE"));
- /**
- * Name of the property which should be used to override the default download url for the wrapper.
- */
- private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
+ public static void main(String[] args) {
+ log("Apache Maven Wrapper Downloader " + WRAPPER_VERSION);
- public static void main(String args[]) {
- System.out.println("- Downloader started");
- File baseDirectory = new File(args[0]);
- System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
-
- // If the maven-wrapper.properties exists, read it and check if it contains a custom
- // wrapperUrl parameter.
- File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
- String url = DEFAULT_DOWNLOAD_URL;
- if(mavenWrapperPropertyFile.exists()) {
- FileInputStream mavenWrapperPropertyFileInputStream = null;
- try {
- mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
- Properties mavenWrapperProperties = new Properties();
- mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
- url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
- } catch (IOException e) {
- System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
- } finally {
- try {
- if(mavenWrapperPropertyFileInputStream != null) {
- mavenWrapperPropertyFileInputStream.close();
- }
- } catch (IOException e) {
- // Ignore ...
- }
- }
+ if (args.length != 2) {
+ System.err.println(" - ERROR wrapperUrl or wrapperJarPath parameter missing");
+ System.exit(1);
}
- System.out.println("- Downloading from: " + url);
- File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
- if(!outputFile.getParentFile().exists()) {
- if(!outputFile.getParentFile().mkdirs()) {
- System.out.println(
- "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
- }
- }
- System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
try {
- downloadFileFromURL(url, outputFile);
- System.out.println("Done");
- System.exit(0);
- } catch (Throwable e) {
- System.out.println("- Error downloading");
- e.printStackTrace();
+ log(" - Downloader started");
+ final URL wrapperUrl = URI.create(args[0]).toURL();
+ final Path baseDir = Paths.get(".").toAbsolutePath().normalize();
+ final Path wrapperJarPath = baseDir.resolve(args[1]).normalize();
+ if (!wrapperJarPath.startsWith(baseDir)) {
+ throw new IOException("Invalid path: outside of allowed directory");
+ }
+ downloadFileFromURL(wrapperUrl, wrapperJarPath);
+ log("Done");
+ } catch (IOException e) {
+ System.err.println("- Error downloading: " + e.getMessage());
+ if (VERBOSE) {
+ e.printStackTrace();
+ }
System.exit(1);
}
}
- private static void downloadFileFromURL(String urlString, File destination) throws Exception {
+ private static void downloadFileFromURL(URL wrapperUrl, Path wrapperJarPath)
+ throws IOException {
+ log(" - Downloading to: " + wrapperJarPath);
if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
final String username = System.getenv("MVNW_USERNAME");
final char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
@@ -105,13 +74,23 @@ protected PasswordAuthentication getPasswordAuthentication() {
}
});
}
- URL website = new URL(urlString);
- ReadableByteChannel rbc;
- rbc = Channels.newChannel(website.openStream());
- FileOutputStream fos = new FileOutputStream(destination);
- fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
- fos.close();
- rbc.close();
+ Path temp = wrapperJarPath
+ .getParent()
+ .resolve(wrapperJarPath.getFileName() + "."
+ + Long.toUnsignedString(ThreadLocalRandom.current().nextLong()) + ".tmp");
+ try (InputStream inStream = wrapperUrl.openStream()) {
+ Files.copy(inStream, temp, StandardCopyOption.REPLACE_EXISTING);
+ Files.move(temp, wrapperJarPath, StandardCopyOption.REPLACE_EXISTING);
+ } finally {
+ Files.deleteIfExists(temp);
+ }
+ log(" - Downloader complete");
+ }
+
+ private static void log(String msg) {
+ if (VERBOSE) {
+ System.out.println(msg);
+ }
}
}
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
index fa87ad7..13e2391 100644
--- a/.mvn/wrapper/maven-wrapper.properties
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -1,2 +1,7 @@
-distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.1/apache-maven-3.6.1-bin.zip
-wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar
+wrapperVersion=3.3.4
+distributionType=source
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
+distributionSha256Sum=0d7125e8c91097b36edb990ea5934e6c68b4440eef4ea96510a0f6815e7eeadb
+wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar
+wrapperSha256Sum=4e2fbf6554bc8a4702cdfdd3bef464f423393d784ddbb037216320ce55d5e4e1
+alwaysUnpack=true
diff --git a/CHANGELOG b/CHANGELOG
deleted file mode 100644
index 930bd39..0000000
--- a/CHANGELOG
+++ /dev/null
@@ -1,33 +0,0 @@
-# 2.1
-
-* Updated saslprep to version 1.1 to remove a build dependency coming from stringprep module
-
-# 2.0
-
-* Out of beta testing
-
-# 2.0-beta3
-
-* Fixed licenses issues
-
-# 2.0-beta2
-
-* Added saslprep tests
-
-# 2.0-beta1
-
-* Add new dependency StringPrep
-
-# 1.9-beta1
-
-* API change to be compatible with Java 7
-* Added standard SASLPrep
-* Failover to bouncy castle implementation of PBKDF2WithHmacSHA256 to support Oracle JDK 7
-
-# 1.0.0-beta.2
-
-* Fix maven issue and javadoc
-
-# 1.0.0-beta.1
-
-* First version
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..5a74e5e
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,50 @@
+# Changelog
+All notable changes to this project will be documented in this file.
+
+## [Unreleased]
+
+## [3.2] - 2025-09-16
+### :lock: Security
+- Fix Timing Attack Vulnerability in SCRAM Authentication
+
+### :ghost: Maintenance
+- Updated dependencies and maven plugins.
+- Use `central-publishing-maven-plugin` to deploy to Maven Central.
+
+## [3.1] - 2024-06-26
+### :building_construction: Improvements
+- Ensure the `LICENSE` file is included in the Jar file.
+- Update of the `saslprep` dependency to 2.2.
+
+### :ghost: Maintenance
+- Added coverage report module.
+- Updated dependencies and maven plugins.
+- Remove `nexus-staging-maven-plugin`.
+
+## [3.0] - 2024-04-03
+### :boom: Breaking changes
+- :warning: Full refactor of the `scram` java implementation, this release is compatible with Java 8+, but it's incompatible with previous releases :warning:
+
+### :rocket: New features
+- Fully rewrite the `ScramClient` allowing negotiation of channel-binding properly.
+- Create Multi-release Modular JARs, the modules names are:
+ - `com.ongres.scram.common` for the common scram messages.
+ - `com.ongres.scram.client` for the scram client implementation.
+- Add `StringPreparation.POSTGRESQL_PREPARATION`, for any error in SASL preparation, it falls back to return the raw string.
+- Now the released jars are reproducible.
+- Publish CycloneDX SBOM.
+- Implementation of `tls-server-end-point` channel binding data extraction.
+
+### :building_construction: Improvements
+- Update of the `saslprep` dependency to 2.1.
+- Now the password is passed as a `char[]`.
+- Improve Javadoc documentation.
+
+### :ghost: Maintenance
+- Migrate the main repo back to GitHub.
+- Remove the shaded Bouncy Castle pbkdf2 and base64 implementation used for Java 7 support.
+
+[3.0]: https://github.com/ongres/scram/compare/2.1...3.0
+[3.1]: https://github.com/ongres/scram/compare/3.0...3.1
+[3.2]: https://github.com/ongres/scram/compare/3.1...3.2
+[Unreleased]: https://github.com/ongres/scram/compare/3.2...main
diff --git a/LICENSE b/LICENSE
index 13fdb10..6837c1e 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright 2017, OnGres.
+Copyright (c) 2017 OnGres, Inc.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
diff --git a/NOTICE b/NOTICE
deleted file mode 100644
index 168a4fd..0000000
--- a/NOTICE
+++ /dev/null
@@ -1,25 +0,0 @@
-This source code includes source code from other projects. Below the list
-of the project were the source code has been copied together with the
-path of the copied source files, their license, their notice (if any)
-and the modification that have been done.
-
-1. saslprep
-
-Dependency SaslPrep is used from project stringprep developed by various authors (https://gitlab.com/ongresinc/stringprep)
-and licensed under the BSD 2-Clause "Simplified" License (see thirdparty/saslprep-LICENSE).
-
-2. Tests of SaslPrep
-
-File common/src/test/java/com/ongres/scram/common/stringprep/ScramFunctionsTest contains tests
-from project passlib_lambda developed by davidchua (https://github.com/davidchua/passlib_lambda)
-and licensed under the MIT license (see thirdparty/passlib_lambda-LICENSE).
-The source has been modified to be included under the Java package
-com.ongres.scram.common.ScramFunctionsTest and to only include used parts.
-
-3. bouncy castle
-
-All Java source files included into folder common/src/main/java/com/ongres/scram/common/bouncycastle
-are copied from project bouncy castle developed by various authors (https://www.bouncycastle.org/)
-and licensed under the bouncy castle license (see thirdparty/bouncycastle-LICENSE).
-The source has been modified to be included under the Java package com.ongres.scram.common.bouncycastle
-and to only include used parts.
diff --git a/README.md b/README.md
index caa651c..138111e 100644
--- a/README.md
+++ b/README.md
@@ -1,70 +1,72 @@
# SCRAM Java Implementation
-## Overview
-
-SCRAM (Salted Challenge Response Authentication Mechanism) is part of the family
-of Simple Authentication and Security Layer
-([SASL, RFC 4422](https://tools.ietf.org/html/rfc4422)) authentication
-mechanisms.
-
-It is described as part of [RFC 5802](https://tools.ietf.org/html/rfc5802) and
-[RFC7677](https://tools.ietf.org/html/rfc7677).
-
-This project will serve for the basis of
-[PostgreSQL's](https://www.postgresql.org) [JDBC](https://jdbc.postgresql.org/)
-driver SCRAM support (coming in PostgreSQL 10).
-
-The code is licensed under the BSD "Simplified 2 Clause" license (see [LICENSE](LICENSE)).
-
-
-## Goals
-
-This project aims to provide a complete clean-room implementation of SCRAM. It
-is written in Java and provided in a modular, re-usable way, independent of
-other software or programs.
-
-Current functionality includes:
+
+[](https://github.com/jvm-repo-rebuild/reproducible-central/blob/master/content/com/ongres/scram/README.md)
+
-* [Common infrastructure](common) for building both client and server SCRAM implementations.
-* A [Client API](client) for using SCRAM as a client.
-* Support for both SHA-1 and SHA-256.
-* Basic support for channel binding.
-* No runtime external dependencies.
-* Well tested (+75 tests).
-
-
-Current limitations:
-
-* Server API and integration tests will be added soon.
-
-
-## How to use the client API
-
-Please read [Client's README.md](client).
-
-Javadoc: [](http://javadoc.io/doc/com.ongres.scram/client)
+> Salted Challenge Response Authentication Mechanism (SCRAM)
+## Overview
+SCRAM (Salted Challenge Response Authentication Mechanism) is part of the family of
+Simple Authentication and Security Layer
+([SASL, RFC 4422](https://datatracker.ietf.org/doc/html/rfc4422)) authentication mechanisms. It is described as part of [RFC 5802](https://datatracker.ietf.org/doc/html/rfc5802) and
+[RFC 7677](https://datatracker.ietf.org/doc/html/rfc7677).
-## Common API
+This project provides a robust and well-tested implementation of the Salted Challenge
+Response Authentication Mechanism (SCRAM) in Java. It adheres to the specifications
+outlined in RFC 5802 and RFC 7677, ensuring secure user authentication.
-'common' is the module that contains code common to both client and server SCRAM projects.
-If you with to develop either a client or server API, you may very well build on top of this
-API. Import maven dependency:
+This SCRAM Java implementation can be used for [PostgreSQL](https://www.postgresql.org) (which supports [SASL authentication](https://www.postgresql.org/docs/current/sasl-authentication.html) since PostgreSQL 10) through the [PostgreSQL JDBC Driver](https://jdbc.postgresql.org/) and others projects that connect from Java.
-
- * doFinal leaves the MAC in the same state it was after the last init. - * - * @param out the array the MAC is to be output to. - * @param outOff the offset into the out buffer the output is to start at. - * @exception DataLengthException if there isn't enough space in out. - * @exception IllegalStateException if the MAC is not initialised. - */ - public int doFinal(byte[] out, int outOff) - throws DataLengthException, IllegalStateException; - - /** - * Reset the MAC. At the end of resetting the MAC should be in the - * in the same state it was after the last init (if there was one). - */ - public void reset(); -} diff --git a/common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/Memoable.java b/common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/Memoable.java deleted file mode 100644 index 4d435d9..0000000 --- a/common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/Memoable.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2019, 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 com.ongres.scram.common.bouncycastle.pbkdf2; - -/** - * Interface for Memoable objects. Memoable objects allow the taking of a snapshot of their internal state - * via the copy() method and then reseting the object back to that state later using the reset() method. - */ -public interface Memoable -{ - /** - * Produce a copy of this object with its configuration and in its current state. - *
- * The returned object may be used simply to store the state, or may be used as a similar object - * starting from the copied state. - */ - Memoable copy(); - - /** - * Restore a copied object state into this object. - *
- * Implementations of this method should try to avoid or minimise memory allocation to perform the reset. - * - * @param other an object originally {@link #copy() copied} from an object of the same type as this instance. - * @throws ClassCastException if the provided object is not of the correct type. - */ - void reset(Memoable other); -} diff --git a/common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/PBEParametersGenerator.java b/common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/PBEParametersGenerator.java deleted file mode 100644 index 6b78a23..0000000 --- a/common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/PBEParametersGenerator.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright 2019, 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 com.ongres.scram.common.bouncycastle.pbkdf2; - -/** - * super class for all Password Based Encryption (PBE) parameter generator classes. - */ -public abstract class PBEParametersGenerator -{ - protected byte[] password; - protected byte[] salt; - protected int iterationCount; - - /** - * base constructor. - */ - protected PBEParametersGenerator() - { - } - - /** - * initialise the PBE generator. - * - * @param password the password converted into bytes (see below). - * @param salt the salt to be mixed with the password. - * @param iterationCount the number of iterations the "mixing" function - * is to be applied for. - */ - public void init( - byte[] password, - byte[] salt, - int iterationCount) - { - this.password = password; - this.salt = salt; - this.iterationCount = iterationCount; - } - - /** - * return the password byte array. - * - * @return the password byte array. - */ - public byte[] getPassword() - { - return password; - } - - /** - * return the salt byte array. - * - * @return the salt byte array. - */ - public byte[] getSalt() - { - return salt; - } - - /** - * return the iteration count. - * - * @return the iteration count. - */ - public int getIterationCount() - { - return iterationCount; - } - - /** - * generate derived parameters for a key of length keySize. - * - * @param keySize the length, in bits, of the key required. - * @return a parameters object representing a key. - */ - public abstract CipherParameters generateDerivedParameters(int keySize); - - /** - * converts a password to a byte array according to the scheme in - * PKCS5 (UTF-8, no padding) - * - * @param password a character array representing the password. - * @return a byte array representing the password. - */ - public static byte[] PKCS5PasswordToUTF8Bytes( - char[] password) - { - if (password != null) - { - return Strings.toUTF8ByteArray(password); - } - else - { - return new byte[0]; - } - } -} diff --git a/common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/PKCS5S2ParametersGenerator.java b/common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/PKCS5S2ParametersGenerator.java deleted file mode 100644 index 2129f4c..0000000 --- a/common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/PKCS5S2ParametersGenerator.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2019, 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 com.ongres.scram.common.bouncycastle.pbkdf2; - -/** - * Generator for PBE derived keys and ivs as defined by PKCS 5 V2.0 Scheme 2. - * This generator uses a SHA-1 HMac as the calculation function. - *
- * The document this implementation is based on can be found at - * - * RSA's PKCS5 Page - */ -public class PKCS5S2ParametersGenerator - extends PBEParametersGenerator -{ - private Mac hMac; - private byte[] state; - - public PKCS5S2ParametersGenerator(Digest digest) - { - hMac = new HMac(digest); - state = new byte[hMac.getMacSize()]; - } - - private void F( - byte[] S, - int c, - byte[] iBuf, - byte[] out, - int outOff) - { - if (c == 0) - { - throw new IllegalArgumentException("iteration count must be at least 1."); - } - - if (S != null) - { - hMac.update(S, 0, S.length); - } - - hMac.update(iBuf, 0, iBuf.length); - hMac.doFinal(state, 0); - - System.arraycopy(state, 0, out, outOff, state.length); - - for (int count = 1; count < c; count++) - { - hMac.update(state, 0, state.length); - hMac.doFinal(state, 0); - - for (int j = 0; j != state.length; j++) - { - out[outOff + j] ^= state[j]; - } - } - } - - private byte[] generateDerivedKey( - int dkLen) - { - int hLen = hMac.getMacSize(); - int l = (dkLen + hLen - 1) / hLen; - byte[] iBuf = new byte[4]; - byte[] outBytes = new byte[l * hLen]; - int outPos = 0; - - CipherParameters param = new KeyParameter(password); - - hMac.init(param); - - for (int i = 1; i <= l; i++) - { - // Increment the value in 'iBuf' - int pos = 3; - while (++iBuf[pos] == 0) - { - --pos; - } - - F(salt, iterationCount, iBuf, outBytes, outPos); - outPos += hLen; - } - - return outBytes; - } - - /** - * Generate a key parameter derived from the password, salt, and iteration - * count we are currently initialised with. - * - * @param keySize the size of the key we want (in bits) - * @return a KeyParameter object. - */ - public CipherParameters generateDerivedParameters( - int keySize) - { - keySize = keySize / 8; - - byte[] dKey = Arrays.copyOfRange(generateDerivedKey(keySize), 0, keySize); - - return new KeyParameter(dKey, 0, keySize); - } -} diff --git a/common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/Pack.java b/common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/Pack.java deleted file mode 100644 index 5c3a5a3..0000000 --- a/common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/Pack.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2019, 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 com.ongres.scram.common.bouncycastle.pbkdf2; - -/** - * Utility methods for converting byte arrays into ints and longs, and back again. - */ -public abstract class Pack -{ - public static int bigEndianToInt(byte[] bs, int off) - { - int n = bs[ off] << 24; - n |= (bs[++off] & 0xff) << 16; - n |= (bs[++off] & 0xff) << 8; - n |= (bs[++off] & 0xff); - return n; - } - - public static long bigEndianToLong(byte[] bs, int off) - { - int hi = bigEndianToInt(bs, off); - int lo = bigEndianToInt(bs, off + 4); - return ((long)(hi & 0xffffffffL) << 32) | (long)(lo & 0xffffffffL); - } - - public static void longToBigEndian(long n, byte[] bs, int off) - { - intToBigEndian((int)(n >>> 32), bs, off); - intToBigEndian((int)(n & 0xffffffffL), bs, off + 4); - } - - public static byte[] longToBigEndian(long[] ns) - { - byte[] bs = new byte[8 * ns.length]; - longToBigEndian(ns, bs, 0); - return bs; - } - - public static void longToBigEndian(long[] ns, byte[] bs, int off) - { - for (int i = 0; i < ns.length; ++i) - { - longToBigEndian(ns[i], bs, off); - off += 8; - } - } - - public static short littleEndianToShort(byte[] bs, int off) - { - int n = bs[ off] & 0xff; - n |= (bs[++off] & 0xff) << 8; - return (short)n; - } - - public static void intToBigEndian(int n, byte[] bs, int off) - { - bs[ off] = (byte)(n >>> 24); - bs[++off] = (byte)(n >>> 16); - bs[++off] = (byte)(n >>> 8); - bs[++off] = (byte)(n ); - } -} diff --git a/common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/RuntimeCryptoException.java b/common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/RuntimeCryptoException.java deleted file mode 100644 index 60e2b6c..0000000 --- a/common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/RuntimeCryptoException.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2019, 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 com.ongres.scram.common.bouncycastle.pbkdf2; - -/** - * the foundation class for the exceptions thrown by the crypto packages. - */ -public class RuntimeCryptoException - extends RuntimeException -{ - /** - * base constructor. - */ - public RuntimeCryptoException() - { - } - - /** - * create a RuntimeCryptoException with the given message. - * - * @param message the message to be carried with the exception. - */ - public RuntimeCryptoException( - String message) - { - super(message); - } -} diff --git a/common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/SHA256Digest.java b/common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/SHA256Digest.java deleted file mode 100644 index b68ee09..0000000 --- a/common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/SHA256Digest.java +++ /dev/null @@ -1,382 +0,0 @@ -/* - * Copyright 2019, 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 com.ongres.scram.common.bouncycastle.pbkdf2; - -/** - * FIPS 180-2 implementation of SHA-256. - * - *
- * block word digest - * SHA-1 512 32 160 - * SHA-256 512 32 256 - * SHA-384 1024 64 384 - * SHA-512 1024 64 512 - *- */ -public class SHA256Digest - extends GeneralDigest - implements EncodableDigest -{ - private static final int DIGEST_LENGTH = 32; - - private int H1, H2, H3, H4, H5, H6, H7, H8; - - private int[] X = new int[64]; - private int xOff; - - /** - * Standard constructor - */ - public SHA256Digest() - { - reset(); - } - - /** - * Copy constructor. This will copy the state of the provided - * message digest. - */ - public SHA256Digest(SHA256Digest t) - { - super(t); - - copyIn(t); - } - - private void copyIn(SHA256Digest t) - { - super.copyIn(t); - - H1 = t.H1; - H2 = t.H2; - H3 = t.H3; - H4 = t.H4; - H5 = t.H5; - H6 = t.H6; - H7 = t.H7; - H8 = t.H8; - - System.arraycopy(t.X, 0, X, 0, t.X.length); - xOff = t.xOff; - } - - /** - * State constructor - create a digest initialised with the state of a previous one. - * - * @param encodedState the encoded state from the originating digest. - */ - public SHA256Digest(byte[] encodedState) - { - super(encodedState); - - H1 = Pack.bigEndianToInt(encodedState, 16); - H2 = Pack.bigEndianToInt(encodedState, 20); - H3 = Pack.bigEndianToInt(encodedState, 24); - H4 = Pack.bigEndianToInt(encodedState, 28); - H5 = Pack.bigEndianToInt(encodedState, 32); - H6 = Pack.bigEndianToInt(encodedState, 36); - H7 = Pack.bigEndianToInt(encodedState, 40); - H8 = Pack.bigEndianToInt(encodedState, 44); - - xOff = Pack.bigEndianToInt(encodedState, 48); - for (int i = 0; i != xOff; i++) - { - X[i] = Pack.bigEndianToInt(encodedState, 52 + (i * 4)); - } - } - - - public String getAlgorithmName() - { - return "SHA-256"; - } - - public int getDigestSize() - { - return DIGEST_LENGTH; - } - - protected void processWord( - byte[] in, - int inOff) - { - // Note: Inlined for performance -// X[xOff] = Pack.bigEndianToInt(in, inOff); - int n = in[inOff] << 24; - n |= (in[++inOff] & 0xff) << 16; - n |= (in[++inOff] & 0xff) << 8; - n |= (in[++inOff] & 0xff); - X[xOff] = n; - - if (++xOff == 16) - { - processBlock(); - } - } - - protected void processLength( - long bitLength) - { - if (xOff > 14) - { - processBlock(); - } - - X[14] = (int)(bitLength >>> 32); - X[15] = (int)(bitLength & 0xffffffff); - } - - public int doFinal( - byte[] out, - int outOff) - { - finish(); - - Pack.intToBigEndian(H1, out, outOff); - Pack.intToBigEndian(H2, out, outOff + 4); - Pack.intToBigEndian(H3, out, outOff + 8); - Pack.intToBigEndian(H4, out, outOff + 12); - Pack.intToBigEndian(H5, out, outOff + 16); - Pack.intToBigEndian(H6, out, outOff + 20); - Pack.intToBigEndian(H7, out, outOff + 24); - Pack.intToBigEndian(H8, out, outOff + 28); - - reset(); - - return DIGEST_LENGTH; - } - - /** - * reset the chaining variables - */ - public void reset() - { - super.reset(); - - /* SHA-256 initial hash value - * The first 32 bits of the fractional parts of the square roots - * of the first eight prime numbers - */ - - H1 = 0x6a09e667; - H2 = 0xbb67ae85; - H3 = 0x3c6ef372; - H4 = 0xa54ff53a; - H5 = 0x510e527f; - H6 = 0x9b05688c; - H7 = 0x1f83d9ab; - H8 = 0x5be0cd19; - - xOff = 0; - for (int i = 0; i != X.length; i++) - { - X[i] = 0; - } - } - - protected void processBlock() - { - // - // expand 16 word block into 64 word blocks. - // - for (int t = 16; t <= 63; t++) - { - X[t] = Theta1(X[t - 2]) + X[t - 7] + Theta0(X[t - 15]) + X[t - 16]; - } - - // - // set up working variables. - // - int a = H1; - int b = H2; - int c = H3; - int d = H4; - int e = H5; - int f = H6; - int g = H7; - int h = H8; - - int t = 0; - for(int i = 0; i < 8; i ++) - { - // t = 8 * i - h += Sum1(e) + Ch(e, f, g) + K[t] + X[t]; - d += h; - h += Sum0(a) + Maj(a, b, c); - ++t; - - // t = 8 * i + 1 - g += Sum1(d) + Ch(d, e, f) + K[t] + X[t]; - c += g; - g += Sum0(h) + Maj(h, a, b); - ++t; - - // t = 8 * i + 2 - f += Sum1(c) + Ch(c, d, e) + K[t] + X[t]; - b += f; - f += Sum0(g) + Maj(g, h, a); - ++t; - - // t = 8 * i + 3 - e += Sum1(b) + Ch(b, c, d) + K[t] + X[t]; - a += e; - e += Sum0(f) + Maj(f, g, h); - ++t; - - // t = 8 * i + 4 - d += Sum1(a) + Ch(a, b, c) + K[t] + X[t]; - h += d; - d += Sum0(e) + Maj(e, f, g); - ++t; - - // t = 8 * i + 5 - c += Sum1(h) + Ch(h, a, b) + K[t] + X[t]; - g += c; - c += Sum0(d) + Maj(d, e, f); - ++t; - - // t = 8 * i + 6 - b += Sum1(g) + Ch(g, h, a) + K[t] + X[t]; - f += b; - b += Sum0(c) + Maj(c, d, e); - ++t; - - // t = 8 * i + 7 - a += Sum1(f) + Ch(f, g, h) + K[t] + X[t]; - e += a; - a += Sum0(b) + Maj(b, c, d); - ++t; - } - - H1 += a; - H2 += b; - H3 += c; - H4 += d; - H5 += e; - H6 += f; - H7 += g; - H8 += h; - - // - // reset the offset and clean out the word buffer. - // - xOff = 0; - for (int i = 0; i < 16; i++) - { - X[i] = 0; - } - } - - /* SHA-256 functions */ - private int Ch( - int x, - int y, - int z) - { - return (x & y) ^ ((~x) & z); - } - - private int Maj( - int x, - int y, - int z) - { - return (x & y) ^ (x & z) ^ (y & z); - } - - private int Sum0( - int x) - { - return ((x >>> 2) | (x << 30)) ^ ((x >>> 13) | (x << 19)) ^ ((x >>> 22) | (x << 10)); - } - - private int Sum1( - int x) - { - return ((x >>> 6) | (x << 26)) ^ ((x >>> 11) | (x << 21)) ^ ((x >>> 25) | (x << 7)); - } - - private int Theta0( - int x) - { - return ((x >>> 7) | (x << 25)) ^ ((x >>> 18) | (x << 14)) ^ (x >>> 3); - } - - private int Theta1( - int x) - { - return ((x >>> 17) | (x << 15)) ^ ((x >>> 19) | (x << 13)) ^ (x >>> 10); - } - - /* SHA-256 Constants - * (represent the first 32 bits of the fractional parts of the - * cube roots of the first sixty-four prime numbers) - */ - static final int K[] = { - 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, - 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, - 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, - 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, - 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, - 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, - 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, - 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 - }; - - public Memoable copy() - { - return new SHA256Digest(this); - } - - public void reset(Memoable other) - { - SHA256Digest d = (SHA256Digest)other; - - copyIn(d); - } - - public byte[] getEncodedState() - { - byte[] state = new byte[52 + xOff * 4]; - - super.populateState(state); - - Pack.intToBigEndian(H1, state, 16); - Pack.intToBigEndian(H2, state, 20); - Pack.intToBigEndian(H3, state, 24); - Pack.intToBigEndian(H4, state, 28); - Pack.intToBigEndian(H5, state, 32); - Pack.intToBigEndian(H6, state, 36); - Pack.intToBigEndian(H7, state, 40); - Pack.intToBigEndian(H8, state, 44); - Pack.intToBigEndian(xOff, state, 48); - - for (int i = 0; i != xOff; i++) - { - Pack.intToBigEndian(X[i], state, 52 + (i * 4)); - } - - return state; - } -} - diff --git a/common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/Strings.java b/common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/Strings.java deleted file mode 100644 index 19a24c5..0000000 --- a/common/src/main/java/com/ongres/scram/common/bouncycastle/pbkdf2/Strings.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright 2019, 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 com.ongres.scram.common.bouncycastle.pbkdf2; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -/** - * String utilities. - */ -public final class Strings -{ - - public static byte[] toUTF8ByteArray(char[] string) - { - ByteArrayOutputStream bOut = new ByteArrayOutputStream(); - - try - { - toUTF8ByteArray(string, bOut); - } - catch (IOException e) - { - throw new IllegalStateException("cannot encode string to byte array!"); - } - - return bOut.toByteArray(); - } - - public static void toUTF8ByteArray(char[] string, OutputStream sOut) - throws IOException - { - char[] c = string; - int i = 0; - - while (i < c.length) - { - char ch = c[i]; - - if (ch < 0x0080) - { - sOut.write(ch); - } - else if (ch < 0x0800) - { - sOut.write(0xc0 | (ch >> 6)); - sOut.write(0x80 | (ch & 0x3f)); - } - // surrogate pair - else if (ch >= 0xD800 && ch <= 0xDFFF) - { - // in error - can only happen, if the Java String class has a - // bug. - if (i + 1 >= c.length) - { - throw new IllegalStateException("invalid UTF-16 codepoint"); - } - char W1 = ch; - ch = c[++i]; - char W2 = ch; - // in error - can only happen, if the Java String class has a - // bug. - if (W1 > 0xDBFF) - { - throw new IllegalStateException("invalid UTF-16 codepoint"); - } - int codePoint = (((W1 & 0x03FF) << 10) | (W2 & 0x03FF)) + 0x10000; - sOut.write(0xf0 | (codePoint >> 18)); - sOut.write(0x80 | ((codePoint >> 12) & 0x3F)); - sOut.write(0x80 | ((codePoint >> 6) & 0x3F)); - sOut.write(0x80 | (codePoint & 0x3F)); - } - else - { - sOut.write(0xe0 | (ch >> 12)); - sOut.write(0x80 | ((ch >> 6) & 0x3F)); - sOut.write(0x80 | (ch & 0x3F)); - } - - i++; - } - } - - /** - * Convert an array of 8 bit characters into a string. - * - * @param bytes 8 bit characters. - * @return resulting String. - */ - public static String fromByteArray(byte[] bytes) - { - return new String(asCharArray(bytes)); - } - - /** - * Do a simple conversion of an array of 8 bit characters into a string. - * - * @param bytes 8 bit characters. - * @return resulting String. - */ - public static char[] asCharArray(byte[] bytes) - { - char[] chars = new char[bytes.length]; - - for (int i = 0; i != chars.length; i++) - { - chars[i] = (char)(bytes[i] & 0xff); - } - - return chars; - } -} diff --git a/common/src/main/java/com/ongres/scram/common/exception/ScramException.java b/common/src/main/java/com/ongres/scram/common/exception/ScramException.java deleted file mode 100644 index 430dbdc..0000000 --- a/common/src/main/java/com/ongres/scram/common/exception/ScramException.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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 com.ongres.scram.common.exception; - - -import javax.security.sasl.SaslException; - - -/** - * This class represents an error when using SCRAM, which is a SASL method. - * - * {@link SaslException} - */ -public class ScramException extends SaslException { - /** - * Constructs a new instance of ScramException with a detailed message. - * @param detail A String containing details about the exception - */ - public ScramException(String detail) { - super(detail); - } - - /** - * Constructs a new instance of ScramException with a detailed message and a root cause. - * @param detail A String containing details about the exception - * @param ex The root exception - */ - public ScramException(String detail, Throwable ex) { - super(detail, ex); - } -} diff --git a/common/src/main/java/com/ongres/scram/common/exception/ScramInvalidServerSignatureException.java b/common/src/main/java/com/ongres/scram/common/exception/ScramInvalidServerSignatureException.java deleted file mode 100644 index d0712fc..0000000 --- a/common/src/main/java/com/ongres/scram/common/exception/ScramInvalidServerSignatureException.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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 com.ongres.scram.common.exception; - - -/** - * This class represents an error when parsing SCRAM messages - */ -public class ScramInvalidServerSignatureException extends ScramException { - /** - * Constructs a new instance of ScramInvalidServerSignatureException with a detailed message. - * @param detail A String containing details about the exception - */ - public ScramInvalidServerSignatureException(String detail) { - super(detail); - } - - /** - * Constructs a new instance of ScramInvalidServerSignatureException with a detailed message and a root cause. - * @param detail A String containing details about the exception - * @param ex The root exception - */ - public ScramInvalidServerSignatureException(String detail, Throwable ex) { - super(detail, ex); - } -} diff --git a/common/src/main/java/com/ongres/scram/common/exception/ScramParseException.java b/common/src/main/java/com/ongres/scram/common/exception/ScramParseException.java deleted file mode 100644 index b9ba1ce..0000000 --- a/common/src/main/java/com/ongres/scram/common/exception/ScramParseException.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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 com.ongres.scram.common.exception; - - -/** - * This class represents an error when parsing SCRAM messages - */ -public class ScramParseException extends ScramException { - /** - * Constructs a new instance of ScramParseException with a detailed message. - * @param detail A String containing details about the exception - */ - public ScramParseException(String detail) { - super(detail); - } - - /** - * Constructs a new instance of ScramParseException with a detailed message and a root cause. - * @param detail A String containing details about the exception - * @param ex The root exception - */ - public ScramParseException(String detail, Throwable ex) { - super(detail, ex); - } -} diff --git a/common/src/main/java/com/ongres/scram/common/exception/ScramServerErrorException.java b/common/src/main/java/com/ongres/scram/common/exception/ScramServerErrorException.java deleted file mode 100644 index dcf0872..0000000 --- a/common/src/main/java/com/ongres/scram/common/exception/ScramServerErrorException.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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 com.ongres.scram.common.exception; - - -import com.ongres.scram.common.message.ServerFinalMessage; - - -/** - * This class represents an error when parsing SCRAM messages - */ -public class ScramServerErrorException extends ScramException { - private final ServerFinalMessage.Error error; - - private static String toString(ServerFinalMessage.Error error) { - return "Server-final-message is an error message. Error: " + error.getErrorMessage(); - } - - /** - * Constructs a new instance of ScramServerErrorException with a detailed message. - * @param error The SCRAM error in the message - */ - public ScramServerErrorException(ServerFinalMessage.Error error) { - super(toString(error)); - this.error = error; - } - - /** - * Constructs a new instance of ScramServerErrorException with a detailed message and a root cause. - * @param error The SCRAM error in the message - * @param ex The root exception - */ - public ScramServerErrorException(ServerFinalMessage.Error error, Throwable ex) { - super(toString(error), ex); - this.error = error; - } - - public ServerFinalMessage.Error getError() { - return error; - } -} diff --git a/common/src/main/java/com/ongres/scram/common/gssapi/Gs2AttributeValue.java b/common/src/main/java/com/ongres/scram/common/gssapi/Gs2AttributeValue.java deleted file mode 100644 index 3869738..0000000 --- a/common/src/main/java/com/ongres/scram/common/gssapi/Gs2AttributeValue.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 com.ongres.scram.common.gssapi; - - -import com.ongres.scram.common.util.AbstractCharAttributeValue; - -/** - * Parse and write GS2 Attribute-Value pairs. - */ -public class Gs2AttributeValue extends AbstractCharAttributeValue { - public Gs2AttributeValue(Gs2Attributes attribute, String value) { - super(attribute, value); - } - - public static StringBuffer writeTo(StringBuffer sb, Gs2Attributes attribute, String value) { - return new Gs2AttributeValue(attribute, value).writeTo(sb); - } - - /** - * Parses a potential Gs2AttributeValue String. - * @param value The string that contains the Attribute-Value pair (where value is optional). - * @return The parsed class, or null if the String was null. - * @throws IllegalArgumentException If the String is an invalid Gs2AttributeValue - */ - public static Gs2AttributeValue parse(String value) throws IllegalArgumentException { - if(null == value) { - return null; - } - - if(value.length() < 1 || value.length() == 2 || (value.length() > 2 && value.charAt(1) != '=')) { - throw new IllegalArgumentException("Invalid Gs2AttributeValue"); - } - - return new Gs2AttributeValue( - Gs2Attributes.byChar(value.charAt(0)), - value.length() > 2 ? value.substring(2) : null - ); - } -} diff --git a/common/src/main/java/com/ongres/scram/common/gssapi/Gs2Attributes.java b/common/src/main/java/com/ongres/scram/common/gssapi/Gs2Attributes.java deleted file mode 100644 index d101e73..0000000 --- a/common/src/main/java/com/ongres/scram/common/gssapi/Gs2Attributes.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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 com.ongres.scram.common.gssapi; - - -import com.ongres.scram.common.ScramAttributes; -import com.ongres.scram.common.util.CharAttribute; - - -/** - * Possible values of a GS2 Attribute. - * - * @see [RFC5802] Formal Syntax - */ -public enum Gs2Attributes implements CharAttribute { - /** - * Channel binding attribute. Client doesn't support channel binding. - */ - CLIENT_NOT(Gs2CbindFlag.CLIENT_NOT.getChar()), - - /** - * Channel binding attribute. Client does support channel binding but thinks the server does not. - */ - CLIENT_YES_SERVER_NOT(Gs2CbindFlag.CLIENT_YES_SERVER_NOT.getChar()), - - /** - * Channel binding attribute. Client requires channel binding. The selected channel binding follows "p=". - */ - CHANNEL_BINDING_REQUIRED(Gs2CbindFlag.CHANNEL_BINDING_REQUIRED.getChar()), - - /** - * SCRAM attribute. This attribute specifies an authorization identity. - */ - AUTHZID(ScramAttributes.AUTHZID.getChar()) - ; - - private final char flag; - - Gs2Attributes(char flag) { - this.flag = flag; - } - - @Override - public char getChar() { - return flag; - } - - public static Gs2Attributes byChar(char c) { - switch(c) { - case 'n': return CLIENT_NOT; - case 'y': return CLIENT_YES_SERVER_NOT; - case 'p': return CHANNEL_BINDING_REQUIRED; - case 'a': return AUTHZID; - } - - throw new IllegalArgumentException("Invalid GS2Attribute character '" + c + "'"); - } - - public static Gs2Attributes byGS2CbindFlag(Gs2CbindFlag cbindFlag) { - return byChar(cbindFlag.getChar()); - } -} diff --git a/common/src/main/java/com/ongres/scram/common/gssapi/Gs2CbindFlag.java b/common/src/main/java/com/ongres/scram/common/gssapi/Gs2CbindFlag.java deleted file mode 100644 index e2025fa..0000000 --- a/common/src/main/java/com/ongres/scram/common/gssapi/Gs2CbindFlag.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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 com.ongres.scram.common.gssapi; - - -import com.ongres.scram.common.util.CharAttribute; - - -/** - * Possible values of a GS2 Cbind Flag (channel binding; part of GS2 header). - * These values are sent by the client, and so are interpreted from this perspective. - * - * @see [RFC5802] Formal Syntax - */ -public enum Gs2CbindFlag implements CharAttribute { - /** - * Client doesn't support channel binding. - */ - CLIENT_NOT('n'), - - /** - * Client does support channel binding but thinks the server does not. - */ - CLIENT_YES_SERVER_NOT('y'), - - /** - * Client requires channel binding. The selected channel binding follows "p=". - */ - CHANNEL_BINDING_REQUIRED('p') - ; - - private final char flag; - - Gs2CbindFlag(char flag) { - this.flag = flag; - } - - @Override - public char getChar() { - return flag; - } - - public static Gs2CbindFlag byChar(char c) { - switch(c) { - case 'n': return CLIENT_NOT; - case 'y': return CLIENT_YES_SERVER_NOT; - case 'p': return CHANNEL_BINDING_REQUIRED; - } - - throw new IllegalArgumentException("Invalid Gs2CbindFlag character '" + c + "'"); - } -} diff --git a/common/src/main/java/com/ongres/scram/common/gssapi/Gs2Header.java b/common/src/main/java/com/ongres/scram/common/gssapi/Gs2Header.java deleted file mode 100644 index b794967..0000000 --- a/common/src/main/java/com/ongres/scram/common/gssapi/Gs2Header.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * 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 com.ongres.scram.common.gssapi; - - -import com.ongres.scram.common.util.StringWritableCsv; -import com.ongres.scram.common.ScramStringFormatting; -import com.ongres.scram.common.util.AbstractStringWritable; - -import static com.ongres.scram.common.util.Preconditions.checkNotNull; - - -/** - * GSS Header. Format: - * - * {@code - * gs2-header = gs2-cbind-flag "," [ authzid ] "," - * gs2-cbind-flag = ("p=" cb-name) / "n" / "y" - * authzid = "a=" saslname - * } - * - * Current implementation does not support channel binding. - * If p is used as the cbind flag, the cb-name value is not validated. - * - * @see [RFC5802] Formal Syntax - */ -public class Gs2Header extends AbstractStringWritable { - private final Gs2AttributeValue cbind; - private final Gs2AttributeValue authzid; - - /** - * Construct and validates a Gs2Header. - * Only provide the channel binding name if the channel binding flag is set to required. - * @param cbindFlag The channel binding flag - * @param cbName The channel-binding name. Should be not null iif channel binding is required - * @param authzid The optional SASL authorization identity - * @throws IllegalArgumentException If the channel binding flag and argument are invalid - */ - public Gs2Header(Gs2CbindFlag cbindFlag, String cbName, String authzid) throws IllegalArgumentException { - checkNotNull(cbindFlag, "cbindFlag"); - if(cbindFlag == Gs2CbindFlag.CHANNEL_BINDING_REQUIRED ^ cbName != null) { - throw new IllegalArgumentException("Specify channel binding flag and value together, or none"); - } - // TODO: cbName is not being properly validated - cbind = new Gs2AttributeValue(Gs2Attributes.byGS2CbindFlag(cbindFlag), cbName); - - this.authzid = authzid == null ? - null : new Gs2AttributeValue(Gs2Attributes.AUTHZID, ScramStringFormatting.toSaslName(authzid)) - ; - } - - /** - * Construct and validates a Gs2Header with no authzid. - * Only provide the channel binding name if the channel binding flag is set to required. - * @param cbindFlag The channel binding flag - * @param cbName The channel-binding name. Should be not null iif channel binding is required - * @throws IllegalArgumentException If the channel binding flag and argument are invalid - */ - public Gs2Header(Gs2CbindFlag cbindFlag, String cbName) throws IllegalArgumentException { - this(cbindFlag, cbName, null); - } - - /** - * Construct and validates a Gs2Header with no authzid nor channel binding. - * @param cbindFlag The channel binding flag - * @throws IllegalArgumentException If the channel binding is supported (no cbname can be provided here) - */ - public Gs2Header(Gs2CbindFlag cbindFlag) { - this(cbindFlag, null, null); - } - - public Gs2CbindFlag getChannelBindingFlag() { - return Gs2CbindFlag.byChar(cbind.getChar()); - } - - public String getChannelBindingName() { - return cbind.getValue(); - } - - public String getAuthzid() { - return authzid != null ? authzid.getValue() : null; - } - - @Override - public StringBuffer writeTo(StringBuffer sb) { - return StringWritableCsv.writeTo(sb, cbind, authzid); - } - - /** - * Read a Gs2Header from a String. String may contain trailing fields that will be ignored. - * @param message The String containing the Gs2Header - * @return The parsed Gs2Header object - * @throws IllegalArgumentException If the format/values of the String do not conform to a Gs2Header - */ - public static Gs2Header parseFrom(String message) throws IllegalArgumentException { - checkNotNull(message, "Null message"); - - String[] gs2HeaderSplit = StringWritableCsv.parseFrom(message, 2); - if(gs2HeaderSplit.length == 0) { - throw new IllegalArgumentException("Invalid number of fields for the GS2 Header"); - } - - Gs2AttributeValue gs2cbind = Gs2AttributeValue.parse(gs2HeaderSplit[0]); - return new Gs2Header( - Gs2CbindFlag.byChar(gs2cbind.getChar()), - gs2cbind.getValue(), - gs2HeaderSplit[1] == null || gs2HeaderSplit[1].isEmpty() ? - null : Gs2AttributeValue.parse(gs2HeaderSplit[1]).getValue() - ); - } -} diff --git a/common/src/main/java/com/ongres/scram/common/message/ClientFinalMessage.java b/common/src/main/java/com/ongres/scram/common/message/ClientFinalMessage.java deleted file mode 100644 index a47ebe4..0000000 --- a/common/src/main/java/com/ongres/scram/common/message/ClientFinalMessage.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * 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 com.ongres.scram.common.message; - - -import com.ongres.scram.common.ScramAttributeValue; -import com.ongres.scram.common.ScramAttributes; -import com.ongres.scram.common.ScramStringFormatting; -import com.ongres.scram.common.gssapi.Gs2Header; -import com.ongres.scram.common.util.StringWritable; -import com.ongres.scram.common.util.StringWritableCsv; - -import static com.ongres.scram.common.util.Preconditions.checkNotEmpty; -import static com.ongres.scram.common.util.Preconditions.checkNotNull; - - -/** - * Constructs and parses client-final-messages. Formal syntax is: - * - * {@code - * client-final-message-without-proof = channel-binding "," nonce ["," extensions] - * client-final-message = client-final-message-without-proof "," proof - * } - * - * Note that extensions are not supported. - * - * @see [RFC5802] Section 7 - */ -public class ClientFinalMessage implements StringWritable { - private final String cbind; - private final String nonce; - private final byte[] proof; - - private static String generateCBind(Gs2Header gs2Header, byte[] cbindData) { - StringBuffer sb = new StringBuffer(); - gs2Header.writeTo(sb) - .append(','); - - if(null != cbindData) { - new ScramAttributeValue( - ScramAttributes.CHANNEL_BINDING, - ScramStringFormatting.base64Encode(cbindData) - ).writeTo(sb); - } - - return sb.toString(); - } - - /** - * Constructus a client-final-message with the provided gs2Header (the same one used in the client-first-message), - * optionally the channel binding data, and the nonce. - * This method is intended to be used by SCRAM clients, and not to be constructed directly. - * @param gs2Header The GSS-API header - * @param cbindData If using channel binding, the channel binding data - * @param nonce The nonce - * @param proof The bytes representing the computed client proof - */ - public ClientFinalMessage(Gs2Header gs2Header, byte[] cbindData, String nonce, byte[] proof) { - this.cbind = generateCBind( - checkNotNull(gs2Header, "gs2Header"), - cbindData - ); - this.nonce = checkNotEmpty(nonce, "nonce"); - this.proof = checkNotNull(proof, "proof"); - } - - private static StringBuffer writeToWithoutProof(StringBuffer sb, String cbind, String nonce) { - return StringWritableCsv.writeTo( - sb, - new ScramAttributeValue(ScramAttributes.CHANNEL_BINDING, ScramStringFormatting.base64Encode(cbind)), - new ScramAttributeValue(ScramAttributes.NONCE, nonce) - ); - } - - private static StringBuffer writeToWithoutProof( - StringBuffer sb, Gs2Header gs2Header, byte[] cbindData, String nonce - ) { - return writeToWithoutProof( - sb, - generateCBind( - checkNotNull(gs2Header, "gs2Header"), - cbindData - ), - nonce - ); - } - - /** - * Returns a StringBuffer filled in with the formatted output of a client-first-message without the proof value. - * This is useful for computing the auth-message, used in turn to compute the proof. - * @param gs2Header The GSS-API header - * @param cbindData The optional channel binding data - * @param nonce The nonce - * @return The String representation of the part of the message that excludes the proof - */ - public static StringBuffer writeToWithoutProof(Gs2Header gs2Header, byte[] cbindData, String nonce) { - return writeToWithoutProof(new StringBuffer(), gs2Header, cbindData, nonce); - } - - @Override - public StringBuffer writeTo(StringBuffer sb) { - writeToWithoutProof(sb, cbind, nonce); - - return StringWritableCsv.writeTo( - sb, - null, // This marks the position of writeToWithoutProof, required for the "," - new ScramAttributeValue(ScramAttributes.CLIENT_PROOF, ScramStringFormatting.base64Encode(proof)) - ); - } - - @Override - public String toString() { - return writeTo(new StringBuffer()).toString(); - } -} diff --git a/common/src/main/java/com/ongres/scram/common/message/ClientFirstMessage.java b/common/src/main/java/com/ongres/scram/common/message/ClientFirstMessage.java deleted file mode 100644 index b2e9291..0000000 --- a/common/src/main/java/com/ongres/scram/common/message/ClientFirstMessage.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * 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 com.ongres.scram.common.message; - - -import com.ongres.scram.common.ScramAttributeValue; -import com.ongres.scram.common.ScramAttributes; -import com.ongres.scram.common.ScramStringFormatting; -import com.ongres.scram.common.exception.ScramParseException; -import com.ongres.scram.common.gssapi.Gs2CbindFlag; -import com.ongres.scram.common.gssapi.Gs2Header; -import com.ongres.scram.common.util.StringWritable; -import com.ongres.scram.common.util.StringWritableCsv; - -import static com.ongres.scram.common.util.Preconditions.checkNotEmpty; -import static com.ongres.scram.common.util.Preconditions.checkNotNull; - - -/** - * Constructs and parses client-first-messages. - * Message contains a {@link Gs2Header}, a username and a nonce. Formal syntax is: - * - * {@code - * client-first-message-bare = [reserved-mext ","] username "," nonce ["," extensions] - client-first-message = gs2-header client-first-message-bare - * } - * - * Note that extensions are not supported. - * - * @see [RFC5802] Section 7 - */ -public class ClientFirstMessage implements StringWritable { - private final Gs2Header gs2Header; - private final String user; - private final String nonce; - - /** - * Constructs a client-first-message for the given user, nonce and gs2Header. - * This constructor is intended to be instantiated by a scram client, and not directly. - * The client should be providing the header, and nonce (and probably the user too). - * @param gs2Header The GSS-API header - * @param user The SCRAM user - * @param nonce The nonce for this session - * @throws IllegalArgumentException If any of the arguments is null or empty - */ - public ClientFirstMessage(Gs2Header gs2Header, String user, String nonce) throws IllegalArgumentException { - this.gs2Header = checkNotNull(gs2Header, "gs2Header"); - this.user = checkNotEmpty(user, "user"); - this.nonce = checkNotEmpty(nonce, "nonce"); - } - - private static Gs2Header gs2Header(Gs2CbindFlag gs2CbindFlag, String authzid, String cbindName) { - checkNotNull(gs2CbindFlag, "gs2CbindFlag"); - if(Gs2CbindFlag.CHANNEL_BINDING_REQUIRED == gs2CbindFlag && null == cbindName) { - throw new IllegalArgumentException("Channel binding name is required if channel binding is specified"); - } - - return new Gs2Header(gs2CbindFlag, cbindName, authzid); - } - - /** - * Constructs a client-first-message for the given parameters. - * Under normal operation, this constructor is intended to be instantiated by a scram client, and not directly. - * However, this constructor is more user- or test-friendly, as the arguments are easier to provide without - * building other indirect object parameters. - * @param gs2CbindFlag The channel-binding flag - * @param authzid The optional authzid - * @param cbindName The optional channel binding name - * @param user The SCRAM user - * @param nonce The nonce for this session - * @throws IllegalArgumentException If the flag, user or nonce are null or empty - */ - public ClientFirstMessage(Gs2CbindFlag gs2CbindFlag, String authzid, String cbindName, String user, String nonce) { - this(gs2Header(gs2CbindFlag, authzid, cbindName), user, nonce); - } - - /** - * Constructs a client-first-message for the given parameters, with no channel binding nor authzid. - * Under normal operation, this constructor is intended to be instantiated by a scram client, and not directly. - * However, this constructor is more user- or test-friendly, as the arguments are easier to provide without - * building other indirect object parameters. - * @param user The SCRAM user - * @param nonce The nonce for this session - * @throws IllegalArgumentException If the user or nonce are null or empty - */ - public ClientFirstMessage(String user, String nonce) { - this(gs2Header(Gs2CbindFlag.CLIENT_NOT, null, null), user, nonce); - } - - public Gs2CbindFlag getChannelBindingFlag() { - return gs2Header.getChannelBindingFlag(); - } - - public boolean isChannelBinding() { - return gs2Header.getChannelBindingFlag() == Gs2CbindFlag.CHANNEL_BINDING_REQUIRED; - } - - public String getChannelBindingName() { - return gs2Header.getChannelBindingName(); - } - - public String getAuthzid() { - return gs2Header.getAuthzid(); - } - - public Gs2Header getGs2Header() { - return gs2Header; - } - - public String getUser() { - return user; - } - - public String getNonce() { - return nonce; - } - - /** - * Limited version of the {@link StringWritableCsv#toString()} method, that doesn't write the GS2 header. - * This method is useful to construct the auth message used as part of the SCRAM algorithm. - * @param sb A StringBuffer where to write the data to. - * @return The same StringBuffer - */ - public StringBuffer writeToWithoutGs2Header(StringBuffer sb) { - return StringWritableCsv.writeTo( - sb, - new ScramAttributeValue(ScramAttributes.USERNAME, ScramStringFormatting.toSaslName(user)), - new ScramAttributeValue(ScramAttributes.NONCE, nonce) - ); - } - - @Override - public StringBuffer writeTo(StringBuffer sb) { - StringWritableCsv.writeTo( - sb, - gs2Header, - null // This marks the position of the rest of the elements, required for the "," - ); - - return writeToWithoutGs2Header(sb); - } - - /** - * Construct a {@link ClientFirstMessage} instance from a message (String) - * @param clientFirstMessage The String representing the client-first-message - * @return The instance - * @throws ScramParseException If the message is not a valid client-first-message - * @throws IllegalArgumentException If the message is null or empty - */ - public static ClientFirstMessage parseFrom(String clientFirstMessage) - throws ScramParseException, IllegalArgumentException { - checkNotEmpty(clientFirstMessage, "clientFirstMessage"); - - Gs2Header gs2Header = Gs2Header.parseFrom(clientFirstMessage); // Takes first two fields - String[] userNonceString; - try { - userNonceString = StringWritableCsv.parseFrom(clientFirstMessage, 2, 2); - } catch (IllegalArgumentException e) { - throw new ScramParseException("Illegal series of attributes in client-first-message", e); - } - - ScramAttributeValue user = ScramAttributeValue.parse(userNonceString[0]); - if(ScramAttributes.USERNAME.getChar() != user.getChar()) { - throw new ScramParseException("user must be the 3rd element of the client-first-message"); - } - - ScramAttributeValue nonce = ScramAttributeValue.parse(userNonceString[1]); - if(ScramAttributes.NONCE.getChar() != nonce.getChar()) { - throw new ScramParseException("nonce must be the 4th element of the client-first-message"); - } - - return new ClientFirstMessage(gs2Header, user.getValue(), nonce.getValue()); - } - - @Override - public String toString() { - return writeTo(new StringBuffer()).toString(); - } -} diff --git a/common/src/main/java/com/ongres/scram/common/message/ServerFinalMessage.java b/common/src/main/java/com/ongres/scram/common/message/ServerFinalMessage.java deleted file mode 100644 index 5961d00..0000000 --- a/common/src/main/java/com/ongres/scram/common/message/ServerFinalMessage.java +++ /dev/null @@ -1,219 +0,0 @@ -/* - * 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 com.ongres.scram.common.message; - - -import static com.ongres.scram.common.util.Preconditions.checkNotEmpty; -import static com.ongres.scram.common.util.Preconditions.checkNotNull; - -import java.util.HashMap; -import java.util.Map; - -import com.ongres.scram.common.ScramAttributeValue; -import com.ongres.scram.common.ScramAttributes; -import com.ongres.scram.common.ScramStringFormatting; -import com.ongres.scram.common.exception.ScramParseException; -import com.ongres.scram.common.util.StringWritable; -import com.ongres.scram.common.util.StringWritableCsv; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - - -/** - * Constructs and parses server-final-messages. Formal syntax is: - * - * {@code - * server-error = "e=" server-error-value - * - * server-error-value = "invalid-encoding" / - * "extensions-not-supported" / ; unrecognized 'm' value - * "invalid-proof" / - * "channel-bindings-dont-match" / - * "server-does-support-channel-binding" / - * ; server does not support channel binding - * "channel-binding-not-supported" / - * "unsupported-channel-binding-type" / - * "unknown-user" / - * "invalid-username-encoding" / - * ; invalid username encoding (invalid UTF-8 or - * ; SASLprep failed) - * "no-resources" / - * "other-error" / - * server-error-value-ext - * ; Unrecognized errors should be treated as "other-error". - * ; In order to prevent information disclosure, the server - * ; may substitute the real reason with "other-error". - * - * server-error-value-ext = value - * ; Additional error reasons added by extensions - * ; to this document. - * - * verifier = "v=" base64 - * ;; base-64 encoded ServerSignature. - * - * server-final-errorMessage = (server-error / verifier) - * ["," extensions] - * } - * - * Note that extensions are not supported (and, consequently, error message extensions). - * - * @see [RFC5802] Section 7 - */ -public class ServerFinalMessage implements StringWritable { - - /** - * Possible error messages sent on a server-final-message. - */ - public enum Error { - INVALID_ENCODING("invalid-encoding"), - EXTENSIONS_NOT_SUPPORTED("extensions-not-supported"), - INVALID_PROOF("invalid-proof"), - CHANNEL_BINDINGS_DONT_MATCH("channel-bindings-dont-match"), - SERVER_DOES_SUPPORT_CHANNEL_BINDING("server-does-support-channel-binding"), - CHANNEL_BINDING_NOT_SUPPORTED("channel-binding-not-supported"), - UNSUPPORTED_CHANNEL_BINDING_TYPE("unsupported-channel-binding-type"), - UNKNOWN_USER("unknown-user"), - INVALID_USERNAME_ENCODING("invalid-username-encoding"), - NO_RESOURCES("no-resources"), - OTHER_ERROR("other-error") - ; - - private static final Map
Example of usage: + * + *
{@code
+ * ScramClient scramClient = ScramClient.builder()
+ * .advertisedMechanisms(Arrays.asList("SCRAM-SHA-256", "SCRAM-SHA-256-PLUS"))
+ * .username("user")
+ * .password("pencil".toCharArray())
+ * .channelBinding("tls-server-end-point", channelBindingData) // client supports channel binding
+ * .build();
+ *
+ * // The build() call negotiates the SCRAM mechanism to be used. In this example,
+ * // since the server advertise support for the SCRAM-SHA-256-PLUS mechanism,
+ * // and the builder is set with the channel binding type and data, the constructed
+ * // scramClient will use the "SCRAM-SHA-256-PLUS" mechanism for authentication.
+ *
+ * // Send the client-first-message ("p=...,,n=...,r=...")
+ * ClientFirstMessage clientFirstMsg = scramClient.clientFirstMessage();
+ * ...
+ * // Receive the server-first-message
+ * ServerFirstMessage serverFirstMsg = scramClient.serverFirstMessage("r=...,s=...,i=...");
+ * ...
+ * // Send the client-final-message ("c=...,r=...,p=...")
+ * ClientFinalMessage clientFinalMsg = scramClient.clientFinalMessage();
+ * ...
+ * // Receive the server-final-message, throw an ScramException on error
+ * ServerFinalMessage serverFinalMsg = scramClient.serverFinalMessage("v=...");
+ * }
+ *
+ * Commonly, a protocol will specify that the server advertises supported and available + * mechanisms to the client via some facility provided by the protocol, and the client will then + * select the "best" mechanism from this list that it supports and finds suitable. + * + *
When building the ScramClient, it provides mechanism negotiation based on parameters, if + * channel binding is missing the client will use {@code "n"} as gs2-cbind-flag, if the channel + * binding is set, but the mechanisms send by the server do not advertise the {@code -PLUS} + * version, it will use {@code "y"} as gs2-cbind-flag, when both client and server support channel + * binding, it will use {@code "p=" cb-name} as gs2-cbind-flag. + * + * @see RFC-5802: Salted Challenge Response + * Authentication Mechanism (SCRAM) SASL and GSS-API Mechanisms + * @see RFC-7677: SCRAM-SHA-256 and + * SCRAM-SHA-256-PLUS Simple Authentication and Security Layer (SASL) Mechanisms + */ +public final class ScramClient implements MessageFlow { + + private final ScramMechanism scramMechanism; + private final Gs2CbindFlag channelBinding; + private final StringPreparation stringPreparation; + private final String username; + private final char[] password; + private final byte[] saltedPassword; + private final byte[] clientKey; + private final byte[] serverKey; + private final String cbindType; + private final byte[] cbindData; + private final String authzid; + private final String nonce; + + private Stage currentState = Stage.NONE; + private ClientFirstMessage clientFirstMessage; + + private ServerFirstProcessor serverFirstProcessor; + + private ClientFinalProcessor clientFinalProcessor; + + /** + * Constructs a SCRAM client, to perform an authentication for a given user. This class can not be + * instantiated directly, use a {@link #builder()} is used instead. + * + * @param builder The Builder used to initialize this client + */ + private ScramClient(@NotNull Builder builder) { + this.channelBinding = builder.channelBinding; + this.scramMechanism = builder.selectedScramMechanism; + this.stringPreparation = builder.stringPreparation; + this.username = builder.username; + this.password = builder.password != null ? builder.password.clone() : null; + this.saltedPassword = builder.saltedPassword; + this.clientKey = builder.clientKey; + this.serverKey = builder.serverKey; + this.nonce = builder.nonce; + this.cbindType = builder.cbindType; + this.cbindData = builder.cbindData; + this.authzid = builder.authzid; + } + + /** + * Returns the scram mechanism negotiated by this SASL client. + * + * @return the SCRAM mechanims selected during the negotiation + */ + public ScramMechanism getScramMechanism() { + return scramMechanism; + } + + /** + * Returns the text representation of a SCRAM {@code client-first-message}. + * + * @apiNote should be the initial call and can be called only once + * @return The {@code client-first-message} + */ + @Override + public ClientFirstMessage clientFirstMessage() { + if (currentState != Stage.NONE) { + throw new IllegalStateException("Invalid state for processing client first message"); + } + this.clientFirstMessage = + new ClientFirstMessage(channelBinding, cbindType, authzid, username, nonce); + this.currentState = Stage.CLIENT_FIRST; + return clientFirstMessage; + } + + /** + * Process the {@code server-first-message}, from its String representation. + * + * @apiNote should be called after {@link #clientFirstMessage()} and can be called only once + * @param serverFirstMessage The {@code server-first-message} + * @throws ScramParseException If the message is not a valid server-first-message + * @throws IllegalArgumentException If the message is null or empty + */ + @Override + public ServerFirstMessage serverFirstMessage(String serverFirstMessage) + throws ScramParseException { + if (currentState != Stage.CLIENT_FIRST) { + throw new IllegalStateException("Invalid state for processing server first message"); + } + checkNotEmpty(serverFirstMessage, "serverFirstMessage"); + this.serverFirstProcessor = + new ServerFirstProcessor(scramMechanism, stringPreparation, serverFirstMessage, nonce, + clientFirstMessage); + this.currentState = Stage.SERVER_FIRST; + return serverFirstProcessor.getServerFirstMessage(); + } + + /** + * Returns the text representation of a SCRAM {@code client-final-message}. + * + * @apiNote should be called after {@link #serverFirstMessage(String)} and can be called only once + * @return The {@code client-final-message} + */ + @Override + public ClientFinalMessage clientFinalMessage() { + if (currentState != Stage.SERVER_FIRST || serverFirstProcessor == null) { + throw new IllegalStateException("Invalid state for processing client final message"); + } + if (password != null) { + this.clientFinalProcessor = serverFirstProcessor.clientFinalProcessor(password); + Arrays.fill(password, (char) 0); // clear password after use + } else if (saltedPassword != null) { + this.clientFinalProcessor = serverFirstProcessor.clientFinalProcessor(saltedPassword); + } else if (clientKey != null && serverKey != null) { + this.clientFinalProcessor = serverFirstProcessor.clientFinalProcessor(clientKey, serverKey); + } + ClientFinalMessage clientFinalMessage = clientFinalProcessor.clientFinalMessage(cbindData); + this.currentState = Stage.CLIENT_FINAL; + return clientFinalMessage; + } + + /** + * Process and verify the {@code server-final-message}, from its String representation. + * + * @apiNote should be called after {@link #clientFinalMessage()} and can be called only once + * @param serverFinalMessage The {@code server-final-message} + * @throws ScramParseException If the message is not a valid + * @throws ScramServerErrorException If the message is an error + * @throws ScramInvalidServerSignatureException If the verification fails + * @throws IllegalArgumentException If the message is null or empty + */ + @Override + public ServerFinalMessage serverFinalMessage(String serverFinalMessage) + throws ScramParseException, ScramServerErrorException, ScramInvalidServerSignatureException { + if (currentState != Stage.CLIENT_FINAL || clientFinalProcessor == null) { + throw new IllegalStateException("Invalid state for processing server final message"); + } + ServerFinalMessage receiveServerFinalMessage = + clientFinalProcessor.receiveServerFinalMessage(serverFinalMessage); + this.currentState = Stage.SERVER_FINAL; + return receiveServerFinalMessage; + } + + /** + * Creates a builder for {@link ScramClient ScramClient} instances. + * + * @return Builder instance to contruct a {@link ScramClient ScramClient} + */ + public static MechanismsBuildStage builder() { + return new Builder(); + } + + /** + * Builder stage for the advertised mechanisms. + */ + public interface MechanismsBuildStage { + + /** + * List of the advertised mechanisms that will be negotiated between the server and the client. + * + * @param scramMechanisms list with the IANA-registered mechanism name of this SASL client + * @return {@code this} builder for use in a chained invocation + */ + UsernameBuildStage advertisedMechanisms(@NotNull Collection<@NotNull String> scramMechanisms); + } + + /** + * Builder stage for the required username. + */ + public interface UsernameBuildStage { + + /** + * Sets the username. + * + * @param username the required username + * @return {@code this} builder for use in a chained invocation + */ + PasswordBuildStage username(@NotNull String username); + } + + /** + * Builder stage for the password (or a ClientKey/ServerKey, or SaltedPassword). + */ + public interface PasswordBuildStage { + + /** + * Sets the password. + * + * @param password the required password + * @return {@code this} builder for use in a chained invocation + */ + FinalBuildStage password(char @NotNull [] password); + + /** + * Sets the SaltedPassword. + * + * @param saltedPassword the required SaltedPassword + * @return {@code this} builder for use in a chained invocation + */ + FinalBuildStage saltedPassword(byte @NotNull [] saltedPassword); + + /** + * Sets the ClientKey/ServerKey. + * + * @param clientKey the required ClientKey + * @param serverKey the required ServerKey + * @return {@code this} builder for use in a chained invocation + */ + FinalBuildStage clientAndServerKey(byte @NotNull [] clientKey, byte @NotNull [] serverKey); + } + + /** + * Builder stage for the optional atributes and the final build() call. + */ + public interface FinalBuildStage { + + /** + * If the client supports channel binding negotiation, this method sets the type and data used + * for channel binding. + * + * @apiNote If {@code cbindType} or {@code cbindData} are null, sets the gs2-cbind-flag to 'n' + * and does not use channel binding. + * + * @param cbindType channel bynding type name + * @param cbindData channel binding data + * @return {@code this} builder for use in a chained invocation + */ + FinalBuildStage channelBinding(@Nullable String cbindType, byte @Nullable [] cbindData); + + /** + * Sets the StringPreparation, is recommended to leave the default SASL_PREPARATION. + * + * @param stringPreparation type of string preparation normalization + * @return {@code this} builder for use in a chained invocation + */ + FinalBuildStage stringPreparation(@NotNull StringPreparation stringPreparation); + + /** + * Sets the authzid. + * + * @param authzid the optional authorization id + * @return {@code this} builder for use in a chained invocation + */ + FinalBuildStage authzid(@NotNull String authzid); + + /** + * Sets a non-default length for the nonce generation. + * + *
The default value is 24. This call overwrites the length used for the client nonce.
+ *
+ * @param length The length of the nonce. Must be positive and greater than 0
+ * @return {@code this} builder for use in a chained invocation
+ * @throws IllegalArgumentException If length is less than 1
+ */
+ FinalBuildStage nonceLength(int length);
+
+ /**
+ * The client will use a default nonce generator, unless an external one is provided by this
+ * method.
+ *
+ * @apiNote you should rely on the default randomly generated nonce instead of this, this call
+ * exists mostly for testing with a predefined nonce
+ * @param nonceSupplier A supplier of valid nonce Strings. Please note that according to the SCRAM RFC only ASCII
+ * printable characters (except the comma, ',') are permitted on a nonce. Length is not
+ * limited.
+ * @return {@code this} builder for use in a chained invocation
+ * @throws IllegalArgumentException If nonceSupplier is null
+ */
+ FinalBuildStage nonceSupplier(@NotNull Supplier<@NotNull String> nonceSupplier);
+
+ /**
+ * Selects a non-default SecureRandom instance, based on the given algorithm and optionally
+ * provider. This SecureRandom instance will be used to generate secure random values, like the
+ * ones required to generate the nonce. Algorithm and provider names are those supported by the
+ * {@link SecureRandom} class.
+ *
+ * @param algorithm The name of the algorithm to use
+ * @param provider The name of the provider of SecureRandom. Might be null
+ * @return {@code this} builder for use in a chained invocation
+ * @throws IllegalArgumentException If algorithm is null, or either the algorithm or provider
+ * are not supported
+ */
+ FinalBuildStage secureRandomAlgorithmProvider(@NotNull String algorithm,
+ @Nullable String provider);
+
+ /**
+ * Returns the fully contructed ScramClient ready to start the message flow with the server.
+ *
+ * @return ScramClient specific for the set of parameters
+ * @throws IllegalArgumentException if any parameter set is invalid
+ */
+ ScramClient build();
+ }
+
+ /**
+ * Builds instances of type {@link ScramClient ScramClient}. Initialize attributes and then invoke
+ * the {@link #build()} method to create an instance.
+ *
+ * @apiNote {@code Builder} is not thread-safe and generally should not be stored in a field or
+ * collection, but instead used immediately to create instances.
+ */
+ static final class Builder
+ implements MechanismsBuildStage, UsernameBuildStage, PasswordBuildStage, FinalBuildStage {
+
+ ScramMechanism selectedScramMechanism;
+ Collection Concrete sub-classes should also provide a static parse(String) creation method.
+ */
+abstract class AbstractCharAttributeValue Not all the available attributes may be available in this implementation.
+ */
+enum ScramAttributes implements CharSupplier {
+
+ /**
+ * This attribute specifies the name of the user whose password is used for authentication (a.k.a.
+ * "authentication identity" [RFC4422]). If the
+ * "a" attribute is not specified (which would normally be the case), this username is also the
+ * identity that will be associated with the connection subsequent to authentication and
+ * authorization.
+ *
+ * The client SHOULD prepare the username using the "SASLprep" profile [RFC4013] of the "stringprep" algorithm [RFC3454] treating it as a query string (i.e.,
+ * unassigned Unicode code points are allowed).
+ *
+ * The characters ',' or '=' in usernames are sent as '=2C' and '=3D' respectively.
+ */
+ USERNAME('n'),
+
+ /**
+ * This is an optional attribute, and is part of the GS2 [RFC5801] bridge between the GSS-API and SASL.
+ * This attribute specifies an authorization identity. A client may include it in its first
+ * message to the server if it wants to authenticate as one user, but subsequently act as a
+ * different user. This is typically used by an administrator to perform some management task on
+ * behalf of another user, or by a proxy in some situations.
+ *
+ * If this attribute is omitted (as it normally would be), the authorization identity is
+ * assumed to be derived from the username specified with the (required) "n" attribute.
+ *
+ * The server always authenticates the user specified by the "n" attribute. If the "a"
+ * attribute specifies a different user, the server associates that identity with the connection
+ * after successful authentication and authorization checks.
+ *
+ * The syntax of this field is the same as that of the "n" field with respect to quoting of '='
+ * and ','.
+ */
+ AUTHZID('a'),
+
+ /**
+ * This attribute specifies a sequence of random printable ASCII characters excluding ',' (which
+ * forms the nonce used as input to the hash function). No quoting is applied to this string.
+ */
+ NONCE('r'),
+
+ /**
+ * This REQUIRED attribute specifies the base64-encoded GS2 header and channel binding data. The
+ * attribute data consist of: {@link java.security.MessageDigest}: "Every implementation of the Java platform is required to
+ * support the following standard MessageDigest algorithms: {@code SHA-1}, {@code SHA-256}".
+ *
+ * {@link javax.crypto.Mac}: "Every implementation of the Java platform is required to support
+ * the following standard Mac algorithms: {@code HmacSHA1}, {@code HmacSHA256}".
+ *
+ * @see SASL
+ * SCRAM Family Mechanisms
+ */
+public enum ScramMechanism {
+
+ /**
+ * SCRAM-SHA-1 mechanism, defined in RFC-5802.
+ */
+ SCRAM_SHA_1("SCRAM-SHA-1", "SHA-1", 160, "HmacSHA1", 4096),
+ /**
+ * SCRAM-SHA-1-PLUS mechanism, defined in RFC-5802.
+ */
+ SCRAM_SHA_1_PLUS("SCRAM-SHA-1-PLUS", "SHA-1", 160, "HmacSHA1", 4096),
+ /**
+ * SCRAM-SHA-224 mechanism, not defined in an RFC.
+ */
+ SCRAM_SHA_224("SCRAM-SHA-224", "SHA-224", 224, "HmacSHA224", 4096),
+ /**
+ * SCRAM-SHA-224-PLUS mechanism, not defined in an RFC.
+ */
+ SCRAM_SHA_224_PLUS("SCRAM-SHA-224-PLUS", "SHA-224", 224, "HmacSHA224", 4096),
+ /**
+ * SCRAM-SHA-256 mechanism, defined in RFC-7677.
+ */
+ SCRAM_SHA_256("SCRAM-SHA-256", "SHA-256", 256, "HmacSHA256", 4096),
+ /**
+ * SCRAM-SHA-256-PLUS mechanism, defined in RFC-7677.
+ */
+ SCRAM_SHA_256_PLUS("SCRAM-SHA-256-PLUS", "SHA-256", 256, "HmacSHA256", 4096),
+ /**
+ * SCRAM-SHA-384 mechanism, not defined in an RFC.
+ */
+ SCRAM_SHA_384("SCRAM-SHA-384", "SHA-384", 384, "HmacSHA384", 4096),
+ /**
+ * SCRAM-SHA-384-PLUS mechanism, not defined in an RFC.
+ */
+ SCRAM_SHA_384_PLUS("SCRAM-SHA-384-PLUS", "SHA-384", 384, "HmacSHA384", 4096),
+ /**
+ * SCRAM-SHA-512 mechanism.
+ */
+ SCRAM_SHA_512("SCRAM-SHA-512", "SHA-512", 512, "HmacSHA512", 10000),
+ /**
+ * SCRAM-SHA-512-PLUS mechanism.
+ */
+ SCRAM_SHA_512_PLUS("SCRAM-SHA-512-PLUS", "SHA-512", 512, "HmacSHA512", 10000);
+
+ private static final @Unmodifiable Map Must be a value registered under IANA: SASL SCRAM
+ * Family Mechanisms
+ *
+ * @return The mechanism name
+ */
+ @NotNull
+ public String getName() {
+ return mechanismName;
+ }
+
+ /**
+ * The mechanism {@code -PLUS} require channel binding.
+ *
+ * @return true if the mechanism requires channel binding
+ */
+ public boolean isPlus() {
+ return channelBinding;
+ }
+
+ /**
+ * Returns the length of the key length of the algorithm.
+ *
+ * @return The length (in bits)
+ */
+ int getKeyLength() {
+ return keyLength;
+ }
+
+ int getIterationCount() {
+ return iterationCount;
+ }
+
+ /**
+ * Calculate a message digest, according to the algorithm of the SCRAM mechanism.
+ *
+ * @param message the message
+ * @return The calculated message digest
+ * @throws ScramRuntimeException If the algorithm is not provided by current JVM or any included
+ * implementations
+ */
+ byte @NotNull [] digest(byte @NotNull [] message) {
+ try {
+ return MessageDigest.getInstance(hashAlgorithmName).digest(message);
+ } catch (NoSuchAlgorithmException e) {
+ throw new ScramRuntimeException(
+ "Hash algorithm " + hashAlgorithmName + " not present in current JVM", e);
+ }
+ }
+
+ /**
+ * Calculate the hmac of a key and a message, according to the algorithm of the SCRAM mechanism.
+ *
+ * @param key the key
+ * @param message the message
+ * @return The calculated message hmac instance
+ * @throws ScramRuntimeException If the algorithm is not provided by current JVM or any included
+ * implementations
+ */
+ byte @NotNull [] hmac(byte @NotNull [] key, byte @NotNull [] message) {
+ try {
+ return CryptoUtil.hmac(new SecretKeySpec(key, hmacAlgorithmName),
+ Mac.getInstance(hmacAlgorithmName), message);
+ } catch (NoSuchAlgorithmException e) {
+ throw new ScramRuntimeException(
+ "HMAC algorithm " + hmacAlgorithmName + " not present in current JVM", e);
+ }
+ }
+
+ /**
+ * Compute the salted password.
+ *
+ * @param stringPreparation Type of preparation to perform in the string
+ * @param password Password used
+ * @param salt Salt used
+ * @param iterationCount Number of iterations
+ * @return The salted password
+ * @throws ScramRuntimeException If the algorithm is not provided by current JVM or any included
+ * implementations
+ */
+ byte @NotNull [] saltedPassword(@NotNull StringPreparation stringPreparation,
+ char @NotNull [] password, byte @NotNull [] salt, int iterationCount) {
+ final char[] normalizedPassword = stringPreparation.normalize(password);
+ try {
+ return CryptoUtil.hi(
+ SecretKeyFactory.getInstance(keyFactoryAlgorithmName),
+ keyLength,
+ normalizedPassword,
+ salt,
+ iterationCount);
+ } catch (NoSuchAlgorithmException ex) {
+ throw new ScramRuntimeException(
+ "Unsupported " + keyFactoryAlgorithmName + " for " + mechanismName, ex);
+ }
+ }
+
+ /**
+ * Gets a SCRAM mechanism given its standard IANA name, supported by the Java security provider.
+ *
+ * @apiNote This will get only the mechanims supported by the Java security provider, if the
+ * configured security provider lacks the algorithm this method will return {@code null}.
+ *
+ * @param name The standard IANA full name of the mechanism.
+ * @return An instance that contains the ScramMechanism if it was found, or null otherwise.
+ */
+ public static @Nullable ScramMechanism byName(@NotNull String name) {
+ return BY_NAME_MAPPING.get(checkNotNull(name, "name"));
+ }
+
+ /**
+ * List all the supported SCRAM mechanisms by this client implementation.
+ *
+ * @return A unmodifiable list of the IANA-registered, SCRAM supported mechanisms
+ */
+ @Unmodifiable
+ public static @NotNull List<@NotNull String> supportedMechanisms() {
+ return Collections.unmodifiableList(SUPPORTED_MECHANISMS);
+ }
+
+ private static boolean isAlgorithmSupported(@NotNull ScramMechanism mechanism) {
+ try {
+ MessageDigest.getInstance(mechanism.hashAlgorithmName);
+ Mac.getInstance(mechanism.hmacAlgorithmName);
+ SecretKeyFactory.getInstance(mechanism.keyFactoryAlgorithmName);
+ return true;
+ } catch (NoSuchAlgorithmException e) {
+ return false;
+ }
+ }
+
+}
diff --git a/scram-common/src/main/java/com/ongres/scram/common/ScramStringFormatting.java b/scram-common/src/main/java/com/ongres/scram/common/ScramStringFormatting.java
new file mode 100644
index 0000000..188e322
--- /dev/null
+++ b/scram-common/src/main/java/com/ongres/scram/common/ScramStringFormatting.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common;
+
+import static com.ongres.scram.common.util.Preconditions.checkNotEmpty;
+import static com.ongres.scram.common.util.Preconditions.checkNotNull;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.util.Base64;
+
+import com.ongres.saslprep.SASLprep;
+import com.ongres.stringprep.Profile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Class with static methods that provide support for converting to/from salNames.
+ *
+ * @see [RFC5802] Section 7: Formal
+ * Syntax
+ */
+final class ScramStringFormatting {
+
+ static final Profile SASL_PREP = new SASLprep();
+
+ private ScramStringFormatting() {
+ throw new IllegalStateException("Utility class");
+ }
+
+ /**
+ * Given a value-safe-char (normalized UTF-8 String), return one where characters ',' and '=' are
+ * represented by '=2C' or '=3D', respectively.
+ *
+ * @param value The value to convert so saslName
+ * @return The saslName, with caracter escaped (if any)
+ */
+ @NotNull
+ static String toSaslName(@NotNull final String value) {
+ if (value.isEmpty()) {
+ return value;
+ }
+
+ final char[] originalChars = SASL_PREP.prepareQuery(value.toCharArray());
+
+ int comma = 0;
+ int equal = 0;
+ // Fast path
+ for (char c : originalChars) {
+ if (',' == c) {
+ comma++;
+ } else if ('=' == c) {
+ equal++;
+ }
+ }
+ if (comma == 0 && equal == 0) {
+ return new String(originalChars);
+ }
+
+ // Replace chars
+ char[] saslChars = new char[originalChars.length + comma * 2 + equal * 2];
+ int i = 0;
+ for (char c : originalChars) {
+ if (',' == c) {
+ saslChars[i++] = '=';
+ saslChars[i++] = '2';
+ saslChars[i++] = 'C';
+ } else if ('=' == c) {
+ saslChars[i++] = '=';
+ saslChars[i++] = '3';
+ saslChars[i++] = 'D';
+ } else {
+ saslChars[i++] = c;
+ }
+ }
+
+ return new String(saslChars);
+ }
+
+ /**
+ * Given a saslName, return a non-escaped String.
+ *
+ * @param value The saslName
+ * @return The saslName, unescaped
+ * @throws IllegalArgumentException If a ',' character is present, or a '=' not followed by either
+ * '2C' or '3D'
+ */
+ @Nullable
+ static String fromSaslName(@Nullable String value) {
+ if (null == value || value.isEmpty()) {
+ return value;
+ }
+
+ int equal = 0;
+ char[] orig = value.toCharArray();
+
+ // Fast path
+ for (int i = 0; i < orig.length; i++) {
+ if (orig[i] == ',') {
+ throw new IllegalArgumentException("Invalid ',' character present in saslName");
+ }
+ if (orig[i] == '=') {
+ equal++;
+ if (i + 2 > orig.length - 1) {
+ throw new IllegalArgumentException("Invalid '=' character present in saslName");
+ }
+ if (!(orig[i + 1] == '2' && orig[i + 2] == 'C'
+ || orig[i + 1] == '3' && orig[i + 2] == 'D')) {
+ throw new IllegalArgumentException(
+ "Invalid char '=" + orig[i + 1] + orig[i + 2] + "' found in saslName");
+ }
+ }
+ }
+ if (equal == 0) {
+ return value;
+ }
+
+ // Replace characters
+ char[] replaced = new char[orig.length - equal * 2];
+
+ for (int r = 0, o = 0; r < replaced.length; r++) {
+ if ('=' == orig[o]) {
+ if (orig[o + 1] == '2' && orig[o + 2] == 'C') {
+ replaced[r] = ',';
+ } else if (orig[o + 1] == '3' && orig[o + 2] == 'D') {
+ replaced[r] = '=';
+ }
+ o += 3;
+ } else {
+ replaced[r] = orig[o];
+ o += 1;
+ }
+ }
+
+ return new String(replaced);
+ }
+
+ static @NotNull String base64Encode(byte @NotNull [] value) {
+ checkNotNull(value, "value");
+ return new String(Base64.getEncoder().encode(value), UTF_8);
+ }
+
+ static byte @NotNull [] base64Decode(@NotNull String value) {
+ checkNotEmpty(value, "value");
+ return Base64.getDecoder().decode(value.getBytes(UTF_8));
+ }
+}
diff --git a/scram-common/src/main/java/com/ongres/scram/common/ServerFinalMessage.java b/scram-common/src/main/java/com/ongres/scram/common/ServerFinalMessage.java
new file mode 100644
index 0000000..a7696a5
--- /dev/null
+++ b/scram-common/src/main/java/com/ongres/scram/common/ServerFinalMessage.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common;
+
+import static com.ongres.scram.common.util.Preconditions.castNonNull;
+import static com.ongres.scram.common.util.Preconditions.checkNotEmpty;
+import static com.ongres.scram.common.util.Preconditions.checkNotNull;
+
+import com.ongres.scram.common.exception.ScramParseException;
+import com.ongres.scram.common.exception.ServerErrorValue;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Constructs and parses {@code server-final-messages}.
+ *
+ * Even though the [RFC5802] is not very
+ * clear about it, this implementation will normalize non-printable US-ASCII characters similarly
+ * to what SASLprep does (i.e., removing them).
+ */
+ NO_PREPARATION {
+ @Override
+ char[] doNormalize(char[] value) {
+ return UsAsciiUtils.toPrintable(value);
+ }
+ },
+ /**
+ * Implementation of StringPreparation that performs {@code SASLprep} preparation. UTF-8 byte
+ * sequences that are prohibited by the SASLprep algorithm will produce an exception.
+ */
+ SASL_PREPARATION {
+ @Override
+ char[] doNormalize(char[] value) {
+ return ScramStringFormatting.SASL_PREP.prepareStored(value);
+ }
+ },
+ /**
+ * Implementation of StringPreparation that performs {@code SASLprep} preparation for PostgreSQL.
+ *
+ * Is used by the client to verify that the server has access to the user's authentication
+ * information.
+ */
+public class ScramInvalidServerSignatureException extends ScramException {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructs a new instance of ScramInvalidServerSignatureException with a detailed message.
+ *
+ * @param detail A String containing details about the exception
+ */
+ public ScramInvalidServerSignatureException(String detail) {
+ super(detail);
+ }
+
+ /**
+ * Constructs a new instance of ScramInvalidServerSignatureException with a detailed message and a
+ * root cause.
+ *
+ * @param detail A String containing details about the exception
+ * @param ex The root exception
+ */
+ public ScramInvalidServerSignatureException(String detail, Throwable ex) {
+ super(detail, ex);
+ }
+}
diff --git a/scram-common/src/main/java/com/ongres/scram/common/exception/ScramParseException.java b/scram-common/src/main/java/com/ongres/scram/common/exception/ScramParseException.java
new file mode 100644
index 0000000..64e19e6
--- /dev/null
+++ b/scram-common/src/main/java/com/ongres/scram/common/exception/ScramParseException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common.exception;
+
+/**
+ * This class represents an error when parsing SCRAM messages.
+ */
+public class ScramParseException extends ScramException {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructs a new instance of ScramParseException with a detailed message.
+ *
+ * @param detail A String containing details about the exception
+ */
+ public ScramParseException(String detail) {
+ super(detail);
+ }
+
+ /**
+ * Constructs a new instance of ScramParseException with a detailed message and a root cause.
+ *
+ * @param detail A String containing details about the exception
+ * @param ex The root exception
+ */
+ public ScramParseException(String detail, Throwable ex) {
+ super(detail, ex);
+ }
+}
diff --git a/scram-common/src/main/java/com/ongres/scram/common/exception/ScramRuntimeException.java b/scram-common/src/main/java/com/ongres/scram/common/exception/ScramRuntimeException.java
new file mode 100644
index 0000000..42a77c8
--- /dev/null
+++ b/scram-common/src/main/java/com/ongres/scram/common/exception/ScramRuntimeException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common.exception;
+
+/**
+ * This class represents an error when using SCRAM, which is a SASL method.
+ */
+public class ScramRuntimeException extends RuntimeException {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructs a new instance of ScramRuntimeException with a detailed message.
+ *
+ * @param detail A String containing details about the exception
+ */
+ public ScramRuntimeException(String detail) {
+ super(detail);
+ }
+
+ /**
+ * Constructs a new instance of ScramRuntimeException with a detailed message and a root cause.
+ *
+ * @param detail A String containing details about the exception
+ * @param ex The root exception
+ */
+ public ScramRuntimeException(String detail, Throwable ex) {
+ super(detail, ex);
+ }
+}
diff --git a/scram-common/src/main/java/com/ongres/scram/common/exception/ScramServerErrorException.java b/scram-common/src/main/java/com/ongres/scram/common/exception/ScramServerErrorException.java
new file mode 100644
index 0000000..1d055ca
--- /dev/null
+++ b/scram-common/src/main/java/com/ongres/scram/common/exception/ScramServerErrorException.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common.exception;
+
+/**
+ * This class specifies an error that occurred during authentication exchange in a
+ * {@code server-final-message}.
+ *
+ * It is sent by the server in its {@code server-final-message} and can help diagnose the reason
+ * for the authentication exchange failure.
+ */
+public class ScramServerErrorException extends ScramException {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * {@code server-error-value}.
+ */
+ private final String serverError;
+
+ /**
+ * Constructs a new instance of ScramServerErrorException with a detailed message.
+ *
+ * @param serverError The SCRAM error in the message
+ */
+ public ScramServerErrorException(String serverError) {
+ super(ServerErrorValue.getErrorMessage(serverError));
+ this.serverError = serverError;
+ }
+
+ /**
+ * Constructs a new instance of ScramServerErrorException with a detailed message and a root
+ * cause.
+ *
+ * @param serverError The SCRAM error in the message
+ * @param ex The root exception
+ */
+ public ScramServerErrorException(String serverError, Throwable ex) {
+ super(ServerErrorValue.getErrorMessage(serverError), ex);
+ this.serverError = serverError;
+ }
+
+ /**
+ * Return the "e=" server-error-value from the server-final-message.
+ *
+ * @return the error type returned in the server-final-message
+ */
+ public String getServerError() {
+ return serverError;
+ }
+}
diff --git a/scram-common/src/main/java/com/ongres/scram/common/exception/ServerErrorValue.java b/scram-common/src/main/java/com/ongres/scram/common/exception/ServerErrorValue.java
new file mode 100644
index 0000000..fc7c8f6
--- /dev/null
+++ b/scram-common/src/main/java/com/ongres/scram/common/exception/ServerErrorValue.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common.exception;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * This attribute specifies an error that occurred during authentication exchange. It is sent by the
+ * server in its final message and can help diagnose the reason for the authentication exchange
+ * failure.
+ */
+public final class ServerErrorValue {
+
+ private static final ConcurrentMap The TLS server's certificate bytes need to be hashed with SHA-256 if its signature algorithm
+ * is MD5 or SHA-1 as per RFC 5929 (https://tools.ietf.org/html/rfc5929#section-4.1). If something
+ * else is used, the same hash as the signature algorithm is used.
+ *
+ * @param signatureAlgorithm the signature algorithm name for the certificate signature algorithm
+ * @return the MessageDigest algorithm, or {@code null} if the name is not recognized
+ * @see The tls-server-end-point
+ * Channel Binding Type
+ */
+ private static MessageDigest getDigestAlgorithm(final String signatureAlgorithm) {
+ int index = signatureAlgorithm.indexOf("with");
+ String algorithm = index > 0 ? signatureAlgorithm.substring(0, index) : "SHA-256";
+ // if the certificate's signatureAlgorithm uses a single hash
+ // function and that hash function neither MD5 nor SHA-1, then use
+ // the hash function associated with the certificate's signatureAlgorithm.
+ if (!algorithm.startsWith("SHA3-")) {
+ algorithm = algorithm.replace("SHA", "SHA-");
+ }
+ // if the certificate's signatureAlgorithm uses a single hash
+ // function, and that hash function is either MD5 [RFC1321] or SHA-1
+ // [RFC3174], then use SHA-256 [FIPS-180-3]
+ if ("MD5".equals(algorithm) || "SHA-1".equals(algorithm)) {
+ algorithm = "SHA-256";
+ }
+
+ try {
+ return MessageDigest.getInstance(algorithm);
+ } catch (NoSuchAlgorithmException e) {
+ return null;
+ }
+ }
+
+ /**
+ * The hash of the TLS server's certificate [RFC5280] as it appears, octet for octet, in the
+ * server's Certificate message. Note that the Certificate message contains a certificate_list, in
+ * which the first element is the server's certificate.
+ *
+ * The TLS server's certificate bytes need to be hashed with SHA-256 if its signature algorithm
+ * is MD5 or SHA-1 as per RFC 5929 (https://tools.ietf.org/html/rfc5929#section-4.1). If something
+ * else is used, the same hash as the signature algorithm is used.
+ *
+ * @param serverCert the TLS server's peer certificate
+ * @return the hash of the TLS server's peer certificate
+ * @throws CertificateEncodingException if an encoding error occurs.
+ */
+ public static byte @NotNull [] getChannelBindingData(final @NotNull X509Certificate serverCert)
+ throws CertificateEncodingException {
+ MessageDigest digestAlgorithm = getDigestAlgorithm(serverCert.getSigAlgName());
+ if (digestAlgorithm == null) {
+ return new byte[0];
+ }
+ return digestAlgorithm.digest(serverCert.getEncoded());
+ }
+
+}
diff --git a/scram-common/src/main/java9/module-info.java b/scram-common/src/main/java9/module-info.java
new file mode 100644
index 0000000..f114de7
--- /dev/null
+++ b/scram-common/src/main/java9/module-info.java
@@ -0,0 +1,11 @@
+/*
+ * Copyright (c) 2024 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+module com.ongres.scram.common {
+ requires transitive com.ongres.saslprep;
+ exports com.ongres.scram.common;
+ exports com.ongres.scram.common.exception;
+ exports com.ongres.scram.common.util;
+}
\ No newline at end of file
diff --git a/scram-common/src/test/java/com/ongres/scram/JarFileCheckIT.java b/scram-common/src/test/java/com/ongres/scram/JarFileCheckIT.java
new file mode 100644
index 0000000..813c84f
--- /dev/null
+++ b/scram-common/src/test/java/com/ongres/scram/JarFileCheckIT.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2024 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.module.ModuleDescriptor;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+class JarFileCheckIT {
+
+ private static JarFile jarFile;
+ private static Path buildJarPath;
+
+ @BeforeAll
+ static void beforeAll() throws IOException {
+ buildJarPath = Paths.get(System.getProperty("buildJar"));
+ assertTrue(Files.exists(buildJarPath));
+ jarFile = new JarFile(buildJarPath.toFile(), true);
+ }
+
+ @AfterAll
+ static void afterAll() throws IOException {
+ jarFile.close();
+ }
+
+ @Test
+ void checkLicense() throws IOException {
+ JarEntry jarLicense = jarFile.getJarEntry("META-INF/LICENSE");
+ assertNotNull(jarLicense, "LICENSE file should be present in the final JAR file");
+ try (InputStream is = jarFile.getInputStream(jarLicense);
+ BufferedReader reader = new BufferedReader(new InputStreamReader(is, UTF_8))) {
+ String line = reader.readLine();
+ assertEquals("Copyright (c) 2017 OnGres, Inc.", line);
+ }
+ }
+
+ @Test
+ void checkMultiReleaseManifest() throws IOException {
+ Attributes mainAttributes = jarFile.getManifest().getMainAttributes();
+ String multiReleaseValue = mainAttributes.getValue(new Attributes.Name("Multi-Release"));
+ assertNotNull(multiReleaseValue);
+ assertEquals("true", multiReleaseValue);
+ }
+
+ @Test
+ void checkModuleInfoPresent() throws IOException {
+ JarEntry jarModuleInfo = jarFile.getJarEntry("META-INF/versions/9/module-info.class");
+ ModuleDescriptor moduleDescriptor = ModuleDescriptor.read(jarFile.getInputStream(jarModuleInfo));
+ assertNotNull(moduleDescriptor);
+ assertEquals("com.ongres.scram.common", moduleDescriptor.name());
+ }
+}
diff --git a/scram-common/src/test/java/com/ongres/scram/common/ClientFinalMessageTest.java b/scram-common/src/test/java/com/ongres/scram/common/ClientFinalMessageTest.java
new file mode 100644
index 0000000..324ba73
--- /dev/null
+++ b/scram-common/src/test/java/com/ongres/scram/common/ClientFinalMessageTest.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+class ClientFinalMessageTest {
+
+ @Test
+ void writeToWithoutProofValid() {
+ StringBuilder sb = ClientFinalMessage.withoutProof(new StringBuilder(),
+ new Gs2Header(Gs2CbindFlag.CLIENT_NOT), null, RfcExampleSha1.FULL_NONCE);
+
+ assertEquals(RfcExampleSha1.CLIENT_FINAL_MESSAGE_WITHOUT_PROOF, sb.toString());
+ }
+
+}
diff --git a/scram-common/src/test/java/com/ongres/scram/common/ClientFirstMessageTest.java b/scram-common/src/test/java/com/ongres/scram/common/ClientFirstMessageTest.java
new file mode 100644
index 0000000..524fba1
--- /dev/null
+++ b/scram-common/src/test/java/com/ongres/scram/common/ClientFirstMessageTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common;
+
+import static com.ongres.scram.common.RfcExampleSha1.CLIENT_NONCE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.ongres.scram.common.exception.ScramParseException;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+class ClientFirstMessageTest {
+
+ @ParameterizedTest
+ @CsvSource(value = {"CLIENT_NOT,cbind,,user,nonce",
+ "CHANNEL_BINDING_REQUIRED,,,user,nonce",
+ "CHANNEL_BINDING_REQUIRED,tls-*,,user,nonce",
+ "CLIENT_YES_SERVER_NOT,,,,nonce",
+ "CLIENT_YES_SERVER_NOT,,,'',nonce",
+ "CHANNEL_BINDING_REQUIRED,tls-export,,u,",
+ "CHANNEL_BINDING_REQUIRED,tls-export,,u,''"})
+ void constructorTestInvalid(@NotNull Gs2CbindFlag flag, String cbName, String authzid,
+ @NotNull String user, @NotNull String nonce) {
+ assertThrows(IllegalArgumentException.class,
+ () -> new ClientFirstMessage(flag, cbName, authzid, user, nonce));
+ }
+
+ @Test
+ void writeToValidValues() {
+ assertEquals("n,,n=user,r=fyko",
+ new ClientFirstMessage("user", "fyko").toString());
+ assertEquals("y,,n=user,r=fyko",
+ new ClientFirstMessage(Gs2CbindFlag.CLIENT_YES_SERVER_NOT, null, null, "user", "fyko")
+ .toString());
+ assertEquals("p=tls-server-end-point,,n=user,r=fyko",
+ new ClientFirstMessage(Gs2CbindFlag.CHANNEL_BINDING_REQUIRED, "tls-server-end-point", null,
+ "user", "fyko").toString());
+ assertEquals("p=tls-server-end-point,a=authzid,n=user,r=fyko",
+ new ClientFirstMessage(Gs2CbindFlag.CHANNEL_BINDING_REQUIRED, "tls-server-end-point",
+ "authzid", "user", "fyko").toString());
+ }
+
+ @Test
+ void parseToValidValues() throws ScramParseException {
+ assertEquals(ClientFirstMessage.parseFrom("n,,n=user,r=fyko").toString(),
+ new ClientFirstMessage("user", "fyko").toString());
+ assertEquals(ClientFirstMessage.parseFrom("y,,n=user,r=fyko").toString(),
+ new ClientFirstMessage(Gs2CbindFlag.CLIENT_YES_SERVER_NOT, null, null, "user", "fyko")
+ .toString());
+ assertEquals(ClientFirstMessage.parseFrom("p=tls-server-end-point,,n=user,r=fyko").toString(),
+ new ClientFirstMessage(Gs2CbindFlag.CHANNEL_BINDING_REQUIRED, "tls-server-end-point", null,
+ "user", "fyko").toString());
+ assertEquals(
+ ClientFirstMessage.parseFrom("p=tls-server-end-point,a=authzid,n=user,r=fyko").toString(),
+ new ClientFirstMessage(Gs2CbindFlag.CHANNEL_BINDING_REQUIRED, "tls-server-end-point",
+ "authzid", "user", "fyko").toString());
+ }
+
+ @Test
+ void parseFromValidValues() throws ScramParseException {
+ ClientFirstMessage m1 = ClientFirstMessage.parseFrom("n,,n=user,r=" + CLIENT_NONCE);
+ assertFalse(m1.isChannelBindingRequired());
+ assertSame(Gs2CbindFlag.CLIENT_NOT, m1.getGs2Header().getChannelBindingFlag());
+ assertNull(m1.getGs2Header().getChannelBindingName());
+ assertNull(m1.getGs2Header().getAuthzid());
+ assertEquals("user", m1.getUsername());
+ assertEquals(CLIENT_NONCE, m1.getClientNonce());
+
+ ClientFirstMessage m2 = ClientFirstMessage.parseFrom("y,,n=user,r=" + CLIENT_NONCE);
+ assertTrue(
+ !m2.isChannelBindingRequired()
+ && m2.getGs2Header().getChannelBindingFlag() == Gs2CbindFlag.CLIENT_YES_SERVER_NOT
+ && null == m2.getGs2Header().getAuthzid() && "user".equals(m2.getUsername())
+ && CLIENT_NONCE.equals(m2.getClientNonce()));
+
+ ClientFirstMessage m3 = ClientFirstMessage.parseFrom("y,a=user2,n=user,r=" + CLIENT_NONCE);
+ assertTrue(
+ !m3.isChannelBindingRequired()
+ && m3.getGs2Header().getChannelBindingFlag() == Gs2CbindFlag.CLIENT_YES_SERVER_NOT
+ && null != m3.getGs2Header().getAuthzid()
+ && "user2".equals(m3.getGs2Header().getAuthzid())
+ && "user".equals(m3.getUsername()) && CLIENT_NONCE.equals(m3.getClientNonce()));
+
+ ClientFirstMessage m4 =
+ ClientFirstMessage.parseFrom("p=tls-unique,a=user2,n=user,r=" + CLIENT_NONCE);
+ assertTrue(m4.isChannelBindingRequired());
+ assertSame(Gs2CbindFlag.CHANNEL_BINDING_REQUIRED, m4.getGs2Header().getChannelBindingFlag());
+ assertNotNull(m4.getGs2Header().getChannelBindingName());
+ assertEquals("tls-unique", m4.getGs2Header().getChannelBindingName());
+ assertNotNull(m4.getGs2Header().getAuthzid());
+ assertEquals("user2", m4.getGs2Header().getAuthzid());
+ assertEquals("user", m4.getUsername());
+ assertEquals(CLIENT_NONCE, m4.getClientNonce());
+ }
+
+ @Test
+ void parseFromInvalidValues() {
+ String[] invalidValues = new String[] {
+ "n,,r=user,r=" + CLIENT_NONCE, "n,,z=user,r=" + CLIENT_NONCE, "n,,n=user", "n,", "n,,",
+ "n,,n=user,r", "n,,n=user,r="
+ };
+
+ int n = 0;
+ for (String s : invalidValues) {
+ try {
+ assertNotNull(ClientFirstMessage.parseFrom(s));
+ } catch (ScramParseException e) {
+ n++;
+ }
+ }
+
+ assertEquals(invalidValues.length, n);
+ }
+}
diff --git a/scram-common/src/test/java/com/ongres/scram/common/Gs2AttributeValueTest.java b/scram-common/src/test/java/com/ongres/scram/common/Gs2AttributeValueTest.java
new file mode 100644
index 0000000..72d30ef
--- /dev/null
+++ b/scram-common/src/test/java/com/ongres/scram/common/Gs2AttributeValueTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.ongres.scram.common.Gs2AttributeValue;
+import com.ongres.scram.common.Gs2Attributes;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.junit.jupiter.params.provider.NullSource;
+
+class Gs2AttributeValueTest {
+
+ @ParameterizedTest
+ @NullSource
+ @EnumSource(value = Gs2Attributes.class, names = {"CHANNEL_BINDING_REQUIRED", "AUTHZID"})
+ void constructorNotAllowsNullValuesForCbAuthzId(Gs2Attributes attrib) {
+ assertThrows(IllegalArgumentException.class,
+ () -> new Gs2AttributeValue(attrib, null));
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = Gs2Attributes.class, names = {"CLIENT_NOT", "CLIENT_YES_SERVER_NOT"})
+ void constructorAllowsNullValuesForClientnClienty(Gs2Attributes attrib) {
+ Gs2AttributeValue gs2 = assertDoesNotThrow(() -> new Gs2AttributeValue(attrib, null));
+ assertNotNull(gs2);
+ }
+
+ @Test
+ void parseIllegalValuesStructure() {
+ String[] values =
+ new String[] {"", "as", "asdfjkl", Gs2Attributes.CHANNEL_BINDING_REQUIRED.getChar() + "="};
+ int n = 0;
+ for (String value : values) {
+ try {
+ assertNotNull(Gs2AttributeValue.parse(value));
+ } catch (IllegalArgumentException e) {
+ n++;
+ }
+ }
+
+ assertEquals(values.length, n, "Not every illegal value thrown IllegalArgumentException");
+ }
+
+ @Test
+ void parseIllegalValuesInvalidGS2Attibute() {
+ String[] values = new String[] {"z=asdfasdf", "i=value"};
+
+ int n = 0;
+ for (String value : values) {
+ try {
+ assertNotNull(Gs2AttributeValue.parse(value));
+ } catch (IllegalArgumentException e) {
+ n++;
+ }
+ }
+
+ assertEquals(values.length, n, "Not every illegal value thrown IllegalArgumentException");
+ }
+
+ @Test
+ void parseLegalValues() {
+ String[] values = new String[] {"n", "y", "p=value", "a=authzid"};
+ for (String value : values) {
+ assertNotNull(Gs2AttributeValue.parse(value));
+ }
+ }
+}
diff --git a/scram-common/src/test/java/com/ongres/scram/common/Gs2HeaderTest.java b/scram-common/src/test/java/com/ongres/scram/common/Gs2HeaderTest.java
new file mode 100644
index 0000000..6e0574e
--- /dev/null
+++ b/scram-common/src/test/java/com/ongres/scram/common/Gs2HeaderTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.ongres.scram.common.Gs2CbindFlag;
+import com.ongres.scram.common.Gs2Header;
+import org.junit.jupiter.api.Test;
+
+class Gs2HeaderTest {
+ private static final String[] VALID_GS2HEADER_STRINGS = new String[] {
+ "n,", "y,", "n,a=blah", "p=tls-server-end-point,", "p=tls-server-end-point,a=b"
+ };
+ private static final Gs2Header[] VALID_GS_2_HEADERS = new Gs2Header[] {
+ new Gs2Header(Gs2CbindFlag.CLIENT_NOT),
+ new Gs2Header(Gs2CbindFlag.CLIENT_YES_SERVER_NOT),
+ new Gs2Header(Gs2CbindFlag.CLIENT_NOT, null, "blah"),
+ new Gs2Header(Gs2CbindFlag.CHANNEL_BINDING_REQUIRED, "tls-server-end-point"),
+ new Gs2Header(Gs2CbindFlag.CHANNEL_BINDING_REQUIRED, "tls-server-end-point", "b")
+ };
+
+ private void assertGS2Header(String expected, Gs2Header gs2Header) {
+ assertEquals(expected, gs2Header.writeTo(new StringBuilder()).toString());
+ }
+
+ @Test
+ void constructorValid() {
+ for (int i = 0; i < VALID_GS2HEADER_STRINGS.length; i++) {
+ assertGS2Header(VALID_GS2HEADER_STRINGS[i], VALID_GS_2_HEADERS[i]);
+ }
+ }
+
+ @Test
+ void constructorInvalid1() {
+ assertThrows(IllegalArgumentException.class,
+ () -> new Gs2Header(Gs2CbindFlag.CHANNEL_BINDING_REQUIRED, null));
+ }
+
+ @Test
+ void constructorInvalid2() {
+ assertThrows(IllegalArgumentException.class,
+ () -> new Gs2Header(Gs2CbindFlag.CLIENT_NOT, "blah"));
+ }
+
+ @Test
+ void constructorInvalid3() {
+ assertThrows(IllegalArgumentException.class,
+ () -> new Gs2Header(Gs2CbindFlag.CLIENT_YES_SERVER_NOT, "blah"));
+ }
+
+ @Test
+ void constructorInvalid4() {
+ assertThrows(IllegalArgumentException.class,
+ () -> new Gs2Header(Gs2CbindFlag.CHANNEL_BINDING_REQUIRED, null, "b"));
+ }
+
+ @Test
+ void parseFromInvalid() {
+ String[] invalids = new String[] {"Z,", "n,Z=blah", "p,", "n=a,"};
+ int n = 0;
+ for (String invalid : invalids) {
+ try {
+ Gs2Header.parseFrom(invalid);
+ } catch (IllegalArgumentException e) {
+ n++;
+ }
+ }
+
+ assertEquals(invalids.length, n);
+ }
+
+ @Test
+ void parseFromValid() {
+ for (int i = 0; i < VALID_GS2HEADER_STRINGS.length; i++) {
+ assertGS2Header(
+ VALID_GS_2_HEADERS[i].writeTo(new StringBuilder()).toString(),
+ Gs2Header.parseFrom(VALID_GS2HEADER_STRINGS[i]));
+ }
+ }
+}
diff --git a/scram-common/src/test/java/com/ongres/scram/common/RfcExampleSha1.java b/scram-common/src/test/java/com/ongres/scram/common/RfcExampleSha1.java
new file mode 100644
index 0000000..2e7aff0
--- /dev/null
+++ b/scram-common/src/test/java/com/ongres/scram/common/RfcExampleSha1.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common;
+
+/**
+ * Constants for examples of the RFC for SHA-1 tests.
+ */
+public class RfcExampleSha1 {
+ public static final String USER = "user";
+ public static final String PASSWORD = "pencil";
+ public static final String CLIENT_NONCE = "fyko+d2lbbFgONRv9qkxdawL";
+ public static final String CLIENT_FIRST_MESSAGE_WITHOUT_GS2_HEADER =
+ "n=" + USER + ",r=" + CLIENT_NONCE;
+ public static final String CLIENT_FIRST_MESSAGE =
+ "n,," + CLIENT_FIRST_MESSAGE_WITHOUT_GS2_HEADER;
+ public static final String SERVER_SALT = "QSXCR+Q6sek8bf92";
+ public static final int SERVER_ITERATIONS = 4096;
+ public static final String SERVER_NONCE = "3rfcNHYJY1ZVvWVs7j";
+ public static final String FULL_NONCE = CLIENT_NONCE + SERVER_NONCE;
+ public static final String SERVER_FIRST_MESSAGE = "r=" + FULL_NONCE + ",s=" + SERVER_SALT
+ + ",i=" + SERVER_ITERATIONS;
+ public static final String GS2_HEADER_BASE64 = "biws";
+ public static final String CLIENT_FINAL_MESSAGE_WITHOUT_PROOF = "c=" + GS2_HEADER_BASE64
+ + ",r=" + FULL_NONCE;
+ public static final String AUTH_MESSAGE = CLIENT_FIRST_MESSAGE_WITHOUT_GS2_HEADER + ","
+ + SERVER_FIRST_MESSAGE + ","
+ + CLIENT_FINAL_MESSAGE_WITHOUT_PROOF;
+ public static final String CLIENT_FINAL_MESSAGE_PROOF = "v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=";
+ public static final String CLIENT_FINAL_MESSAGE = CLIENT_FINAL_MESSAGE_WITHOUT_PROOF
+ + ",p=" + CLIENT_FINAL_MESSAGE_PROOF;
+ public static final String SERVER_FINAL_MESSAGE = "v=rmF9pqV8S7suAoZWja4dJRkFsKQ=";
+}
diff --git a/scram-common/src/test/java/com/ongres/scram/common/RfcExampleSha256.java b/scram-common/src/test/java/com/ongres/scram/common/RfcExampleSha256.java
new file mode 100644
index 0000000..4036d4d
--- /dev/null
+++ b/scram-common/src/test/java/com/ongres/scram/common/RfcExampleSha256.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common;
+
+/**
+ * Constants for examples of the RFC for SHA-256 tests.
+ */
+public class RfcExampleSha256 {
+ public static final String USER = "user";
+ public static final String PASSWORD = "pencil";
+ public static final String CLIENT_NONCE = "rOprNGfwEbeRWgbNEkqO";
+ public static final String CLIENT_FIRST_MESSAGE_WITHOUT_GS2_HEADER =
+ "n=" + USER + ",r=" + CLIENT_NONCE;
+ public static final String CLIENT_FIRST_MESSAGE =
+ "n," + "," + CLIENT_FIRST_MESSAGE_WITHOUT_GS2_HEADER;
+ public static final String SERVER_SALT = "W22ZaJ0SNY7soEsUEjb6gQ==";
+ public static final int SERVER_ITERATIONS = 4096;
+ public static final String SERVER_NONCE = "%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0";
+ public static final String FULL_NONCE = CLIENT_NONCE + SERVER_NONCE;
+ public static final String SERVER_FIRST_MESSAGE = "r=" + FULL_NONCE + ",s=" + SERVER_SALT
+ + ",i=" + SERVER_ITERATIONS;
+ public static final String GS2_HEADER_BASE64 = "biws";
+ public static final String CLIENT_FINAL_MESSAGE_WITHOUT_PROOF = "c=" + GS2_HEADER_BASE64
+ + ",r=" + FULL_NONCE;
+ public static final String AUTH_MESSAGE = CLIENT_FIRST_MESSAGE_WITHOUT_GS2_HEADER + ","
+ + SERVER_FIRST_MESSAGE + ","
+ + CLIENT_FINAL_MESSAGE_WITHOUT_PROOF;
+ public static final String CLIENT_FINAL_MESSAGE_PROOF =
+ "dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=";
+ public static final String CLIENT_FINAL_MESSAGE = CLIENT_FINAL_MESSAGE_WITHOUT_PROOF
+ + ",p=" + CLIENT_FINAL_MESSAGE_PROOF;
+ public static final String SERVER_FINAL_MESSAGE =
+ "v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=";
+}
diff --git a/scram-common/src/test/java/com/ongres/scram/common/SaslPrepTest.java b/scram-common/src/test/java/com/ongres/scram/common/SaslPrepTest.java
new file mode 100644
index 0000000..0540df2
--- /dev/null
+++ b/scram-common/src/test/java/com/ongres/scram/common/SaslPrepTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+
+import com.ongres.stringprep.Profile;
+import com.ongres.stringprep.Stringprep;
+import org.junit.jupiter.api.Test;
+
+class SaslPrepTest {
+
+ private static final Profile saslPrep = Stringprep.getProvider("SASLprep");
+
+ @Test
+ void rfc4013Examples() throws IOException {
+ // Taken from https://tools.ietf.org/html/rfc4013#section-3
+ assertEquals("IX", saslPrep.prepareStored("I\u00ADX"));
+ assertEquals("user", saslPrep.prepareStored("user"));
+ assertEquals("USER", saslPrep.prepareStored("USER"));
+ assertEquals("a", saslPrep.prepareStored("\u00AA"));
+ assertEquals("IX", saslPrep.prepareStored("\u2168"));
+ try {
+ saslPrep.prepareStored("\u0007");
+ fail("Should throw IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ assertEquals("Prohibited ASCII control \"0x0007\"", e.getMessage());
+ }
+ try {
+ saslPrep.prepareStored("\u0627\u0031");
+ fail("Should thow IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ assertEquals("RandALCat character is not the first and the last character", e.getMessage());
+ }
+ }
+}
diff --git a/scram-common/src/test/java/com/ongres/scram/common/ScramAttributeValueTest.java b/scram-common/src/test/java/com/ongres/scram/common/ScramAttributeValueTest.java
new file mode 100644
index 0000000..a378a86
--- /dev/null
+++ b/scram-common/src/test/java/com/ongres/scram/common/ScramAttributeValueTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common;
+
+import static com.ongres.scram.common.RfcExampleSha1.CLIENT_FINAL_MESSAGE_PROOF;
+import static com.ongres.scram.common.RfcExampleSha1.CLIENT_NONCE;
+import static com.ongres.scram.common.RfcExampleSha1.FULL_NONCE;
+import static com.ongres.scram.common.RfcExampleSha1.GS2_HEADER_BASE64;
+import static com.ongres.scram.common.RfcExampleSha1.SERVER_FINAL_MESSAGE;
+import static com.ongres.scram.common.RfcExampleSha1.SERVER_ITERATIONS;
+import static com.ongres.scram.common.RfcExampleSha1.SERVER_SALT;
+import static com.ongres.scram.common.RfcExampleSha1.USER;
+import static com.ongres.scram.common.ScramAttributes.CLIENT_PROOF;
+import static com.ongres.scram.common.ScramAttributes.USERNAME;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.ongres.scram.common.exception.ScramParseException;
+import org.junit.jupiter.api.Test;
+
+class ScramAttributeValueTest {
+
+ private static final Object NULL = null; // Skip validation for null analisys
+
+ @Test
+ void constructorDoesNotAllowNullValue() {
+ assertThrows(IllegalArgumentException.class,
+ () -> new ScramAttributeValue(USERNAME, (String) NULL),
+ "A null value must throw an IllegalArgumentException");
+ }
+
+ @Test
+ void parseIllegalValuesStructure() {
+ String[] values = new String[] {
+ null, "", "asdf", "asdf=a", CLIENT_PROOF.getChar() + "=", CLIENT_PROOF.getChar() + ",a"
+ };
+ int n = 0;
+ for (String value : values) {
+ try {
+ assertNotNull(ScramAttributeValue.parse(value));
+ } catch (ScramParseException e) {
+ n++;
+ }
+ }
+
+ assertEquals(values.length, n, "Not every illegal value thrown ScramParseException");
+ }
+
+ @Test
+ void parseIllegalValuesInvalidSCRAMAttibute() {
+ // SCRAM allows for extensions. If a new attribute is supported and its value has been used
+ // below,
+ // test will fail and will need to be fixed
+ String[] values = new String[] {"z=asdfasdf", "!=value"};
+
+ for (String value : values) {
+ assertThrows(ScramParseException.class, () -> ScramAttributeValue.parse(value));
+ }
+ }
+
+ @Test
+ void parseLegalValues() throws ScramParseException {
+ String[] legalValues = new String[] {
+ CLIENT_PROOF.getChar() + "=" + "proof",
+ USERNAME.getChar() + "=" + "username",
+ "n=" + USER,
+ "r=" + CLIENT_NONCE,
+ "r=" + FULL_NONCE,
+ "s=" + SERVER_SALT,
+ "i=" + SERVER_ITERATIONS,
+ "c=" + GS2_HEADER_BASE64,
+ "p=" + CLIENT_FINAL_MESSAGE_PROOF,
+ SERVER_FINAL_MESSAGE,
+ };
+ for (String value : legalValues) {
+ assertNotNull(ScramAttributeValue.parse(value));
+ }
+
+ assertNotNull(ScramAttributeValue.parse("e=unsupported-channel-binding-type"));
+ }
+}
diff --git a/scram-common/src/test/java/com/ongres/scram/common/ScramFunctionsTest.java b/scram-common/src/test/java/com/ongres/scram/common/ScramFunctionsTest.java
new file mode 100644
index 0000000..33419b6
--- /dev/null
+++ b/scram-common/src/test/java/com/ongres/scram/common/ScramFunctionsTest.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.Base64;
+import java.util.Locale;
+
+import org.junit.jupiter.api.Test;
+
+class ScramFunctionsTest {
+ private void assertBytesEqualsBase64(String expected, byte[] actual) {
+ assertArrayEquals(ScramStringFormatting.base64Decode(expected), actual);
+ }
+
+ @Test
+ void hmac() {
+ String message = "The quick brown fox jumps over the lazy dog";
+ byte[] key = generateSaltedPasswordSha256();
+ assertBytesEqualsBase64(
+ "1zw4SuJ+BRmn6corI3Y1+eGQwGQ=",
+ ScramFunctions.hmac(ScramMechanism.SCRAM_SHA_1,
+ key, message.getBytes(StandardCharsets.US_ASCII)));
+ assertBytesEqualsBase64(
+ "+Q4a/8FjMG6MoLE/8y8LWyEBJmjpVLbuXtU8rnLd/5E=",
+ ScramFunctions.hmac(ScramMechanism.SCRAM_SHA_256,
+ key, message.getBytes(StandardCharsets.US_ASCII)));
+ assertBytesEqualsBase64(
+ "uu/n7DW8kGuWWANfaHT/rwEU/EBufylMLTWOCtLmvxp"
+ + "2Zmx01UpZO4nauZfkaSWob8jt7no0+xWIVXv/d7LvkQ==",
+ ScramFunctions.hmac(ScramMechanism.SCRAM_SHA_512,
+ key, message.getBytes(StandardCharsets.US_ASCII)));
+ }
+
+ private byte[] generateSaltedPassword() {
+ return ScramFunctions.saltedPassword(
+ ScramMechanism.SCRAM_SHA_1, StringPreparation.SASL_PREPARATION, "pencil".toCharArray(),
+ Base64.getDecoder().decode("QSXCR+Q6sek8bf92".getBytes(StandardCharsets.UTF_8)), 4096);
+ }
+
+ private byte[] generateSaltedPasswordSha256() {
+ byte[] salt = Base64.getDecoder().decode("Fgh8JU2AlRjBHUsIU/GgtQ==");
+ byte[] saltedPassword = ScramFunctions.saltedPassword(
+ ScramMechanism.SCRAM_SHA_256, StringPreparation.SASL_PREPARATION, "test".toCharArray(),
+ salt, 4096);
+ byte[] clientKey = ScramFunctions.clientKey(ScramMechanism.SCRAM_SHA_256, saltedPassword);
+ byte[] storedKey = ScramFunctions.storedKey(ScramMechanism.SCRAM_SHA_256, clientKey);
+ byte[] serverKey = ScramFunctions.serverKey(ScramMechanism.SCRAM_SHA_256, saltedPassword);
+ String encodeToStringSalt = Base64.getEncoder().encodeToString(salt);
+ String encodeToStringClient = Base64.getEncoder().encodeToString(storedKey);
+ String encodeToStringServer = Base64.getEncoder().encodeToString(serverKey);
+ assertEquals("XiT346dvVvPmnmTWeW0djrcMYBGuiQDh8QYbBJaBm/I=", encodeToStringClient);
+ assertEquals("CY9vUvDF8v6FIR8Zwircvd82YV58J5AwWiMWwfssuwg=", encodeToStringServer);
+ String pw =
+ String.format(Locale.ROOT, "%S$%d:%s$%s:%s", ScramMechanism.SCRAM_SHA_256.getName(), 4096,
+ encodeToStringSalt, encodeToStringClient, encodeToStringServer);
+
+ assertEquals(
+ "SCRAM-SHA-256$4096:Fgh8JU2AlRjBHUsIU/GgtQ==$XiT346dvVvPmnmTWeW0djrcMYBGuiQDh8QYbBJaBm/I=:"
+ + "CY9vUvDF8v6FIR8Zwircvd82YV58J5AwWiMWwfssuwg=",
+ pw);
+
+ return ScramFunctions.saltedPassword(
+ ScramMechanism.SCRAM_SHA_256, StringPreparation.SASL_PREPARATION, "pencil".toCharArray(),
+ Base64.getDecoder().decode("W22ZaJ0SNY7soEsUEjb6gQ=="), 4096);
+ }
+
+ @Test
+ void saltedPassword() {
+ assertBytesEqualsBase64("HZbuOlKbWl+eR8AfIposuKbhX30=", generateSaltedPassword());
+ }
+
+ @Test
+ void saltedPasswordWithSaslPrep() {
+ assertBytesEqualsBase64("YniLes+b8WFMvBhtSACZyyvxeCc=", ScramFunctions.saltedPassword(
+ ScramMechanism.SCRAM_SHA_1, StringPreparation.SASL_PREPARATION,
+ "\u2168\u3000a\u0300".toCharArray(),
+ Base64.getDecoder().decode("0BojBCBE6P2/N4bQ"), 6400));
+ assertBytesEqualsBase64("YniLes+b8WFMvBhtSACZyyvxeCc=", ScramFunctions.saltedPassword(
+ ScramMechanism.SCRAM_SHA_1, StringPreparation.SASL_PREPARATION,
+ "\u00ADIX \u00E0".toCharArray(),
+ Base64.getDecoder().decode("0BojBCBE6P2/N4bQ"), 6400));
+ assertBytesEqualsBase64("YniLes+b8WFMvBhtSACZyyvxeCc=", ScramFunctions.saltedPassword(
+ ScramMechanism.SCRAM_SHA_1, StringPreparation.SASL_PREPARATION, "IX \u00E0".toCharArray(),
+ Base64.getDecoder().decode("0BojBCBE6P2/N4bQ"), 6400));
+ assertBytesEqualsBase64("HZbuOlKbWl+eR8AfIposuKbhX30=", ScramFunctions.saltedPassword(
+ ScramMechanism.SCRAM_SHA_1, StringPreparation.SASL_PREPARATION,
+ "\u0070enc\u1806il".toCharArray(),
+ Base64.getDecoder().decode("QSXCR+Q6sek8bf92"), 4096));
+
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+ () -> ScramFunctions.saltedPassword(
+ ScramMechanism.SCRAM_SHA_1, StringPreparation.SASL_PREPARATION,
+ "\u2168\u3000a\u0300\u0007".toCharArray(),
+ Base64.getDecoder().decode("QSXCR+Q6sek8bf92"), 6400));
+ assertEquals("Prohibited ASCII control \"0x0007\"", e.getMessage());
+
+ assertBytesEqualsBase64("MFB9tXSMpUK2frvCND2TWRGdiVY=",
+ ScramFunctions.saltedPassword(
+ ScramMechanism.SCRAM_SHA_1_PLUS, StringPreparation.SASL_PREPARATION,
+ "\u0070enc\u1806il \u00B4\u00BD".toCharArray(),
+ Base64.getDecoder().decode("QSXCR+Q6sek8bf92"), 4096));
+ }
+
+ @Test
+ void saltedPasswordSha256() {
+ assertBytesEqualsBase64("xKSVEDI6tPlSysH6mUQZOeeOp01r6B3fcJbodRPcYV0=",
+ generateSaltedPasswordSha256());
+ }
+
+ private byte[] generateClientKey() {
+ return ScramFunctions.clientKey(ScramMechanism.SCRAM_SHA_1, generateSaltedPassword());
+ }
+
+ private byte[] generateClientKeySha256() {
+ return ScramFunctions.clientKey(ScramMechanism.SCRAM_SHA_256, generateSaltedPasswordSha256());
+ }
+
+ @Test
+ void clientKey() {
+ assertBytesEqualsBase64("4jTEe/bDZpbdbYUrmaqiuiZVVyg=", generateClientKey());
+ }
+
+ @Test
+ void clientKeySha256() {
+ assertBytesEqualsBase64("pg/JI9Z+hkSpLRa5btpe9GVrDHJcSEN0viVTVXaZbos=",
+ generateClientKeySha256());
+ }
+
+ private byte[] generateStoredKey() {
+ return ScramFunctions.storedKey(ScramMechanism.SCRAM_SHA_1, generateClientKey());
+ }
+
+ private byte[] generateStoredKeySha256() {
+ return ScramFunctions.storedKey(ScramMechanism.SCRAM_SHA_256, generateClientKeySha256());
+ }
+
+ @Test
+ void storedKey() {
+ assertBytesEqualsBase64("6dlGYMOdZcOPutkcNY8U2g7vK9Y=", generateStoredKey());
+ }
+
+ @Test
+ void storedKeySha256() {
+ assertBytesEqualsBase64("WG5d8oPm3OtcPnkdi4Uo7BkeZkBFzpcXkuLmtbsT4qY=",
+ generateStoredKeySha256());
+ }
+
+ private byte[] generateServerKey() {
+ return ScramFunctions.serverKey(ScramMechanism.SCRAM_SHA_1, generateSaltedPassword());
+ }
+
+ private byte[] generateServerKeySha256() {
+ return ScramFunctions.serverKey(ScramMechanism.SCRAM_SHA_256, generateSaltedPasswordSha256());
+ }
+
+ @Test
+ void serverKey() {
+ assertBytesEqualsBase64("D+CSWLOshSulAsxiupA+qs2/fTE=", generateServerKey());
+ }
+
+ @Test
+ void serverKeySha256() {
+ assertBytesEqualsBase64("wfPLwcE6nTWhTAmQ7tl2KeoiWGPlZqQxSrmfPwDl2dU=",
+ generateServerKeySha256());
+ }
+
+ private byte[] generateClientSignature() {
+ return ScramFunctions.clientSignature(ScramMechanism.SCRAM_SHA_1, generateStoredKey(),
+ com.ongres.scram.common.RfcExampleSha1.AUTH_MESSAGE);
+ }
+
+ private byte[] generateClientSignatureSha256() {
+ return ScramFunctions.clientSignature(ScramMechanism.SCRAM_SHA_256, generateStoredKeySha256(),
+ com.ongres.scram.common.RfcExampleSha256.AUTH_MESSAGE);
+ }
+
+ @Test
+ void clientSignature() {
+ assertBytesEqualsBase64("XXE4xIawv6vfSePi2ovW5cedthM=", generateClientSignature());
+ }
+
+ @Test
+ void clientSignatureSha256() {
+ assertBytesEqualsBase64("0nMSRnwopAqKfwXHPA3jPrPL+0qDeDtYFEzxmsa+G98=",
+ generateClientSignatureSha256());
+ }
+
+ private byte[] generateClientProof() {
+ return ScramFunctions.clientProof(generateClientKey(), generateClientSignature());
+ }
+
+ private byte[] generateClientProofSha256() {
+ return ScramFunctions.clientProof(generateClientKeySha256(), generateClientSignatureSha256());
+ }
+
+ @Test
+ void clientProof() {
+ assertBytesEqualsBase64("v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=", generateClientProof());
+ }
+
+ @Test
+ void clientProofSha256() {
+ assertBytesEqualsBase64("dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=",
+ generateClientProofSha256());
+ }
+
+ private byte[] generateServerSignature() {
+ return ScramFunctions.serverSignature(ScramMechanism.SCRAM_SHA_1, generateServerKey(),
+ com.ongres.scram.common.RfcExampleSha1.AUTH_MESSAGE);
+ }
+
+ private byte[] generateServerSignatureSha256() {
+ return ScramFunctions.serverSignature(ScramMechanism.SCRAM_SHA_256, generateServerKeySha256(),
+ com.ongres.scram.common.RfcExampleSha256.AUTH_MESSAGE);
+ }
+
+ @Test
+ void serverSignature() {
+ assertBytesEqualsBase64("rmF9pqV8S7suAoZWja4dJRkFsKQ=", generateServerSignature());
+ }
+
+ @Test
+ void serverSignatureSha256() {
+ assertBytesEqualsBase64("6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=",
+ generateServerSignatureSha256());
+ }
+
+ @Test
+ void verifyClientProof() {
+ assertTrue(
+ ScramFunctions.verifyClientProof(
+ ScramMechanism.SCRAM_SHA_1, generateClientProof(), generateStoredKey(),
+ com.ongres.scram.common.RfcExampleSha1.AUTH_MESSAGE));
+ }
+
+ @Test
+ void verifyClientProofSha256() {
+ assertTrue(
+ ScramFunctions.verifyClientProof(
+ ScramMechanism.SCRAM_SHA_256, generateClientProofSha256(), generateStoredKeySha256(),
+ com.ongres.scram.common.RfcExampleSha256.AUTH_MESSAGE));
+ }
+
+ @Test
+ void verifyServerSignature() {
+ assertTrue(
+ ScramFunctions.verifyServerSignature(
+ ScramMechanism.SCRAM_SHA_1, generateServerKey(),
+ com.ongres.scram.common.RfcExampleSha1.AUTH_MESSAGE, generateServerSignature()));
+ }
+
+ @Test
+ void verifyServerSignatureSha256() {
+ assertTrue(
+ ScramFunctions.verifyServerSignature(
+ ScramMechanism.SCRAM_SHA_256, generateServerKeySha256(),
+ com.ongres.scram.common.RfcExampleSha256.AUTH_MESSAGE,
+ generateServerSignatureSha256()));
+ }
+}
diff --git a/scram-common/src/test/java/com/ongres/scram/common/ScramMechanismTest.java b/scram-common/src/test/java/com/ongres/scram/common/ScramMechanismTest.java
new file mode 100644
index 0000000..c527b78
--- /dev/null
+++ b/scram-common/src/test/java/com/ongres/scram/common/ScramMechanismTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EmptySource;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.NullSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class ScramMechanismTest {
+
+ private static final byte[] EMPTY_KEY = new byte[32];
+
+ @ParameterizedTest
+ @ValueSource(strings = {"SCRAM-SHA-1", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-256-PLUS"})
+ void testIanaScramMechanisms(@NotNull String name) {
+ assertNotNull(ScramMechanism.byName(name));
+ }
+
+ @ParameterizedTest
+ @EnumSource(ScramMechanism.class)
+ void testNameConvention(ScramMechanism scramMechanism) {
+ // Note that SASL mechanism names are limited to 20 octets, which means that only
+ // hash function names with lengths shorter or equal to 9 octets
+ // (20-length("SCRAM-")-length("-PLUS") can be used.
+ String name = scramMechanism.getName();
+ assertTrue(name.startsWith("SCRAM-"), "name should start with SCRAM-");
+ if (scramMechanism.isPlus()) {
+ assertTrue(name.endsWith("-PLUS"), "name should end with -PLUS");
+ } else {
+ assertFalse(name.endsWith("-PLUS"), "name should not end with -PLUS");
+ }
+
+ String hashName = name.replace("SCRAM-", "").replace("-PLUS", "");
+ assertEquals(scramMechanism.getHashAlgorithmName(), hashName);
+ assertTrue(hashName.length() <= 9);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"SCRAM-SHA", "SHA-1-PLUS", "SCRAM-SHA-256-", "SCRAM-SHA-256-PLUS!"})
+ @EmptySource
+ void byNameInvalid(@NotNull String name) {
+ assertNull(ScramMechanism.byName(name));
+ }
+
+ @ParameterizedTest
+ @NullSource
+ void byNullName(@NotNull String name) {
+ assertThrows(IllegalArgumentException.class, () -> ScramMechanism.byName(name));
+ }
+
+ @ParameterizedTest
+ @EnumSource(ScramMechanism.class)
+ void testHashSupportedByJvm(ScramMechanism scramMechanism) {
+ byte[] digest = scramMechanism.digest(new byte[0]);
+ assertNotNull(digest, "got a null digest");
+ assertEquals(scramMechanism.getKeyLength() / 8, digest.length);
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideSupportedMechanisms")
+ void testHmacSupportedByJvm(@NotNull String mechanism) {
+ ScramMechanism scramMechanism = ScramMechanism.byName(mechanism);
+ assertNotNull(scramMechanism);
+ byte[] hmac = scramMechanism.hmac(EMPTY_KEY, new byte[0]);
+ assertNotNull(hmac, "got a null HMAC");
+ assertEquals(scramMechanism.getKeyLength() / 8, hmac.length);
+ }
+
+ private static @NotNull List<@NotNull String> provideSupportedMechanisms() {
+ return ScramMechanism.supportedMechanisms();
+ }
+
+}
diff --git a/scram-common/src/test/java/com/ongres/scram/common/ScramStringFormattingTest.java b/scram-common/src/test/java/com/ongres/scram/common/ScramStringFormattingTest.java
new file mode 100644
index 0000000..3e2cbf6
--- /dev/null
+++ b/scram-common/src/test/java/com/ongres/scram/common/ScramStringFormattingTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+class ScramStringFormattingTest {
+ private static final String[] VALUES_NO_CHARS_TO_BE_ESCAPED =
+ new String[] {"asdf", "''--%%21", " ttt???"};
+ private static final String[] VALUES_TO_BE_ESCAPED = new String[] {
+ ",", "=", "a,b", "===", "a=", ",=,", "=2C", "=3D"
+ };
+ private static final String[] ESCAPED_VALUES = new String[] {
+ "=2C", "=3D", "a=2Cb", "=3D=3D=3D", "a=3D", "=2C=3D=2C", "=3D2C", "=3D3D"
+ };
+ private static final String[] INVALID_SASL_NAMES =
+ new String[] {"=", "as,df", "a=b", " ttt???=2D"};
+
+ @Test
+ void toSaslNameNoCharactersToBeEscaped() {
+ for (String s : VALUES_NO_CHARS_TO_BE_ESCAPED) {
+ assertEquals(s, ScramStringFormatting.toSaslName(s));
+ }
+ }
+
+ @Test
+ void toSaslNameWithCharactersToBeEscaped() {
+ for (int i = 0; i < VALUES_TO_BE_ESCAPED.length; i++) {
+ assertEquals(ESCAPED_VALUES[i], ScramStringFormatting.toSaslName(VALUES_TO_BE_ESCAPED[i]));
+ }
+ }
+
+ @Test
+ void fromSaslNameNoCharactersToBeEscaped() {
+ for (String s : VALUES_NO_CHARS_TO_BE_ESCAPED) {
+ assertEquals(s, ScramStringFormatting.fromSaslName(s));
+ }
+ }
+
+ @Test
+ void fromSaslNameWithCharactersToBeUnescaped() {
+ for (int i = 0; i < ESCAPED_VALUES.length; i++) {
+ assertEquals(VALUES_TO_BE_ESCAPED[i], ScramStringFormatting.fromSaslName(ESCAPED_VALUES[i]));
+ }
+ }
+
+ @Test
+ void fromSaslNameWithInvalidCharacters() {
+ int n = 0;
+ for (String s : INVALID_SASL_NAMES) {
+ try {
+ assertEquals(s, ScramStringFormatting.fromSaslName(s));
+ } catch (IllegalArgumentException e) {
+ n++;
+ }
+ }
+
+ assertTrue(n == INVALID_SASL_NAMES.length, "Not all values produced IllegalArgumentException");
+ }
+
+}
diff --git a/scram-common/src/test/java/com/ongres/scram/common/ServerFinalMessageTest.java b/scram-common/src/test/java/com/ongres/scram/common/ServerFinalMessageTest.java
new file mode 100644
index 0000000..214c967
--- /dev/null
+++ b/scram-common/src/test/java/com/ongres/scram/common/ServerFinalMessageTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common;
+
+import static com.ongres.scram.common.RfcExampleSha1.AUTH_MESSAGE;
+import static com.ongres.scram.common.RfcExampleSha1.PASSWORD;
+import static com.ongres.scram.common.RfcExampleSha1.SERVER_FINAL_MESSAGE;
+import static com.ongres.scram.common.RfcExampleSha1.SERVER_ITERATIONS;
+import static com.ongres.scram.common.RfcExampleSha1.SERVER_SALT;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Base64;
+
+import com.ongres.scram.common.exception.ScramParseException;
+import org.junit.jupiter.api.Test;
+
+class ServerFinalMessageTest {
+ @Test
+ void validConstructor() {
+ byte[] serverKey = ScramFunctions.serverKey(
+ ScramMechanism.SCRAM_SHA_1,
+ ScramFunctions.saltedPassword(ScramMechanism.SCRAM_SHA_1, StringPreparation.NO_PREPARATION,
+ PASSWORD.toCharArray(),
+ Base64.getDecoder().decode(SERVER_SALT),
+ SERVER_ITERATIONS));
+ ServerFinalMessage serverFinalMessage1 = new ServerFinalMessage(
+ ScramFunctions.serverSignature(ScramMechanism.SCRAM_SHA_1, serverKey, AUTH_MESSAGE));
+ assertEquals(SERVER_FINAL_MESSAGE, serverFinalMessage1.toString());
+ assertFalse(serverFinalMessage1.isError());
+
+ ServerFinalMessage serverFinalMessage2 =
+ new ServerFinalMessage("unknown-user");
+ assertEquals(ScramAttributes.ERROR.getChar() + "=unknown-user",
+ serverFinalMessage2.toString());
+ assertTrue(serverFinalMessage2.isError());
+ }
+
+ @Test
+ void validParseFrom() throws ScramParseException {
+ ServerFinalMessage serverFinalMessage1 = ServerFinalMessage.parseFrom(SERVER_FINAL_MESSAGE);
+ assertEquals(SERVER_FINAL_MESSAGE, serverFinalMessage1.toString());
+ assertFalse(serverFinalMessage1.isError());
+
+ ServerFinalMessage serverFinalMessage2 =
+ ServerFinalMessage.parseFrom("e=channel-binding-not-supported");
+ assertEquals("e=channel-binding-not-supported", serverFinalMessage2.toString());
+ assertTrue(serverFinalMessage2.isError());
+ assertEquals("channel-binding-not-supported",
+ serverFinalMessage2.getServerError());
+ }
+
+ @Test
+ void invalidServerError() throws ScramParseException {
+ assertThrows(IllegalArgumentException.class,
+ () -> ServerFinalMessage.parseFrom("e=binding-et-supported"));
+ }
+}
diff --git a/scram-common/src/test/java/com/ongres/scram/common/ServerFirstMessageTest.java b/scram-common/src/test/java/com/ongres/scram/common/ServerFirstMessageTest.java
new file mode 100644
index 0000000..142c54a
--- /dev/null
+++ b/scram-common/src/test/java/com/ongres/scram/common/ServerFirstMessageTest.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common;
+
+import static com.ongres.scram.common.RfcExampleSha1.CLIENT_NONCE;
+import static com.ongres.scram.common.RfcExampleSha1.SERVER_FIRST_MESSAGE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import com.ongres.scram.common.exception.ScramParseException;
+import org.junit.jupiter.api.Test;
+
+class ServerFirstMessageTest {
+ @Test
+ void validConstructor() {
+ ServerFirstMessage serverFirstMessage = new ServerFirstMessage(
+ CLIENT_NONCE,
+ "3rfcNHYJY1ZVvWVs7j",
+ "QSXCR+Q6sek8bf92",
+ 4096);
+
+ assertEquals(SERVER_FIRST_MESSAGE, serverFirstMessage.toString());
+ }
+
+ @Test
+ void validParseFrom() throws ScramParseException {
+ ServerFirstMessage serverFirstMessage =
+ ServerFirstMessage.parseFrom(SERVER_FIRST_MESSAGE, CLIENT_NONCE);
+
+ assertEquals(SERVER_FIRST_MESSAGE, serverFirstMessage.toString());
+ }
+}
diff --git a/scram-common/src/test/java/com/ongres/scram/common/StringPreparationTest.java b/scram-common/src/test/java/com/ongres/scram/common/StringPreparationTest.java
new file mode 100644
index 0000000..257bf71
--- /dev/null
+++ b/scram-common/src/test/java/com/ongres/scram/common/StringPreparationTest.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.security.SecureRandom;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class StringPreparationTest {
+ private static final String[] ONLY_NON_PRINTABLE_STRINGS =
+ new String[] {(char) 13 + "", (char) 13 + "\n\n"};
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ void doNormalizeNullEmpty(char[] value) {
+ for (StringPreparation stringPreparation : StringPreparation.values()) {
+ assertThrows(IllegalArgumentException.class, () -> stringPreparation.normalize(value));
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "toastingxenotime", "infecttolerant", "cobblerjack", "zekedigital", "freshscarisdale",
+ "lamwaylon",
+ "lagopodousmonkeys", "fanfarecheesy", "willowfinnegan", "canoeamoeba", "stinkeroddball",
+ "terracecomet",
+ "cakebrazos", "headersidesaddle", "cloudultracrepidarian", "grimegastropub", "stallchilli",
+ "shawnapentagon", "chapeltarp", "rydbergninja", "differencegym", "europiummuscle",
+ "swilledonce",
+ "defensivesyntaxis", "desktopredundant", "stakingsky", "goofywaiting", "boundsemm",
+ "pipermonstrous",
+ "faintfrog", "riskinsist", "constantjunkie", "rejectbroth", "ceilbeau", "ponyjaialai",
+ "burnishselfies",
+ "unamusedglenmore", "parmesanporcupine", "suteconcerto", "ribstony", "sassytwelve",
+ "coursesnasturtium",
+ "singlecinders", "kinkben", "chiefpussface", "unknownivery", "robterra", "wearycubes",
+ "bearcontent",
+ "aquifertrip", "insulinlick", "batterypeace", "rubigloo", "fixessnizort", "coalorecheesy",
+ "logodarthvader",
+ "equipmentbizarre", "charitycolne", "gradecomputer", "incrediblegases", "ingotflyingfish",
+ "abaftmounting",
+ "kissingfluke", "chesterdinky", "anthropicdip", "portalcairo", "purebredhighjump",
+ "jamaicansteeping",
+ "skaterscoins", "chondrulelocust", "modespretty", "otisnadrid", "lagoonone", "arrivepayday",
+ "lawfulpatsy",
+ "customersdeleted", "superiorarod", "abackwarped", "footballcyclic", "sawtshortstop",
+ "waskerleysanidine",
+ "polythenehead", "carpacciosierra", "gnashgabcheviot", "plunkarnisdale", "surfacebased",
+ "wickedpark",
+ "capitalistivan", "kinglassmuse", "adultsceiriog", "medrones", "climaxshops",
+ "archeangolfer", "tomfront",
+ "kobeshift", "nettleaugustus", "bitesizedlion", "crickedbunting", "englishrichard",
+ "dangerousdelmonico",
+ "sparklemicrosoft", "kneepadsfold", "enunciatesunglasses", "parchmentsteak", "meigpiton",
+ "puttingcitrusy",
+ "eyehash", "newtonatomiser", "witchesburberry", "positionwobbly", "clipboardamber",
+ "ricolobster",
+ "calendarpetal", "shinywound", "dealemral", "moonrakerfinnish", "banditliberated",
+ "whippedfanatical",
+ "jargongreasy", "yumlayla", "dwarfismtransition", "doleriteduce", "sikickball",
+ "columngymnastics", "draybowmont", "jupitersnorkling", "siderealmolding", "dowdyrosary",
+ "novaskeeter",
+ "whickerpulley", "rutlandsliders", "categoryflossed", "coiltiedogfish", "brandwaren",
+ "altairlatigo",
+ "acruxyouthscape", "harmonicdash", "jasperserver", "slicedaggie", "gravityfern",
+ "bitsstorm",
+ "readymadehobby", "surfeitgrape", "pantheonslabs", "ammandecent", "skicrackers",
+ "speyfashions",
+ "languagedeeno", "pettyconfit", "minutesshimmering", "thinhopeangellist",
+ "sleevelesscadmium", "controlarc",
+ "robinvolvox", "postboxskylark", "tortepleasing", "lutzdillinger", "amnioteperl",
+ "burntmaximize",
+ "gamblingearn", "bumsouch", "coronagraphdown", "bodgeelearning", "hackingscraper",
+ "hartterbium",
+ "mindyurgonian", "leidlebalki", "labelthumbs", "lincolncrisps", "pearhamster", "termsfiona",
+ "tickingsomber", "hatellynfi", "northumberlandgrotesque", "harpistcaramel", "gentryswiss",
+ "illusionnooks",
+ "easilyrows", "highgluten", "backedallegiance", "laelsitesearch", "methodfix",
+ "teethminstral",
+ "chemicalchildish", "likablepace", "alikealeph", "nalasincere", "investbaroque",
+ "conditionenvelope",
+ "splintsmccue", "carnonprompt", "resultharvey", "acceptsheba", "redditmonsoon",
+ "multiplepostbox",
+ "invitationchurch", "drinksgaliath", "ordersvivid", "mugsgit", "clumpingfreak"
+ })
+ void doNormalizeValidAsciiCases(String username) {
+ char[] validAsciiUsername = username.toCharArray();
+ for (StringPreparation stringPreparation : StringPreparation.values()) {
+ assertArrayEquals(validAsciiUsername, stringPreparation.normalize(validAsciiUsername));
+ }
+ }
+
+ private static Stream
+ *
+ *
+ * @implNote {@code extensions} are not supported.
+ * @see [RFC5802] Section 7
+ */
+public final class ClientFinalMessage extends AbstractScramMessage {
+
+ /**
+ * channel-binding = "c=" base64 encoding of cbind-input.
+ */
+ private final String cbindInput;
+
+ /**
+ * nonce = "r=" c-nonce [s-nonce]. Second part provided by server.
+ */
+ private final String nonce;
+
+ /**
+ * proof = "p=" base64.
+ */
+ private final byte[] proof;
+
+ /**
+ * Constructus a client-final-message with the provided gs2Header (the same one used in the
+ * client-first-message), optionally the channel binding data, and the nonce. This method is
+ * intended to be used by SCRAM clients, and not to be constructed directly.
+ *
+ * @param gs2Header The GSS-API header
+ * @param cbindData If using channel binding, the channel binding data
+ * @param nonce The nonce
+ * @param proof The bytes representing the computed client proof
+ */
+ public ClientFinalMessage(Gs2Header gs2Header, byte[] cbindData, String nonce, byte[] proof) {
+ this.cbindInput = generateCBindInput(gs2Header, cbindData);
+ this.nonce = checkNotEmpty(nonce, "nonce");
+ this.proof = checkNotNull(proof, "proof").clone();
+ }
+
+ /**
+ * Return the channel-binding "c=" base64 encoding of cbind-input.
+ *
+ * @return the {@code channel-binding}
+ */
+ public String getCbindInput() {
+ return cbindInput;
+ }
+
+ /**
+ * Return the nonce.
+ *
+ * @return the {@code nonce}
+ */
+ public String getNonce() {
+ return nonce;
+ }
+
+ /**
+ * Return the proof.
+ *
+ * @return the {@code proof}
+ */
+ public byte[] getProof() {
+ return proof.clone();
+ }
+
+ private static void checkChannelBinding(Gs2Header gs2Header, byte[] cbindData) {
+ final Gs2CbindFlag channelBindingFlag = gs2Header.getChannelBindingFlag();
+ if (channelBindingFlag == Gs2CbindFlag.CHANNEL_BINDING_REQUIRED
+ && null == cbindData) {
+ throw new IllegalArgumentException("Channel binding data is required");
+ }
+ if (channelBindingFlag != Gs2CbindFlag.CHANNEL_BINDING_REQUIRED
+ && null != cbindData) {
+ throw new IllegalArgumentException("Channel binding data should not be present");
+ }
+ }
+
+ private static @NotNull String generateCBindInput(@NotNull Gs2Header gs2Header,
+ byte @Nullable [] cbindData) {
+ checkNotNull(gs2Header, "gs2Header");
+ checkChannelBinding(gs2Header, cbindData);
+
+ byte[] cbindInput = gs2Header.writeTo(new StringBuilder(32))
+ .append(',').toString().getBytes(StandardCharsets.UTF_8);
+
+ if (null != cbindData && cbindData.length != 0) {
+ byte[] cbindInputNew = new byte[cbindInput.length + cbindData.length];
+ System.arraycopy(cbindInput, 0, cbindInputNew, 0, cbindInput.length);
+ System.arraycopy(cbindData, 0, cbindInputNew, cbindInput.length, cbindData.length);
+ cbindInput = cbindInputNew;
+ }
+
+ return ScramStringFormatting.base64Encode(cbindInput);
+ }
+
+ private StringBuilder writeToWithoutProof(@NotNull StringBuilder sb) {
+ return StringWritableCsv.writeTo(sb,
+ new ScramAttributeValue(ScramAttributes.CHANNEL_BINDING, cbindInput),
+ new ScramAttributeValue(ScramAttributes.NONCE, nonce));
+ }
+
+ static StringBuilder withoutProof(StringBuilder sb, Gs2Header gs2Header, byte[] cbindData,
+ String nonce) {
+ return StringWritableCsv.writeTo(sb,
+ new ScramAttributeValue(ScramAttributes.CHANNEL_BINDING,
+ generateCBindInput(gs2Header, cbindData)),
+ new ScramAttributeValue(ScramAttributes.NONCE, nonce));
+ }
+
+ @Override
+ StringBuilder writeTo(StringBuilder sb) {
+ writeToWithoutProof(sb);
+
+ return StringWritableCsv.writeTo(
+ sb,
+ null, // This marks the position of writeToWithoutProof, required for the ","
+ new ScramAttributeValue(ScramAttributes.CLIENT_PROOF,
+ ScramStringFormatting.base64Encode(proof)));
+ }
+
+}
diff --git a/scram-common/src/main/java/com/ongres/scram/common/ClientFirstMessage.java b/scram-common/src/main/java/com/ongres/scram/common/ClientFirstMessage.java
new file mode 100644
index 0000000..5913149
--- /dev/null
+++ b/scram-common/src/main/java/com/ongres/scram/common/ClientFirstMessage.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common;
+
+import static com.ongres.scram.common.util.Preconditions.checkNotEmpty;
+import static com.ongres.scram.common.util.Preconditions.checkNotNull;
+
+import com.ongres.scram.common.exception.ScramParseException;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Constructs and parses client-first-messages. Message contains a {@code gs2-header}, a username
+ * and a
+ * nonce.
+ *
+ *
+ * cbind-input
+ * gs2-header [ cbind-data ]
+ *
+ * ;; cbind-data MUST be present for
+ * ;; gs2-cbind-flag of "p" and MUST be absent
+ * ;; for "y" or "n".
+ *
+ * channel-binding
+ * "c=" base64
+ *
+ * ;; base64 encoding of cbind-input.
+ *
+ * client-final-message-without-proof
+ * channel-binding "," nonce ["," extensions]
+ *
+ *
+ * client-final-message
+ * client-final-message-without-proof "," proof
+ *
+ *
+ *
+ * @implNote {@code extensions} are not supported.
+ * @see [RFC5802] Section 7
+ */
+public final class ClientFirstMessage extends AbstractScramMessage {
+
+ /**
+ * gs2-header = gs2-cbind-flag "," [ authzid ] ",".
+ */
+ private final @NotNull Gs2Header gs2Header;
+
+ /**
+ * username = "n=" saslname.
+ */
+ private final @NotNull String username;
+
+ /**
+ * nonce= "r=" c-nonce [s-nonce].
+ */
+ private final @NotNull String clientNonce;
+
+ /**
+ * Constructs a client-first-message for the given user, nonce and gs2Header. This constructor is
+ * intended to be instantiated by a scram client, and not directly. The client should be providing
+ * the header, and nonce (and probably the user too).
+ *
+ * @param gs2Header The GSS-API header
+ * @param username The SCRAM username
+ * @param clientNonce The nonce for this session
+ * @throws IllegalArgumentException If any of the arguments is null or empty
+ */
+ public ClientFirstMessage(@NotNull Gs2Header gs2Header, @NotNull String username,
+ @NotNull String clientNonce) {
+ this.gs2Header = checkNotNull(gs2Header, "gs2Header");
+ this.username = ScramStringFormatting.toSaslName(checkNotEmpty(username, "username"));
+ this.clientNonce = checkNotEmpty(clientNonce, "clientNonce");
+ }
+
+ /**
+ * Constructs a client-first-message for the given parameters. Under normal operation, this
+ * constructor is intended to be instantiated by a scram client, and not directly. However, this
+ * constructor is more user- or test-friendly, as the arguments are easier to provide without
+ * building other indirect object parameters.
+ *
+ * @param gs2CbindFlag The channel-binding flag
+ * @param authzid The optional authzid
+ * @param cbindName The optional channel binding name
+ * @param username The SCRAM user
+ * @param clientNonce The nonce for this session
+ * @throws IllegalArgumentException If the flag, user or nonce are null or empty
+ */
+ public ClientFirstMessage(@NotNull Gs2CbindFlag gs2CbindFlag, @Nullable String cbindName,
+ @Nullable String authzid, @NotNull String username, @NotNull String clientNonce) {
+ this(new Gs2Header(gs2CbindFlag, cbindName, authzid), username, clientNonce);
+ }
+
+ /**
+ * Constructs a client-first-message for the given parameters, with no channel binding nor
+ * authzid. Under normal operation, this constructor is intended to be instantiated by a scram
+ * client, and not directly. However, this constructor is more user- or test-friendly, as the
+ * arguments are easier to provide without building other indirect object parameters.
+ *
+ * @param username The SCRAM user
+ * @param clientNonce The nonce for this session
+ * @throws IllegalArgumentException If the user or nonce are null or empty
+ */
+ public ClientFirstMessage(@NotNull String username, @NotNull String clientNonce) {
+ this(new Gs2Header(Gs2CbindFlag.CLIENT_NOT), username, clientNonce);
+ }
+
+ /**
+ * Check to probe if gs2-cbind-flag is set to "p=".
+ *
+ * @return true if the message requires channel binding
+ */
+ public boolean isChannelBindingRequired() {
+ return gs2Header.getChannelBindingFlag() == Gs2CbindFlag.CHANNEL_BINDING_REQUIRED;
+ }
+
+ /**
+ * Return the Gs2Header.
+ *
+ * @return the {@code gs2-header}
+ */
+ public @NotNull Gs2Header getGs2Header() {
+ return gs2Header;
+ }
+
+ /**
+ * Return the username.
+ *
+ * @return the {@code "n=" saslname}
+ */
+ public @NotNull String getUsername() {
+ return username;
+ }
+
+ /**
+ * Return the client nonce.
+ *
+ * @return the {@code c-nonce}
+ */
+ public @NotNull String getClientNonce() {
+ return clientNonce;
+ }
+
+ /**
+ * Limited version of the StringWritableCsv method, that doesn't write the GS2 header. This method
+ * is useful to construct the auth message used as part of the SCRAM algorithm.
+ *
+ * @param sb A StringBuffer where to write the data to.
+ * @return The same StringBuffer
+ */
+ @NotNull
+ StringBuilder clientFirstMessageBare(@NotNull StringBuilder sb) {
+ return StringWritableCsv.writeTo(
+ sb,
+ new ScramAttributeValue(ScramAttributes.USERNAME, username),
+ new ScramAttributeValue(ScramAttributes.NONCE, clientNonce));
+ }
+
+ /**
+ * Construct a {@link ClientFirstMessage} instance from a message (String).
+ *
+ * @param clientFirstMessage The String representing the client-first-message
+ * @return The instance
+ * @throws ScramParseException If the message is not a valid client-first-message
+ * @throws IllegalArgumentException If the message is null or empty
+ */
+ @NotNull
+ public static ClientFirstMessage parseFrom(@NotNull String clientFirstMessage)
+ throws ScramParseException {
+ checkNotEmpty(clientFirstMessage, "clientFirstMessage");
+
+ @NotNull
+ String @NotNull [] userNonceString;
+ try {
+ userNonceString = StringWritableCsv.parseFrom(clientFirstMessage, 2, 2);
+ } catch (IllegalArgumentException e) {
+ throw new ScramParseException("Illegal series of attributes in client-first-message", e);
+ }
+
+ ScramAttributeValue user = ScramAttributeValue.parse(userNonceString[0]);
+ if (ScramAttributes.USERNAME.getChar() != user.getChar()) {
+ throw new ScramParseException("user must be the 3rd element of the client-first-message");
+ }
+
+ ScramAttributeValue nonce = ScramAttributeValue.parse(userNonceString[1]);
+ if (ScramAttributes.NONCE.getChar() != nonce.getChar()) {
+ throw new ScramParseException("nonce must be the 4th element of the client-first-message");
+ }
+
+ Gs2Header gs2Header = Gs2Header.parseFrom(clientFirstMessage); // Takes first two fields
+ return new ClientFirstMessage(gs2Header, user.getValue(), nonce.getValue());
+ }
+
+ @Override
+ StringBuilder writeTo(StringBuilder sb) {
+ StringWritableCsv.writeTo(
+ sb,
+ gs2Header,
+ null // This marks the position of the rest of the elements, required for the ","
+ );
+
+ return clientFirstMessageBare(sb);
+ }
+}
diff --git a/scram-common/src/main/java/com/ongres/scram/common/CryptoUtil.java b/scram-common/src/main/java/com/ongres/scram/common/CryptoUtil.java
new file mode 100644
index 0000000..aac2c20
--- /dev/null
+++ b/scram-common/src/main/java/com/ongres/scram/common/CryptoUtil.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common;
+
+import static com.ongres.scram.common.util.Preconditions.checkArgument;
+import static com.ongres.scram.common.util.Preconditions.checkNotNull;
+import static com.ongres.scram.common.util.Preconditions.gt0;
+
+import java.security.InvalidKeyException;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Locale;
+
+import javax.crypto.Mac;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import com.ongres.scram.common.exception.ScramRuntimeException;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Utility static methods for cryptography related tasks.
+ */
+final class CryptoUtil {
+
+ private CryptoUtil() {
+ throw new IllegalStateException("Utility class");
+ }
+
+ /**
+ * Compute the "Hi" function for SCRAM.
+ *
+ * {@code
+ * Hi(str, salt, i):
+ *
+ * U1 := HMAC(str, salt + INT(1))
+ * U2 := HMAC(str, U1)
+ * ...
+ * Ui-1 := HMAC(str, Ui-2)
+ * Ui := HMAC(str, Ui-1)
+ *
+ * Hi := U1 XOR U2 XOR ... XOR Ui
+ *
+ * where "i" is the iteration count, "+" is the string concatenation
+ * operator, and INT(g) is a 4-octet encoding of the integer g, most
+ * significant octet first.
+ *
+ * Hi() is, essentially, PBKDF2 [RFC2898] with HMAC() as the
+ * pseudorandom function (PRF) and with dkLen == output length of
+ * HMAC() == output length of H().
+ * }
+ *
+ * @param secretKeyFactory The SecretKeyFactory to generate the SecretKey
+ * @param keyLength The length of the key (in bits)
+ * @param password The char array to compute the Hi function
+ * @param salt The salt
+ * @param iterationCount The number of iterations
+ * @return The bytes of the computed Hi value
+ * @throws ScramRuntimeException if unsupported PBEKeySpec
+ */
+ static byte[] hi(SecretKeyFactory secretKeyFactory, int keyLength, char[] password, byte[] salt,
+ int iterationCount) {
+ try {
+ PBEKeySpec spec = new PBEKeySpec(password, salt, iterationCount, keyLength);
+ SecretKey key = secretKeyFactory.generateSecret(spec);
+ spec.clearPassword();
+ return key.getEncoded();
+ } catch (InvalidKeySpecException ex) {
+ throw new ScramRuntimeException(
+ String.format(Locale.ROOT, "Platform error: unsupported PBEKeySpec for %s algorithm",
+ secretKeyFactory.getAlgorithm()),
+ ex);
+ }
+ }
+
+ /**
+ * Computes the HMAC of a given message.
+ *
+ * {@code
+ * HMAC(key, str): Apply the HMAC keyed hash algorithm (defined in
+ * [RFC2104]) using the octet string represented by "key" as the key
+ * and the octet string "str" as the input string. The size of the
+ * result is the hash result size for the hash function in use. For
+ * example, it is 20 octets for SHA-1 (see [RFC3174]).
+ * }
+ *
+ * @param secretKeySpec A key of the given algorithm
+ * @param mac A MAC instance of the given algorithm
+ * @param message The message to compute the HMAC
+ * @return The bytes of the computed HMAC value
+ * @throws ScramRuntimeException unsupported key for HMAC algorithm
+ */
+ static byte[] hmac(SecretKeySpec secretKeySpec, Mac mac, byte[] message) {
+ try {
+ mac.init(secretKeySpec);
+ } catch (InvalidKeyException ex) {
+ throw new ScramRuntimeException(
+ String.format(Locale.ROOT, "Platform error: unsupported key for %s algorithm",
+ mac.getAlgorithm()),
+ ex);
+ }
+ return mac.doFinal(message);
+ }
+
+ /**
+ * Computes a byte-by-byte xor operation.
+ * {@code
+ * XOR: Apply the exclusive-or operation to combine the octet string
+ * on the left of this operator with the octet string on the right of
+ * this operator. The length of the output and each of the two
+ * inputs will be the same for this use.
+ * }
+ *
+ * @param value1 first value to apply xor
+ * @param value2 second value to apply xor
+ * @return xor operation
+ */
+ static byte @NotNull [] xor(byte @NotNull [] value1, byte @NotNull [] value2) {
+ checkNotNull(value1, "value1");
+ checkNotNull(value2, "value2");
+ checkArgument(value1.length == value2.length, "Both values must have the same length");
+
+ byte[] result = new byte[value1.length];
+ for (int i = 0; i < value1.length; i++) {
+ result[i] = (byte) (value1[i] ^ value2[i]);
+ }
+
+ return result;
+ }
+
+ /**
+ * Generates a random salt. Normally the output is encoded to Base64.
+ *
+ * @param saltSize The length of the salt, in bytes
+ * @param random The SecureRandom to use
+ * @return The bye[] representing the salt
+ * @throws IllegalArgumentException if the saltSize is not positive, or if random is null
+ */
+ static byte @NotNull [] salt(int saltSize, @NotNull SecureRandom random) {
+ gt0(saltSize, "saltSize");
+ checkNotNull(random, "random");
+ byte[] randomSalt = new byte[saltSize];
+ random.nextBytes(randomSalt);
+ return randomSalt;
+ }
+}
diff --git a/scram-common/src/main/java/com/ongres/scram/common/Gs2AttributeValue.java b/scram-common/src/main/java/com/ongres/scram/common/Gs2AttributeValue.java
new file mode 100644
index 0000000..e230ddc
--- /dev/null
+++ b/scram-common/src/main/java/com/ongres/scram/common/Gs2AttributeValue.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common;
+
+import static com.ongres.scram.common.util.Preconditions.checkNotNull;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Parse and write GS2 Attribute-Value pairs.
+ */
+final class Gs2AttributeValue extends AbstractCharAttributeValue
+ *
+ * client-first-message-bare
+ * [reserved-mext ","] username "," nonce ["," extensions]
+ *
+ *
+ * client-first-message
+ * gs2-header client-first-message-bare
+ *
+ *
+ *
+ * @see [RFC5802] Formal Syntax
+ */
+public final class Gs2Header extends StringWritable {
+
+ private final @NotNull Gs2AttributeValue gs2CbindFlag;
+ private final @Nullable Gs2AttributeValue authzid;
+
+ /**
+ * Construct and validates a Gs2Header. Only provide the channel binding name if the channel
+ * binding flag is set to required.
+ *
+ * @param cbindFlag The channel binding flag
+ * @param cbName The channel-binding name. Should be not null if channel binding is required
+ * @param authzid The optional SASL authorization identity
+ * @throws IllegalArgumentException If the channel binding flag and argument are invalid
+ */
+ public Gs2Header(@NotNull Gs2CbindFlag cbindFlag, @Nullable String cbName,
+ @Nullable String authzid) {
+ checkChannelBinding(cbindFlag, cbName);
+
+ this.gs2CbindFlag = new Gs2AttributeValue(Gs2Attributes.byGs2CbindFlag(cbindFlag), cbName);
+ this.authzid = authzid == null ? null
+ : new Gs2AttributeValue(Gs2Attributes.AUTHZID, ScramStringFormatting.toSaslName(authzid));
+ }
+
+ /**
+ * Construct and validates a Gs2Header with no authzid. Only provide the channel binding name if
+ * the channel binding flag is set to required.
+ *
+ * @param cbindFlag The channel binding flag
+ * @param cbName The channel-binding name. Should be not null iif channel binding is required
+ * @throws IllegalArgumentException If the channel binding flag and argument are invalid
+ */
+ public Gs2Header(@NotNull Gs2CbindFlag cbindFlag, @Nullable String cbName) {
+ this(cbindFlag, cbName, null);
+ }
+
+ /**
+ * Construct and validates a Gs2Header with no authzid nor channel binding.
+ *
+ * @param cbindFlag The channel binding flag
+ * @throws IllegalArgumentException If the channel binding is supported (no cbname can be provided
+ * here)
+ */
+ public Gs2Header(@NotNull Gs2CbindFlag cbindFlag) {
+ this(cbindFlag, null, null);
+ }
+
+ /**
+ * Return the channel binding flag.
+ *
+ * @return the {@code gs2-cbind-flag}
+ */
+ public @NotNull Gs2CbindFlag getChannelBindingFlag() {
+ return Gs2CbindFlag.byChar(gs2CbindFlag.getChar());
+ }
+
+ /**
+ * Return the channel binding type.
+ *
+ * @return the {@code cb-name}
+ */
+ public @Nullable String getChannelBindingName() {
+ return gs2CbindFlag.getValue();
+ }
+
+ /**
+ * Return the authzid.
+ *
+ * @return the {@code "a=" saslname}
+ */
+ public @Nullable String getAuthzid() {
+ return authzid != null ? castNonNull(authzid).getValue() : null;
+ }
+
+ @Override
+ StringBuilder writeTo(StringBuilder sb) {
+ return StringWritableCsv.writeTo(sb, gs2CbindFlag, authzid);
+ }
+
+ /**
+ * Read a Gs2Header from a String. String may contain trailing fields that will be ignored.
+ *
+ * @param message The String containing the Gs2Header
+ * @return The parsed Gs2Header object
+ * @throws IllegalArgumentException If the format/values of the String do not conform to a
+ * Gs2Header
+ */
+ public static @NotNull Gs2Header parseFrom(@NotNull String message) {
+ checkNotNull(message, "Null message");
+
+ @NotNull
+ String[] gs2HeaderSplit = StringWritableCsv.parseFrom(message, 2);
+ if (gs2HeaderSplit.length == 0) {
+ throw new IllegalArgumentException("Invalid number of fields for the GS2 Header");
+ }
+
+ Gs2AttributeValue gs2cbind = Gs2AttributeValue.parse(castNonNull(gs2HeaderSplit[0]));
+ String authzId = Preconditions.isNullOrEmpty(gs2HeaderSplit[1])
+ ? null
+ : castNonNull(Gs2AttributeValue.parse(gs2HeaderSplit[1])).getValue();
+
+ return new Gs2Header(Gs2CbindFlag.byChar(gs2cbind.getChar()), gs2cbind.getValue(), authzId);
+ }
+
+ private static void checkChannelBinding(@NotNull Gs2CbindFlag cbindFlag,
+ @Nullable String cbName) {
+ checkNotNull(cbindFlag, "cbindFlag");
+ if (cbindFlag == Gs2CbindFlag.CHANNEL_BINDING_REQUIRED ^ cbName != null) {
+ throw new IllegalArgumentException(
+ "Specify required channel binding flag and type together, or none");
+ }
+ if (cbindFlag == Gs2CbindFlag.CHANNEL_BINDING_REQUIRED) {
+ validateChannelBindingType(castNonNull(cbName));
+ }
+ }
+
+ /**
+ * Checks that the channel binding name is valid.
+ *
+ *
+ * gs2-cbind-flag
+ * ("p=" cb-name) / "n" / "y"
+ *
+ * ;; "n" -> client doesn't support channel binding.
+ * ;; "y" -> client does support channel binding
+ * ;; but thinks the server does not.
+ * ;; "p" -> client requires channel binding.
+ * ;; The selected channel binding follows "p=".
+ *
+ * gs2-header
+ * gs2-cbind-flag "," [ authzid ] ","
+ *
+ * ;; GS2 header for SCRAM
+ * ;; (the actual GS2 header includes an optional
+ * ;; flag to indicate that the GSS mechanism is not
+ * ;; "standard", but since SCRAM is "standard", we
+ * ;; don't include that flag).
+ *
+ * authzid
+ * "a=" saslname
+ * {@code
+ * cb-name = 1*(ALPHA / DIGIT / "." / "-")
+ * ;; See RFC 5056, Section 7.
+ * }
+ *
+ * @see IANA
+ * Channel-Binding Types
+ * @param cbname Channel Binding Name
+ * @throws IllegalArgumentException If the name is not a valid channel binding type.
+ */
+ private static void validateChannelBindingType(@NotNull String cbname) {
+ checkNotEmpty(cbname, "cbname");
+ switch (cbname) {
+ // IANA Registered Types
+ case "tls-server-end-point":
+ case "tls-unique":
+ case "tls-exporter":
+ break;
+ default:
+ // https://datatracker.ietf.org/doc/html/rfc5056#section-7
+ for (int i = 0; i < cbname.length(); i++) {
+ char ch = cbname.charAt(i);
+ if (!(ch >= 'A' && ch <= 'Z') && !(ch >= 'a' && ch <= 'z')
+ && !(ch >= '0' && ch <= '9') && !(ch >= '-' && ch <= '.')) {
+ throw new IllegalArgumentException(
+ "Invalid Channel Binding Type name '" + cbname + "'");
+ }
+ }
+ break;
+ }
+ }
+
+}
diff --git a/scram-common/src/main/java/com/ongres/scram/common/ScramAttributeValue.java b/scram-common/src/main/java/com/ongres/scram/common/ScramAttributeValue.java
new file mode 100644
index 0000000..f581bfb
--- /dev/null
+++ b/scram-common/src/main/java/com/ongres/scram/common/ScramAttributeValue.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common;
+
+import static com.ongres.scram.common.util.Preconditions.castNonNull;
+import static com.ongres.scram.common.util.Preconditions.checkNotNull;
+
+import com.ongres.scram.common.exception.ScramParseException;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Parse and write SCRAM Attribute-Value pairs.
+ */
+class ScramAttributeValue extends AbstractCharAttributeValue
+ */
+ CHANNEL_BINDING('c'),
+
+ /**
+ * This attribute specifies the base64-encoded salt used by the server for this user.
+ */
+ SALT('s'),
+
+ /**
+ * This attribute specifies an iteration count for the selected hash function and user.
+ */
+ ITERATION('i'),
+
+ /**
+ * This attribute specifies a base64-encoded ClientProof.
+ */
+ CLIENT_PROOF('p'),
+
+ /**
+ * This attribute specifies a base64-encoded ServerSignature.
+ */
+ SERVER_SIGNATURE('v'),
+
+ /**
+ * This attribute specifies an error that occurred during authentication exchange. Can help
+ * diagnose the reason for the authentication exchange failure.
+ */
+ ERROR('e');
+
+ private final char attributeChar;
+
+ ScramAttributes(char attributeChar) {
+ this.attributeChar = checkNotNull(attributeChar, "attributeChar");
+ }
+
+ @Override
+ public char getChar() {
+ return attributeChar;
+ }
+
+ /**
+ * Find a SCRAMAttribute by its character.
+ *
+ * @param c The character.
+ * @return The SCRAMAttribute that has that character.
+ * @throws ScramParseException If no SCRAMAttribute has this character.
+ */
+ public static ScramAttributes byChar(char c) throws ScramParseException {
+ switch (c) {
+ case 'n':
+ return USERNAME;
+ case 'a':
+ return AUTHZID;
+ case 'r':
+ return NONCE;
+ case 'c':
+ return CHANNEL_BINDING;
+ case 's':
+ return SALT;
+ case 'i':
+ return ITERATION;
+ case 'p':
+ return CLIENT_PROOF;
+ case 'v':
+ return SERVER_SIGNATURE;
+ case 'e':
+ return ERROR;
+ default:
+ throw new ScramParseException("Attribute with char '" + c + "' does not exist");
+ }
+ }
+}
diff --git a/scram-common/src/main/java/com/ongres/scram/common/ScramFunctions.java b/scram-common/src/main/java/com/ongres/scram/common/ScramFunctions.java
new file mode 100644
index 0000000..a129e55
--- /dev/null
+++ b/scram-common/src/main/java/com/ongres/scram/common/ScramFunctions.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+
+import com.ongres.scram.common.util.Preconditions;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Utility functions (mostly crypto) for SCRAM.
+ */
+public final class ScramFunctions {
+
+ private static final byte @NotNull [] CLIENT_KEY_HMAC_MESSAGE = "Client Key".getBytes(UTF_8);
+ private static final byte @NotNull [] SERVER_KEY_HMAC_MESSAGE = "Server Key".getBytes(UTF_8);
+
+ private ScramFunctions() {
+ throw new IllegalStateException("Utility class");
+ }
+
+ /**
+ * Compute the salted password, based on the given SCRAM mechanism, the String preparation
+ * algorithm, the provided salt and the number of iterations.
+ *
+ * {@code
+ * SaltedPassword := Hi(Normalize(password), salt, i)
+ * }
+ *
+ * @param scramMechanism The SCRAM mechanism
+ * @param stringPreparation The String preparation
+ * @param password The non-salted password
+ * @param salt The bytes representing the salt
+ * @param iterationCount The number of iterations
+ * @return The salted password
+ */
+ public static byte @NotNull [] saltedPassword(@NotNull ScramMechanism scramMechanism,
+ @NotNull StringPreparation stringPreparation, char @NotNull [] password,
+ byte @NotNull [] salt, int iterationCount) {
+ return scramMechanism.saltedPassword(stringPreparation, password, salt, iterationCount);
+ }
+
+ /**
+ * Computes the HMAC of the message and key, using the given SCRAM mechanism.
+ *
+ * {@code
+ * HMAC(key, str)
+ * }
+ *
+ * @param scramMechanism The SCRAM mechanism
+ * @param message The message to compute the HMAC
+ * @param key The key used to initialize the MAC
+ * @return The computed HMAC
+ */
+ public static byte @NotNull [] hmac(@NotNull ScramMechanism scramMechanism, byte @NotNull [] key,
+ byte @NotNull [] message) {
+ return scramMechanism.hmac(key, message);
+ }
+
+ /**
+ * Generates a client key, from the salted password.
+ *
+ * {@code
+ * ClientKey := HMAC(SaltedPassword, "Client Key")
+ * }
+ *
+ * @param scramMechanism The SCRAM mechanism
+ * @param saltedPassword The salted password
+ * @return The client key
+ */
+ public static byte[] clientKey(@NotNull ScramMechanism scramMechanism,
+ byte @NotNull [] saltedPassword) {
+ return hmac(scramMechanism, saltedPassword, CLIENT_KEY_HMAC_MESSAGE);
+ }
+
+ /**
+ * Generates a server key, from the salted password.
+ *
+ * {@code
+ * ServerKey := HMAC(SaltedPassword, "Server Key")
+ * }
+ *
+ * @param scramMechanism The SCRAM mechanism
+ * @param saltedPassword The salted password
+ * @return The server key
+ */
+ public static byte[] serverKey(@NotNull ScramMechanism scramMechanism,
+ byte @NotNull [] saltedPassword) {
+ return hmac(scramMechanism, saltedPassword, SERVER_KEY_HMAC_MESSAGE);
+ }
+
+ /**
+ * Computes the hash function of a given value, based on the SCRAM mechanism hash function.
+ *
+ * {@code
+ * H(str)
+ * }
+ *
+ * @param scramMechanism The SCRAM mechanism
+ * @param message The message to hash
+ * @return The hashed value
+ */
+ public static byte[] hash(@NotNull ScramMechanism scramMechanism, byte @NotNull [] message) {
+ return scramMechanism.digest(message);
+ }
+
+ /**
+ * Generates a stored key, from the salted password.
+ *
+ * {@code
+ * StoredKey := H(ClientKey)
+ * }
+ *
+ * @param scramMechanism The SCRAM mechanism
+ * @param clientKey The client key
+ * @return The stored key
+ */
+ public static byte[] storedKey(@NotNull ScramMechanism scramMechanism,
+ byte @NotNull [] clientKey) {
+ return hash(scramMechanism, clientKey);
+ }
+
+ /**
+ * Computes the SCRAM client signature.
+ *
+ * {@code
+ * ClientSignature := HMAC(StoredKey, AuthMessage)
+ * }
+ *
+ * @param scramMechanism The SCRAM mechanism
+ * @param storedKey The stored key
+ * @param authMessage The auth message
+ * @return The client signature
+ */
+ public static byte @NotNull [] clientSignature(@NotNull ScramMechanism scramMechanism,
+ byte @NotNull [] storedKey, @NotNull String authMessage) {
+ return hmac(scramMechanism, storedKey, authMessage.getBytes(UTF_8));
+ }
+
+ /**
+ * Computes the SCRAM client proof to be sent to the server on the client-final-message.
+ *
+ * {@code
+ * ClientProof := ClientKey XOR ClientSignature
+ * }
+ *
+ * @param clientKey The client key
+ * @param clientSignature The client signature
+ * @return The client proof
+ */
+ public static byte[] clientProof(byte @NotNull [] clientKey, byte @NotNull [] clientSignature) {
+ return CryptoUtil.xor(clientKey, clientSignature);
+ }
+
+ /**
+ * Compute the SCRAM server signature.
+ *
+ * {@code
+ * ServerSignature := HMAC(ServerKey, AuthMessage)
+ * }
+ *
+ * @param scramMechanism The SCRAM mechanism
+ * @param serverKey The server key
+ * @param authMessage The auth message
+ * @return The server signature
+ */
+ public static byte @NotNull [] serverSignature(@NotNull ScramMechanism scramMechanism,
+ byte @NotNull [] serverKey, @NotNull String authMessage) {
+ return hmac(scramMechanism, serverKey, authMessage.getBytes(UTF_8));
+ }
+
+ /**
+ * Verifies that a provided client proof is correct.
+ *
+ * @param scramMechanism The SCRAM mechanism
+ * @param clientProof The provided client proof
+ * @param storedKey The stored key
+ * @param authMessage The auth message
+ * @return True if the client proof is correct
+ */
+ public static boolean verifyClientProof(
+ @NotNull ScramMechanism scramMechanism, byte @NotNull [] clientProof,
+ byte @NotNull [] storedKey, @NotNull String authMessage) {
+ byte[] clientSignature = clientSignature(scramMechanism, storedKey, authMessage);
+ byte[] clientKey = CryptoUtil.xor(clientSignature, clientProof);
+ byte[] computedStoredKey = hash(scramMechanism, clientKey);
+ return MessageDigest.isEqual(storedKey, computedStoredKey);
+ }
+
+ /**
+ * Verifies that a provided server proof is correct.
+ *
+ * @param scramMechanism The SCRAM mechanism
+ * @param serverKey The server key
+ * @param authMessage The auth message
+ * @param serverSignature The provided server signature
+ * @return True if the server signature is correct
+ */
+ public static boolean verifyServerSignature(
+ ScramMechanism scramMechanism, byte[] serverKey, String authMessage, byte[] serverSignature) {
+ byte[] computedServerSignature = serverSignature(scramMechanism, serverKey, authMessage);
+ return MessageDigest.isEqual(serverSignature, computedServerSignature);
+ }
+
+ /**
+ * Generates a random string (called a 'nonce'), composed of ASCII printable characters, except
+ * comma (',').
+ *
+ * @param nonceSize The length of the nonce, in characters/bytes
+ * @param random The SecureRandom to use
+ * @return The String representing the nonce
+ * @throws IllegalArgumentException if the nonceSize is not positive, or if random is null
+ */
+ public static String nonce(int nonceSize, SecureRandom random) {
+ Preconditions.gt0(nonceSize, "nonceSize");
+ Preconditions.checkNotNull(random, "random");
+ final StringBuilder nonceBuilder = new StringBuilder(nonceSize);
+ while (nonceBuilder.length() < nonceSize) {
+ int codePoint = random.nextInt(0x7E - 0x21 + 1) + 0x21;
+ if (codePoint != ',') {
+ nonceBuilder.append((char) codePoint);
+ }
+ }
+ return nonceBuilder.toString();
+ }
+
+ /**
+ * Generates a random salt that can be used to generate a salted password.
+ *
+ * @param saltSize The length of the salt, in bytes
+ * @param random The SecureRandom to use
+ * @return The bye[] representing the salt
+ * @throws IllegalArgumentException if the saltSize is not positive, or if random is null
+ */
+ public static byte @NotNull [] salt(int saltSize, @NotNull SecureRandom random) {
+ return CryptoUtil.salt(saltSize, random);
+ }
+
+ /**
+ * The AuthMessage is computed by concatenating messages from the authentication exchange.
+ *
+ * {@code
+ * AuthMessage := client-first-message-bare + "," +
+ * server-first-message + "," +
+ * client-final-message-without-proof
+ * }
+ *
+ * @param clientFirstMessage the {@link ClientFirstMessage ClientFirstMessage}
+ * @param serverFirstMessage the {@link ServerFirstMessage ServerFirstMessage}
+ * @param cbindData the channel binding data, or null
+ * @return the AuthMessage
+ */
+ public static String authMessage(ClientFirstMessage clientFirstMessage,
+ ServerFirstMessage serverFirstMessage, byte[] cbindData) {
+ StringBuilder sb = clientFirstMessage.clientFirstMessageBare(new StringBuilder(96))
+ .append(',').append(serverFirstMessage).append(',');
+ ClientFinalMessage.withoutProof(sb, clientFirstMessage.getGs2Header(),
+ cbindData, serverFirstMessage.getNonce());
+ return sb.toString();
+ }
+
+}
diff --git a/scram-common/src/main/java/com/ongres/scram/common/ScramMechanism.java b/scram-common/src/main/java/com/ongres/scram/common/ScramMechanism.java
new file mode 100644
index 0000000..144bfc5
--- /dev/null
+++ b/scram-common/src/main/java/com/ongres/scram/common/ScramMechanism.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common;
+
+import static com.ongres.scram.common.util.Preconditions.checkNotNull;
+import static com.ongres.scram.common.util.Preconditions.gt0;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import javax.crypto.Mac;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.SecretKeySpec;
+
+import com.ongres.scram.common.exception.ScramRuntimeException;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.Unmodifiable;
+
+/**
+ * SCRAM Mechanisms supported by this library. At least, {@code SCRAM-SHA-1} and
+ * {@code SCRAM-SHA-256} are provided, since both the hash and the HMAC implementations are provided
+ * by the Java JDK version 8 or greater.
+ *
+ *
+ *
+ *
+ * @implNote {@code extensions} are not supported.
+ * @see [RFC5802] Section 7
+ */
+public final class ServerFinalMessage extends AbstractScramMessage {
+
+ private final byte @Nullable [] verifier;
+ private final @Nullable String serverError;
+
+ /**
+ * Constructs a server-final-message with no errors, and the provided server verifier.
+ *
+ * @param verifier The bytes of the computed signature
+ * @throws IllegalArgumentException If the verifier is null
+ */
+ public ServerFinalMessage(byte @NotNull [] verifier) {
+ this.verifier = checkNotNull(verifier, "verifier");
+ this.serverError = null;
+ }
+
+ /**
+ * Constructs a server-final-message which represents a SCRAM error.
+ *
+ * @param serverError The error message
+ * @throws IllegalArgumentException If the error is null
+ */
+ public ServerFinalMessage(@NotNull String serverError) {
+ this.serverError = validateServerErrorType(serverError);
+ this.verifier = null;
+ }
+
+ /**
+ * Whether this server-final-message contains an error.
+ *
+ * @return True if it contains an error, false if it contains a verifier
+ */
+ public boolean isError() {
+ return null != serverError;
+ }
+
+ /**
+ * Get the verifier value from the "v=" server-final-message.
+ *
+ * @return the {@code verifier}
+ */
+ public byte @Nullable [] getVerifier() {
+ return verifier != null ? checkNotNull(verifier, "verifier").clone() : null;
+ }
+
+ /**
+ * Get the server-error-value from the "e=" server-final-message.
+ *
+ * @return the {@code server-error-value}
+ */
+ public @Nullable String getServerError() {
+ return serverError;
+ }
+
+ /**
+ * Parses a server-final-message from a String.
+ *
+ * @param serverFinalMessage The message
+ * @return A constructed server-final-message instance
+ * @throws ScramParseException If the argument is not a valid server-final-message
+ * @throws IllegalArgumentException If the message is null or empty
+ */
+ public static @NotNull ServerFinalMessage parseFrom(@NotNull String serverFinalMessage)
+ throws ScramParseException {
+ checkNotEmpty(serverFinalMessage, "serverFinalMessage");
+
+ @NotNull
+ String @NotNull [] attributeValues = StringWritableCsv.parseFrom(serverFinalMessage, 1, 0);
+ if (attributeValues.length != 1) {
+ throw new ScramParseException("Invalid server-final-message");
+ }
+
+ ScramAttributeValue attributeValue = ScramAttributeValue.parse(attributeValues[0]);
+ if (ScramAttributes.SERVER_SIGNATURE.getChar() == attributeValue.getChar()) {
+ byte[] verifier = ScramStringFormatting.base64Decode(attributeValue.getValue());
+ return new ServerFinalMessage(verifier);
+ } else if (ScramAttributes.ERROR.getChar() == attributeValue.getChar()) {
+ return new ServerFinalMessage(attributeValue.getValue());
+ } else {
+ throw new ScramParseException(
+ "Invalid server-final-message: it must contain either a verifier or an error attribute");
+ }
+ }
+
+ @Override
+ StringBuilder writeTo(StringBuilder sb) {
+ return StringWritableCsv.writeTo(
+ sb,
+ isError()
+ ? new ScramAttributeValue(ScramAttributes.ERROR, castNonNull(serverError))
+ : new ScramAttributeValue(ScramAttributes.SERVER_SIGNATURE,
+ ScramStringFormatting.base64Encode(castNonNull(verifier))));
+ }
+
+ private static String validateServerErrorType(@NotNull String serverError) {
+ checkNotNull(serverError, "serverError");
+ if (ServerErrorValue.getErrorMessage(serverError) == null) {
+ throw new IllegalArgumentException(
+ "Invalid server-error-value '" + serverError + "'");
+ }
+ return serverError;
+ }
+}
diff --git a/scram-common/src/main/java/com/ongres/scram/common/ServerFirstMessage.java b/scram-common/src/main/java/com/ongres/scram/common/ServerFirstMessage.java
new file mode 100644
index 0000000..53c8f56
--- /dev/null
+++ b/scram-common/src/main/java/com/ongres/scram/common/ServerFirstMessage.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common;
+
+import static com.ongres.scram.common.util.Preconditions.castNonNull;
+import static com.ongres.scram.common.util.Preconditions.checkNotEmpty;
+import static com.ongres.scram.common.util.Preconditions.checkNotNull;
+import static com.ongres.scram.common.util.Preconditions.gt0;
+
+import com.ongres.scram.common.exception.ScramParseException;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Constructs and parses {@code server-first-messages}.
+ *
+ *
+ *
+ * server-error
+ * "e=" server-error-value
+ *
+ *
+ * server-error-value
+ * "invalid-encoding" /
+ *
+ * "extensions-not-supported" / ; unrecognized 'm' value
+ * "invalid-proof" /
+ * "channel-bindings-dont-match" /
+ * "server-does-support-channel-binding" /
+ * ; server does not support channel binding
+ * "channel-binding-not-supported" /
+ * "unsupported-channel-binding-type" /
+ * "unknown-user" /
+ * "invalid-username-encoding" /
+ * ; invalid username encoding (invalid UTF-8 or
+ * ; SASLprep failed)
+ * "no-resources" /
+ * "other-error"
+ * ; Unrecognized errors should be treated as "other-error".
+ * ; In order to prevent information disclosure, the server
+ * ; may substitute the real reason with "other-error".
+ *
+ * verifier
+ * "v=" base64
+ *
+ * ;; base-64 encoded ServerSignature.
+ *
+ * server-final-message
+ * (server-error / verifier)
+ *
+ * ["," extensions]
+ *
+ *
+ * @implNote {@code extensions} are not supported.
+ * @see [RFC5802] Section 7
+ */
+public final class ServerFirstMessage extends AbstractScramMessage {
+
+ private final @NotNull String clientNonce;
+ private final @NotNull String serverNonce;
+ private final @NotNull String salt;
+ private final int iterationCount;
+
+ /**
+ * Constructs a server-first-message from a client-first-message and the additional required data.
+ *
+ *
+ *
+ * nonce
+ * "r=" c-nonce [s-nonce]
+ *
+ * ;; Second part provided by server.
+ *
+ * salt
+ * "s=" base64
+ *
+ *
+ * server-first-message
+ * [reserved-mext ","] nonce "," salt ",
+ *
+ * "iteration-count ["," extensions]
+ *
+ *
+ * @param clientNonce The c-nonce used in client-first-message
+ * @param serverNonce The s-nonce returned by the server
+ * @param salt The salt
+ * @param iterationCount The iteration count (must be positive)
+ * @throws IllegalArgumentException If clientFirstMessage, serverNonce or salt are null or empty,
+ * or iteration < 1
+ */
+ public ServerFirstMessage(@NotNull String clientNonce, @NotNull String serverNonce,
+ @NotNull String salt, int iterationCount) {
+ this.clientNonce = checkNotEmpty(clientNonce, "clientNonce");
+ this.serverNonce = checkNotEmpty(serverNonce, "serverNonce");
+ this.salt = checkNotNull(salt, "salt");
+ this.iterationCount = gt0(iterationCount, "iterationCount");
+ }
+
+ /**
+ * The client nonce.
+ *
+ * @return The client nonce
+ */
+ public @NotNull String getClientNonce() {
+ return clientNonce;
+ }
+
+ /**
+ * The server nonce.
+ *
+ * @return The server nonce
+ */
+ public @NotNull String getServerNonce() {
+ return serverNonce;
+ }
+
+ /**
+ * The concatenation of the client nonce and the server nonce: {@code c-nonce [s-nonce]}.
+ *
+ * @return The nonce
+ */
+ public @NotNull String getNonce() {
+ return clientNonce + serverNonce;
+ }
+
+ /**
+ * The salt in base64.
+ *
+ * @return The salt in base64.
+ */
+ public String getSalt() {
+ return salt;
+ }
+
+ /**
+ * The number of iterations.
+ *
+ * @return The number of iterations.
+ */
+ public int getIterationCount() {
+ return iterationCount;
+ }
+
+ /**
+ * Parses a server-first-message from a String.
+ *
+ * @param serverFirstMessage The string representing the server-first-message
+ * @param clientNonce The clientNonce that is present in the client-first-message
+ * @return The parsed instance
+ * @throws ScramParseException If the argument is not a valid server-first-message
+ * @throws IllegalArgumentException If either argument is empty or serverFirstMessage is not a
+ * valid message
+ */
+ public static @NotNull ServerFirstMessage parseFrom(@NotNull String serverFirstMessage,
+ @NotNull String clientNonce) throws ScramParseException {
+ checkNotEmpty(serverFirstMessage, "serverFirstMessage");
+ checkNotEmpty(clientNonce, "clientNonce");
+
+ String[] attributeValues = StringWritableCsv.parseFrom(serverFirstMessage, 3, 0);
+ if (attributeValues.length != 3) {
+ throw new ScramParseException("Invalid server-first-message");
+ }
+
+ ScramAttributeValue nonce = ScramAttributeValue.parse(castNonNull(attributeValues[0]));
+ if (ScramAttributes.NONCE.getChar() != nonce.getChar()) {
+ throw new ScramParseException(
+ "nonce must be the 1st element of the server-first-message");
+ }
+ if (!nonce.getValue().startsWith(clientNonce)) {
+ throw new ScramParseException("parsed nonce does not start with client nonce");
+ }
+
+ ScramAttributeValue salt = ScramAttributeValue.parse(castNonNull(attributeValues[1]));
+ if (ScramAttributes.SALT.getChar() != salt.getChar()) {
+ throw new ScramParseException("salt must be the 2nd element of the server-first-message");
+ }
+
+ ScramAttributeValue iteration = ScramAttributeValue.parse(castNonNull(attributeValues[2]));
+ if (ScramAttributes.ITERATION.getChar() != iteration.getChar()) {
+ throw new ScramParseException(
+ "iteration must be the 3rd element of the server-first-message");
+ }
+
+ int iterationInt;
+ try {
+ iterationInt = Integer.parseInt(iteration.getValue());
+ } catch (NumberFormatException ex) {
+ throw new ScramParseException("invalid iteration", ex);
+ }
+
+ return new ServerFirstMessage(clientNonce, nonce.getValue().substring(clientNonce.length()),
+ salt.getValue(), iterationInt);
+ }
+
+ @Override
+ StringBuilder writeTo(StringBuilder sb) {
+ return StringWritableCsv.writeTo(
+ sb,
+ new ScramAttributeValue(ScramAttributes.NONCE, getNonce()),
+ new ScramAttributeValue(ScramAttributes.SALT, salt),
+ new ScramAttributeValue(ScramAttributes.ITERATION, Integer.toString(iterationCount)));
+ }
+
+}
diff --git a/scram-common/src/main/java/com/ongres/scram/common/StringPreparation.java b/scram-common/src/main/java/com/ongres/scram/common/StringPreparation.java
new file mode 100644
index 0000000..82d45f3
--- /dev/null
+++ b/scram-common/src/main/java/com/ongres/scram/common/StringPreparation.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common;
+
+/**
+ * StringPreparations enumerations to use in SCRAM.
+ */
+public enum StringPreparation {
+
+ /**
+ * Implementation of StringPreparation that performs no preparation. Non US-ASCII characters will
+ * produce an exception.
+ *
+ *
+ *
+ * server-error
+ * "e=" server-error-value
+ *
+ *
+ * verifier
+ * "v=" base64
+ *
+ * ;; base-64 encoded ServerSignature.
+ *
+ * server-final-message
+ * (server-error / verifier)
+ *
+ * ["," extensions]The SCRAM specification dictates that the password is also in UTF-8, and is
+ * processed with the {@code SASLprep} algorithm. PostgreSQL, however, does not require UTF-8 to
+ * be used for the password. When a user's password is set, it is processed with SASLprep as if it
+ * was in UTF-8, regardless of the actual encoding used. However, if it is not a legal UTF-8 byte
+ * sequence, or it contains UTF-8 byte sequences that are prohibited by the SASLprep algorithm,
+ * the raw password will be used without SASLprep processing, instead of throwing an error. This
+ * allows the password to be normalized when it is in UTF-8, but still allows a non-UTF-8 password
+ * to be used, and doesn't require the system to know which encoding the password is
+ * in.
+ *
+ * @see PostgreSQL
+ * SCRAM-SHA-256 Authentication
+ */
+ POSTGRESQL_PREPARATION {
+ @Override
+ char[] doNormalize(char[] value) {
+ try {
+ return ScramStringFormatting.SASL_PREP.prepareStored(value);
+ } catch (IllegalArgumentException ex) {
+ // the raw password will be used without SASLprep processing
+ return value;
+ }
+ }
+ };
+
+ abstract char[] doNormalize(char[] value);
+
+ /**
+ * Normalize acording the selected preparation.
+ *
+ * @param value array of chars to normalize
+ * @return the normalized array of chars
+ * @throws IllegalArgumentException if the string is null or empty
+ */
+ public char[] normalize(char[] value) {
+ if (null == value || value.length == 0) {
+ throw new IllegalArgumentException("Empty string for value");
+ }
+
+ char[] normalized = doNormalize(value);
+
+ if (null == normalized || normalized.length == 0) {
+ throw new IllegalArgumentException("null or empty value after normalization");
+ }
+
+ return normalized;
+ }
+}
diff --git a/scram-common/src/main/java/com/ongres/scram/common/StringWritable.java b/scram-common/src/main/java/com/ongres/scram/common/StringWritable.java
new file mode 100644
index 0000000..176730f
--- /dev/null
+++ b/scram-common/src/main/java/com/ongres/scram/common/StringWritable.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Abstract class to denote classes which can write to a StringBuffer.
+ */
+abstract class StringWritable {
+
+ /**
+ * Write the class information to the given StringBuffer.
+ *
+ * @param sb Where to write the data.
+ * @return The same StringBuffer.
+ */
+ abstract @NotNull StringBuilder writeTo(@NotNull StringBuilder sb);
+
+}
diff --git a/scram-common/src/main/java/com/ongres/scram/common/StringWritableCsv.java b/scram-common/src/main/java/com/ongres/scram/common/StringWritableCsv.java
new file mode 100644
index 0000000..a6497c1
--- /dev/null
+++ b/scram-common/src/main/java/com/ongres/scram/common/StringWritableCsv.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common;
+
+import static com.ongres.scram.common.util.Preconditions.checkNotNull;
+
+import java.util.Arrays;
+
+import com.ongres.scram.common.util.Preconditions;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Helper class to generate Comma Separated Values of StringWritables.
+ */
+final class StringWritableCsv {
+
+ private StringWritableCsv() {
+ throw new IllegalStateException("Utility class");
+ }
+
+ /**
+ * Write a sequence of StringWritableCsv to a StringBuffer. Null StringWritables are not printed,
+ * but separator is still used. Separator is a comma (',')
+ *
+ * @param sb The sb to write to
+ * @param values Zero or more attribute-value pairs to write
+ * @return The same sb, with data filled in (if any)
+ * @throws IllegalArgumentException If sb is null
+ */
+ static @NotNull StringBuilder writeTo(@NotNull StringBuilder sb,
+ @Nullable StringWritable... values) {
+ checkNotNull(sb, "sb");
+ if (null == values || values.length == 0) {
+ return sb;
+ }
+
+ if (null != values[0]) {
+ Preconditions.castNonNull(values[0]).writeTo(sb);
+ }
+ int i = 1;
+ while (i < values.length) {
+ sb.append(',');
+ if (null != values[i]) {
+ Preconditions.castNonNull(values[i]).writeTo(sb);
+ }
+ i++;
+ }
+
+ return sb;
+ }
+
+ /**
+ * Parse a String with a StringWritableCsv into its composing Strings represented as Strings. No
+ * validation is performed on the individual attribute-values returned.
+ *
+ * @param value The String with the set of attribute-values
+ * @param n Number of entries to return (entries will be null of there were not enough). 0 means
+ * unlimited
+ * @param offset How many entries to skip before start returning
+ * @return An array of Strings which represent the individual attribute-values
+ * @throws IllegalArgumentException If value is null or either n or offset are negative
+ */
+ static @NotNull String @NotNull [] parseFrom(@NotNull String value, int n, int offset) {
+ checkNotNull(value, "value");
+ if (n < 0 || offset < 0) {
+ throw new IllegalArgumentException("Limit and offset have to be >= 0");
+ }
+
+ if (value.isEmpty()) {
+ return new String[0];
+ }
+
+ String[] split = value.split(",", -1);
+ if (split.length < offset) {
+ throw new IllegalArgumentException("Not enough items for the given offset");
+ }
+
+ return Arrays.copyOfRange(
+ split,
+ offset,
+ (n == 0 ? split.length : n) + offset);
+ }
+
+ /**
+ * Parse a String with a StringWritableCsv into its composing Strings represented as Strings. No
+ * validation is performed on the individual attribute-values returned. Elements are returned
+ * starting from the first available attribute-value.
+ *
+ * @param value The String with the set of attribute-values
+ * @param n Number of entries to return (entries will be null of there were not enough). 0 means
+ * unlimited
+ * @return An array of Strings which represent the individual attribute-values
+ * @throws IllegalArgumentException If value is null or n is negative
+ */
+ static @NotNull String @NotNull [] parseFrom(@NotNull String value, int n) {
+ return parseFrom(value, n, 0);
+ }
+
+ /**
+ * Parse a String with a StringWritableCsv into its composing Strings represented as Strings. No
+ * validation is performed on the individual attribute-values returned. All the available
+ * attribute-values will be returned.
+ *
+ * @param value The String with the set of attribute-values
+ * @return An array of Strings which represent the individual attribute-values
+ * @throws IllegalArgumentException If value is null
+ */
+ static @NotNull String @NotNull [] parseFrom(@NotNull String value) {
+ return parseFrom(value, 0, 0);
+ }
+}
diff --git a/scram-common/src/main/java/com/ongres/scram/common/UsAsciiUtils.java b/scram-common/src/main/java/com/ongres/scram/common/UsAsciiUtils.java
new file mode 100644
index 0000000..ed8390b
--- /dev/null
+++ b/scram-common/src/main/java/com/ongres/scram/common/UsAsciiUtils.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common;
+
+import static com.ongres.scram.common.util.Preconditions.checkNotNull;
+
+import java.nio.CharBuffer;
+
+/**
+ * Utility to remove non-printable characters from the US-ASCII String.
+ */
+final class UsAsciiUtils {
+
+ private UsAsciiUtils() {
+ throw new IllegalStateException("Utility class");
+ }
+
+ /**
+ * Removes non-printable characters from the US-ASCII String.
+ *
+ * @param value The original String
+ * @return The possibly modified String, without non-printable US-ASCII characters.
+ * @throws IllegalArgumentException If the String is null or contains non US-ASCII characters.
+ */
+ static char[] toPrintable(final char[] value) {
+ checkNotNull(value, "value");
+ CharBuffer charBuffer = CharBuffer.allocate(value.length);
+ for (char ch : value) {
+ if (ch >= 127) {
+ throw new IllegalArgumentException(
+ "value contains character '" + ch + "' which is non US-ASCII");
+ } else if (ch >= 32) {
+ charBuffer.put(ch);
+ }
+ }
+ // Flip the buffer to prepare for reading
+ charBuffer.flip();
+ char[] charArray = new char[charBuffer.remaining()];
+ charBuffer.get(charArray);
+ return charArray;
+ }
+
+ /**
+ * Removes non-printable characters from the US-ASCII String.
+ *
+ * @param value The original String
+ * @return The possibly modified String, without non-printable US-ASCII characters.
+ * @throws IllegalArgumentException If the String is null or contains non US-ASCII characters.
+ */
+ static String toPrintable(final String value) {
+ char[] charArray = checkNotNull(value, "value").toCharArray();
+ return new String(toPrintable(charArray));
+ }
+}
diff --git a/scram-common/src/main/java/com/ongres/scram/common/exception/ScramException.java b/scram-common/src/main/java/com/ongres/scram/common/exception/ScramException.java
new file mode 100644
index 0000000..fb7f570
--- /dev/null
+++ b/scram-common/src/main/java/com/ongres/scram/common/exception/ScramException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common.exception;
+
+/**
+ * This class represents an generic error when using SCRAM, which is a SASL method.
+ */
+public class ScramException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructs a new instance of ScramException with a detailed message.
+ *
+ * @param detail A String containing details about the exception
+ */
+ public ScramException(String detail) {
+ super(detail);
+ }
+
+ /**
+ * Constructs a new instance of ScramException with a detailed message and a root cause.
+ *
+ * @param detail A String containing details about the exception
+ * @param ex The root exception
+ */
+ public ScramException(String detail, Throwable ex) {
+ super(detail, ex);
+ }
+}
diff --git a/scram-common/src/main/java/com/ongres/scram/common/exception/ScramInvalidServerSignatureException.java b/scram-common/src/main/java/com/ongres/scram/common/exception/ScramInvalidServerSignatureException.java
new file mode 100644
index 0000000..d3517da
--- /dev/null
+++ b/scram-common/src/main/java/com/ongres/scram/common/exception/ScramInvalidServerSignatureException.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2017 OnGres, Inc.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+package com.ongres.scram.common.exception;
+
+/**
+ * This class represents an error when verifying the a base64-encoded ServerSignature in a
+ * {@code server-final-message}.
+ *
+ *