pax_global_header00006660000000000000000000000064152103124750014512gustar00rootroot0000000000000052 comment=f6bd6e20e34d4989ee34b06825791657a2cfd4fe ongres-scram-f6bd6e2/000077500000000000000000000000001521031247500146125ustar00rootroot00000000000000ongres-scram-f6bd6e2/.editorconfig000066400000000000000000000002231521031247500172640ustar00rootroot00000000000000root = true [*] charset = utf-8 indent_style = space indent_size = 2 trim_trailing_whitespace = true end_of_line = lf insert_final_newline = true ongres-scram-f6bd6e2/.github/000077500000000000000000000000001521031247500161525ustar00rootroot00000000000000ongres-scram-f6bd6e2/.github/dependabot.yml000066400000000000000000000006411521031247500210030ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "maven" directories: - "/" - "/scram-client/src/it/jpms-scram-client" schedule: interval: "monthly" groups: all-maven-dependencies: patterns: - "*" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" groups: all-github-actions: patterns: - "*" ongres-scram-f6bd6e2/.github/workflows/000077500000000000000000000000001521031247500202075ustar00rootroot00000000000000ongres-scram-f6bd6e2/.github/workflows/codeql.yml000066400000000000000000000052351521031247500222060ustar00rootroot00000000000000name: "CodeQL Advanced" on: push: branches: [ "main" ] pull_request: branches: [ "main" ] schedule: - cron: '42 10 * * 1' permissions: # added using https://github.com/step-security/secure-repo contents: read jobs: analyze: name: Analyze (${{ matrix.language }}) # Runner size impacts CodeQL analysis time. To learn more, please see: # - https://gh.io/recommended-hardware-resources-for-running-codeql # - https://gh.io/supported-runners-and-hardware-resources # - https://gh.io/using-larger-runners (GitHub.com only) # Consider using larger runners or machines with greater resources for possible analysis time improvements. runs-on: 'ubuntu-latest' permissions: # required for all workflows security-events: write # required to fetch internal or private CodeQL packs packages: read # only required for workflows in private repositories actions: read contents: read strategy: fail-fast: false matrix: include: - language: java-kotlin build-mode: manual # This mode only analyzes Java. Set this to 'autobuild' or 'manual' to analyze Kotlin too. steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality - if: matrix.build-mode == 'manual' name: Set up JDK 21 uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: java-version: '21' distribution: 'temurin' cache: maven - if: matrix.build-mode == 'manual' shell: bash run: ./mvnw package -P release -DskipTests -Dmaven.javadoc.skip -Dgpg.skip - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 with: category: "/language:${{matrix.language}}" ongres-scram-f6bd6e2/.github/workflows/maven.yml000066400000000000000000000026721521031247500220470ustar00rootroot00000000000000# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven name: Java CI with Maven on: push: branches: ["main"] pull_request: branches: ["main"] permissions: # added using https://github.com/step-security/secure-repo contents: read jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up JDK 21 uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: java-version: '21' distribution: 'zulu' cache: maven - name: Build with Maven run: ./mvnw -B verify -P checks,run-its # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive dependency-submission: runs-on: ubuntu-latest permissions: contents: write #required for POST snapshot API https://docs.github.com/en/rest/dependency-graph/dependency-submission#create-a-snapshot-of-dependencies-for-a-repository steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Update dependency graph uses: advanced-security/maven-dependency-submission-action@b275d12641ac2d2108b2cbb7598b154ad2f2cee8 ongres-scram-f6bd6e2/.github/workflows/scorecard.yml000066400000000000000000000065431521031247500227070ustar00rootroot00000000000000# This workflow uses actions that are not certified by GitHub. They are provided # by a third-party and are governed by separate terms of service, privacy # policy, and support documentation. name: Scorecard supply-chain security on: # For Branch-Protection check. Only the default branch is supported. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection branch_protection_rule: # To guarantee Maintained check is occasionally updated. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained schedule: - cron: '20 5 * * 2' push: branches: [ "main" ] # Declare default permissions as read only. permissions: read-all jobs: analysis: name: Scorecard analysis runs-on: ubuntu-latest # `publish_results: true` only works when run from the default branch. conditional can be removed if disabled. if: github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request' permissions: # Needed to upload the results to code-scanning dashboard. security-events: write # Needed to publish results and get a badge (see publish_results below). id-token: write # Uncomment the permissions below if installing in a private repository. # contents: read # actions: read steps: - name: "Checkout code" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Run analysis" uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 with: results_file: results.sarif results_format: sarif # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: # - you want to enable the Branch-Protection check on a *public* repository, or # - you are installing Scorecard on a *private* repository # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. # repo_token: ${{ secrets.SCORECARD_TOKEN }} # Public repositories: # - Publish results to OpenSSF REST API for easy access by consumers # - Allows the repository to include the Scorecard badge. # - See https://github.com/ossf/scorecard-action#publishing-results. # For private repositories: # - `publish_results` will always be set to `false`, regardless # of the value entered here. publish_results: true # (Optional) Uncomment file_mode if you have a .gitattributes with files marked export-ignore # file_mode: git # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: SARIF file path: results.sarif retention-days: 5 # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" uses: github/codeql-action/upload-sarif@v4 with: sarif_file: results.sarif ongres-scram-f6bd6e2/.gitignore000066400000000000000000000024361521031247500166070ustar00rootroot00000000000000### 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 ongres-scram-f6bd6e2/.gitlab-ci.yml000066400000000000000000000003271521031247500172500ustar00rootroot00000000000000image: eclipse-temurin:21-jdk stages: - build variables: M2_HOME: ".m2/maven" MAVEN_OPTS: "-Dmaven.repo.local=.m2" build: stage: build cache: paths: - .m2/ script: - ./mvnw clean verify ongres-scram-f6bd6e2/.mvn/000077500000000000000000000000001521031247500154705ustar00rootroot00000000000000ongres-scram-f6bd6e2/.mvn/maven.config000066400000000000000000000002071521031247500177640ustar00rootroot00000000000000--strict-checksums --show-version --errors --fail-fast -DinstallAtEnd=true -DdeployAtEnd=true -DrootDirectory=${session.rootDirectory} ongres-scram-f6bd6e2/.mvn/wrapper/000077500000000000000000000000001521031247500171505ustar00rootroot00000000000000ongres-scram-f6bd6e2/.mvn/wrapper/maven-wrapper.properties000066400000000000000000000003771521031247500240610ustar00rootroot00000000000000wrapperVersion=3.3.4 distributionType=only-script distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zip distributionSha256Sum=5af3b743dd8b876b5c45da33b676251e5f1687712644abb4ee519ca56e1d89ce ongres-scram-f6bd6e2/CHANGELOG.md000066400000000000000000000073041521031247500164270ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. ## [Unreleased] ## [3.3] - 2026-06-04 ### :lock: Security - Prevent silent downgrade attacks during channel binding negotiation via unsupported certificate algorithms. [GHSA-p9jg-fcr6-3mhf](https://github.com/ongres/scram/security/advisories/GHSA-p9jg-fcr6-3mhf) - Harden memory security by explicitly zeroing out highly sensitive cryptographic keys (`saltedPassword`, `clientKey`, and `serverKey`) immediately following the client final message exchange to prevent lingering material in heap memory. ### :rocket: New features - Implement an interrupt-aware implementation of the PBKDF2 'hi' function introducing `ScramInterruptedException`, utilizing a stride-based check to allow long-running cryptographic operations to safely abort without blocking thread shutdown. - Introduce `.channelBindingPolicy()` to the client builder to explicitly configure `DISABLE`, `ALLOW` (default), and `REQUIRE` enforcement modes. - Introduce `MechanismNegotiationException` and `ChannelBindingException` runtime exception hierarchy to provide granular, precise failure types for driver integration loops instead of relying on generic `IllegalArgumentException` throws. - Add support for `RSASSA-PSS` server certificate signature extraction to ensure modern cryptographic algorithms are supported during `tls-server-end-point` channel binding computations. - Add support for `SCRAM-SHA3-512` and `SCRAM-SHA3-512-PLUS` SASL mechanisms to provide modern NIST SHA-3 hashing standards with higher cryptographic resilience against length-extension attacks (supported on modern JVMs only). ### :building_construction: Improvements - Update the `saslprep` dependency to 2.4. - Updated internal Maven plugins and project dependencies to their latest stable versions. ## [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 [3.3]: https://github.com/ongres/scram/compare/3.2...3.3 [Unreleased]: https://github.com/ongres/scram/compare/3.3...main ongres-scram-f6bd6e2/LICENSE000066400000000000000000000023651521031247500156250ustar00rootroot00000000000000Copyright (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: 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. ongres-scram-f6bd6e2/README.md000066400000000000000000000115071521031247500160750ustar00rootroot00000000000000# SCRAM Java Implementation ![Maven Central Version](https://img.shields.io/maven-central/v/com.ongres.scram/scram-aggregator) [![Reproducible Builds](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/jvm-repo-rebuild/reproducible-central/master/content/com/ongres/scram/badge.json)](https://github.com/jvm-repo-rebuild/reproducible-central/blob/master/content/com/ongres/scram/README.md) ![GitHub License](https://img.shields.io/github/license/ongres/scram) > 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). 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. 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. The code is licensed under the BSD "Simplified 2 Clause" license (see [LICENSE](LICENSE)). ## Key Features * Clean-room Implementation: The code is written from scratch, offering a reliable and independent solution. * Modular Structure: The library is designed for modularity, promoting reusability and maintainability. * Client-Server Support: The implementation caters to both client and server-side SCRAM usage in the `scram-common` module. For the moment only the `scram-client` module is implemented. * Multiple Hashing Algorithms: It supports `SHA-1` and `SHA-256` as described in the official RFC 5802 and RFC 7677 respectively, and also provides `SHA-224`, `SHA-384` and `SHA-512` for flexible security strength selection. * Channel Binding support: The library supports client mechanism negotiation with support of channel binding data provided externally. * Extensive Testing: The codebase is thoroughly tested to guarantee its functionality and correctness. * Minimal Dependencies: The library operates with a single dependency based on the implementation of the [SASLprep](https://github.com/ongres/stringprep) required by the RFC 5802. ## How to use the SCRAM Client API [![Maven Central](https://img.shields.io/badge/maven--central-scram_client-informational?style=for-the-badge&logo=apache-maven&logoColor=red)](https://maven-badges.herokuapp.com/maven-central/com.ongres.scram/scram-client) Javadoc: [![Javadocs](http://javadoc.io/badge/com.ongres.scram/scram-client.svg?label=scram-client)](http://javadoc.io/doc/com.ongres.scram/scram-client) ### Example of use ```java byte[] cbindData = ... // Get the channel binding data ScramClient scramClient = ScramClient.builder() .advertisedMechanisms(Arrays.asList("SCRAM-SHA-256", "SCRAM-SHA-256-PLUS")) .username("user") .password("pencil".toCharArray()) .channelBindingPolicy(ChannelBindingPolicy.ALLOW) // enforce a channel binding policy .channelBinding("tls-server-end-point", cbindData) // 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. // FE-> Send the client-first-message ("p=...,,n=...,r=...") ClientFirstMessage clientFirstMsg = scramClient.clientFirstMessage(); ... // <-BE Receive the server-first-message ServerFirstMessage serverFirstMsg = scramClient.serverFirstMessage("r=...,s=...,i=..."); ... // FE-> Send the client-final-message ("c=...,r=...,p=...") ClientFinalMessage clientFinalMsg = scramClient.clientFinalMessage(); ... // <-BE Receive the server-final-message, throw an ScramException on error or invalid signature ServerFinalMessage serverFinalMsg = scramClient.serverFinalMessage("v=..."); ``` ## Contributing We welcome contributions to this project! Feel free to submit pull requests that improve the codebase, add features, or fix bugs. Please make sure your contributions adhere to coding style guidelines and include thorough testing. Make sure to compile with `./mvnw verify -Pchecks` before submitting a PR. By making a contribution to this project, you certify that you adhere to requirements of the [DCO](https://developercertificate.org/) by signing-off your commits (`git commit -s`).: ongres-scram-f6bd6e2/SECURITY.md000066400000000000000000000017201521031247500164030ustar00rootroot00000000000000# Security Policy ## Supported Versions The following table outlines which versions of `scram` are actively supported with security updates. Please upgrade to a supported release to ensure you receive patches for any security issues. | Version | Supported | Java support | | ------- | ------------------ | ------------ | | 3.x | :white_check_mark: | Java 8+ | | < 3.0 | :x: | Java 7+ | ## Reporting a Vulnerability If you believe you have found a security vulnerability, please report it to us privately through GitHub’s security advisory system: [Report a vulnerability](../../security/advisories/new) We will investigate promptly and work with you to fix the issue. --- ## Security Best Practices for Users - Always use the latest supported version of `scram`. - Monitor [GitHub Releases](https://github.com/ongres/scram/releases) for security patches. - Consider subscribing to repository notifications for updates. ongres-scram-f6bd6e2/checks/000077500000000000000000000000001521031247500160525ustar00rootroot00000000000000ongres-scram-f6bd6e2/checks/checkstyle-header.txt000066400000000000000000000001471521031247500222010ustar00rootroot00000000000000^\/\*$ ^ \* Copyright \(c\) 20[12]\d OnGres, Inc\.$ ^ \* SPDX-License-Identifier: BSD-2-Clause$ ^ \*\/$ongres-scram-f6bd6e2/checks/checkstyle-suppressions.xml000066400000000000000000000006121521031247500235040ustar00rootroot00000000000000 ongres-scram-f6bd6e2/checks/checkstyle.xml000066400000000000000000000370471521031247500207450ustar00rootroot00000000000000 ongres-scram-f6bd6e2/checks/forbiddenapis.txt000066400000000000000000000001521521031247500214220ustar00rootroot00000000000000 java.util.Arrays#equals(byte[],byte[]) @ Replace with java.security.MessageDigest#isEqual(byte[],byte[]) ongres-scram-f6bd6e2/checks/pmd-ruleset.xml000066400000000000000000000025341521031247500210410ustar00rootroot00000000000000 Custom Rules .*/target/.* .*/generated/.* ongres-scram-f6bd6e2/checks/spotbugs-exclude.xml000066400000000000000000000020701521031247500220700ustar00rootroot00000000000000 ongres-scram-f6bd6e2/coverage-report/000077500000000000000000000000001521031247500177165ustar00rootroot00000000000000ongres-scram-f6bd6e2/coverage-report/pom.xml000066400000000000000000000026321521031247500212360ustar00rootroot00000000000000 4.0.0 com.ongres.scram scram-parent 3.3 ../scram-parent/pom.xml coverage-report pom JaCoCo Coverage Report com.ongres.scram scram-common com.ongres.scram scram-client org.jacoco jacoco-maven-plugin META-INF/versions/** report-aggregate report-aggregate verify ongres-scram-f6bd6e2/mvnw000077500000000000000000000270161521031247500155350ustar00rootroot00000000000000#!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- # JAVA_HOME - location of a JDK home dir, required when download maven via java source # MVNW_REPOURL - repo url base for downloading maven distribution # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output # ---------------------------------------------------------------------------- set -euf [ "${MVNW_VERBOSE-}" != debug ] || set -x # OS specific support. native_path() { printf %s\\n "$1"; } case "$(uname)" in CYGWIN* | MINGW*) [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" native_path() { cygpath --path --windows "$1"; } ;; esac # set JAVACMD and JAVACCMD set_java_home() { # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched if [ -n "${JAVA_HOME-}" ]; then if [ -x "$JAVA_HOME/jre/sh/java" ]; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" JAVACCMD="$JAVA_HOME/jre/sh/javac" else JAVACMD="$JAVA_HOME/bin/java" JAVACCMD="$JAVA_HOME/bin/javac" if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 return 1 fi fi else JAVACMD="$( 'set' +e 'unset' -f command 2>/dev/null 'command' -v java )" || : JAVACCMD="$( 'set' +e 'unset' -f command 2>/dev/null 'command' -v javac )" || : if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 return 1 fi fi } # hash string like Java String::hashCode hash_string() { str="${1:-}" h=0 while [ -n "$str" ]; do char="${str%"${str#?}"}" h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) str="${str#?}" done printf %x\\n $h } verbose() { :; } [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } die() { printf %s\\n "$1" >&2 exit 1 } trim() { # MWRAPPER-139: # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. # Needed for removing poorly interpreted newline sequences when running in more # exotic environments such as mingw bash on Windows. printf "%s" "${1}" | tr -d '[:space:]' } scriptDir="$(dirname "$0")" scriptName="$(basename "$0")" # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties while IFS="=" read -r key value; do case "${key-}" in distributionUrl) distributionUrl=$(trim "${value-}") ;; distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; esac done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" case "${distributionUrl##*/}" in maven-mvnd-*bin.*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; :Linux*x86_64*) distributionPlatform=linux-amd64 ;; *) echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 distributionPlatform=linux-amd64 ;; esac distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" ;; maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; *) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; esac # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" distributionUrlName="${distributionUrl##*/}" distributionUrlNameMain="${distributionUrlName%.*}" distributionUrlNameMain="${distributionUrlNameMain%-bin}" MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" exec_maven() { unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" } if [ -d "$MAVEN_HOME" ]; then verbose "found existing MAVEN_HOME at $MAVEN_HOME" exec_maven "$@" fi case "${distributionUrl-}" in *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; esac # prepare tmp dir if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } trap clean HUP INT TERM EXIT else die "cannot create temp dir" fi mkdir -p -- "${MAVEN_HOME%/*}" # Download and Install Apache Maven verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." verbose "Downloading from: $distributionUrl" verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" # select .zip or .tar.gz if ! command -v unzip >/dev/null; then distributionUrl="${distributionUrl%.zip}.tar.gz" distributionUrlName="${distributionUrl##*/}" fi # verbose opt __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v # normalize http auth case "${MVNW_PASSWORD:+has-password}" in '') MVNW_USERNAME='' MVNW_PASSWORD='' ;; has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; esac if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then verbose "Found wget ... using wget" wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then verbose "Found curl ... using curl" curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" elif set_java_home; then verbose "Falling back to use Java to download" javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" cat >"$javaSource" <<-END public class Downloader extends java.net.Authenticator { protected java.net.PasswordAuthentication getPasswordAuthentication() { return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); } public static void main( String[] args ) throws Exception { setDefault( new Downloader() ); java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); } } END # For Cygwin/MinGW, switch paths to Windows format before running javac and java verbose " - Compiling Downloader.java ..." "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" verbose " - Running Downloader.java ..." "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" fi # If specified, validate the SHA-256 sum of the Maven distribution zip file if [ -n "${distributionSha256Sum-}" ]; then distributionSha256Result=false if [ "$MVN_CMD" = mvnd.sh ]; then echo "Checksum validation is not supported for maven-mvnd." >&2 echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 elif command -v sha256sum >/dev/null; then if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then distributionSha256Result=true fi elif command -v shasum >/dev/null; then if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then distributionSha256Result=true fi else echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 fi if [ $distributionSha256Result = false ]; then echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 exit 1 fi fi # unzip and move if command -v unzip >/dev/null; then unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" else tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi # Find the actual extracted directory name (handles snapshots where filename != directory name) actualDistributionDir="" # First try the expected directory name (for regular distributions) if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then actualDistributionDir="$distributionUrlNameMain" fi fi # If not found, search for any directory with the Maven executable (for snapshots) if [ -z "$actualDistributionDir" ]; then # enable globbing to iterate over items set +f for dir in "$TMP_DOWNLOAD_DIR"/*; do if [ -d "$dir" ]; then if [ -f "$dir/bin/$MVN_CMD" ]; then actualDistributionDir="$(basename "$dir")" break fi fi done set -f fi if [ -z "$actualDistributionDir" ]; then verbose "Contents of $TMP_DOWNLOAD_DIR:" verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" die "Could not find Maven distribution directory in extracted archive" fi verbose "Found extracted Maven distribution directory: $actualDistributionDir" printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" clean || : exec_maven "$@" ongres-scram-f6bd6e2/mvnw.cmd000066400000000000000000000201441521031247500162670ustar00rootroot00000000000000<# : batch portion @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @REM distributed with this work for additional information @REM regarding copyright ownership. The ASF licenses this file @REM to you under the Apache License, Version 2.0 (the @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM @REM http://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @REM KIND, either express or implied. See the License for the @REM specific language governing permissions and limitations @REM under the License. @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- @REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Optional ENV vars @REM MVNW_REPOURL - repo url base for downloading maven distribution @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output @REM ---------------------------------------------------------------------------- @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) @SET __MVNW_CMD__= @SET __MVNW_ERROR__= @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% @SET PSModulePath= @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) ) @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% @SET __MVNW_PSMODULEP_SAVE= @SET __MVNW_ARG0_NAME__= @SET MVNW_USERNAME= @SET MVNW_PASSWORD= @IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) @echo Cannot start maven from wrapper >&2 && exit /b 1 @GOTO :EOF : end batch / begin powershell #> $ErrorActionPreference = "Stop" if ($env:MVNW_VERBOSE -eq "true") { $VerbosePreference = "Continue" } # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl if (!$distributionUrl) { Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" } switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { "maven-mvnd-*" { $USE_MVND = $true $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" $MVN_CMD = "mvnd.cmd" break } default { $USE_MVND = $false $MVN_CMD = $script -replace '^mvnw','mvn' break } } # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ if ($env:MVNW_REPOURL) { $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" } $distributionUrlName = $distributionUrl -replace '^.*/','' $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' $MAVEN_M2_PATH = "$HOME/.m2" if ($env:MAVEN_USER_HOME) { $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" } if (-not (Test-Path -Path $MAVEN_M2_PATH)) { New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null } $MAVEN_WRAPPER_DISTS = $null if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" } else { $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" } $MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" $MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" exit $? } if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" } # prepare tmp dir $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null trap { if ($TMP_DOWNLOAD_DIR.Exists) { try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } } } New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null # Download and Install Apache Maven Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." Write-Verbose "Downloading from: $distributionUrl" Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" $webclient = New-Object System.Net.WebClient if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) } [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null # If specified, validate the SHA-256 sum of the Maven distribution zip file $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum if ($distributionSha256Sum) { if ($USE_MVND) { Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." } Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." } } # unzip and move Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null # Find the actual extracted directory name (handles snapshots where filename != directory name) $actualDistributionDir = "" # First try the expected directory name (for regular distributions) $expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" $expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { $actualDistributionDir = $distributionUrlNameMain } # If not found, search for any directory with the Maven executable (for snapshots) if (!$actualDistributionDir) { Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { $testPath = Join-Path $_.FullName "bin/$MVN_CMD" if (Test-Path -Path $testPath -PathType Leaf) { $actualDistributionDir = $_.Name } } } if (!$actualDistributionDir) { Write-Error "Could not find Maven distribution directory in extracted archive" } Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null try { Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null } catch { if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { Write-Error "fail to move MAVEN_HOME" } } finally { try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } } Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" ongres-scram-f6bd6e2/pom.xml000066400000000000000000000035301521031247500161300ustar00rootroot00000000000000 4.0.0 com.ongres.scram scram-parent 3.3 scram-parent/pom.xml scram-aggregator pom SCRAM - Aggregator Java Implementation of the Salted Challenge Response Authentication Mechanism (SCRAM) https://github.com/ongres/scram 2017 OnGres, Inc https://www.ongres.com BSD 2-Clause "Simplified" License https://spdx.org/licenses/BSD-2-Clause repo com.ongres.aht Álvaro Hernández Tortosa aht@ongres.com com.ongres.matteom Matteo Melli matteom@ongres.com com.ongres.jorsol Jorge Solórzano jorsol@ongres.com scram-parent scram-common scram-client coverage coverage-report ongres-scram-f6bd6e2/scram-client/000077500000000000000000000000001521031247500171735ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-client/pom.xml000066400000000000000000000026161521031247500205150ustar00rootroot00000000000000 4.0.0 com.ongres.scram scram-parent 3.3 ../scram-parent/pom.xml scram-client SCRAM - Client com.ongres.scram scram-common coverage org.jacoco jacoco-maven-plugin run-its org.apache.maven.plugins maven-failsafe-plugin org.apache.maven.plugins maven-invoker-plugin ongres-scram-f6bd6e2/scram-client/src/000077500000000000000000000000001521031247500177625ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-client/src/it/000077500000000000000000000000001521031247500203765ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-client/src/it/jpms-scram-client/000077500000000000000000000000001521031247500237265ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-client/src/it/jpms-scram-client/invoker.properties000066400000000000000000000001371521031247500275220ustar00rootroot00000000000000# build project if JRE version is 11 or higher invoker.java.version = 11+ invoker.goals = test ongres-scram-f6bd6e2/scram-client/src/it/jpms-scram-client/pom.xml000066400000000000000000000030621521031247500252440ustar00rootroot00000000000000 4.0.0 com.ongres.scram.it jpms-scram-client JPMS Scram Client 3.3 UTF-8 ${java.specification.version} ${java.specification.version} ${java.specification.version} com.ongres.scram scram-client ${project.version} org.junit.jupiter junit-jupiter 6.1.0 test maven-compiler-plugin 3.15.0 maven-jar-plugin 3.5.0 maven-surefire-plugin 3.5.6 ongres-scram-f6bd6e2/scram-client/src/it/jpms-scram-client/src/000077500000000000000000000000001521031247500245155ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-client/src/it/jpms-scram-client/src/test/000077500000000000000000000000001521031247500254745ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-client/src/it/jpms-scram-client/src/test/java/000077500000000000000000000000001521031247500264155ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-client/src/it/jpms-scram-client/src/test/java/module-info.java000066400000000000000000000004501521031247500314750ustar00rootroot00000000000000/* * Copyright (c) 2024 OnGres, Inc. * SPDX-License-Identifier: BSD-2-Clause */ open module test.scram { requires com.ongres.scram.client; requires transitive org.junit.jupiter.engine; requires transitive org.junit.jupiter.api; requires transitive org.junit.jupiter.params; }ongres-scram-f6bd6e2/scram-client/src/it/jpms-scram-client/src/test/java/test/000077500000000000000000000000001521031247500273745ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-client/src/it/jpms-scram-client/src/test/java/test/scram/000077500000000000000000000000001521031247500305015ustar00rootroot00000000000000ScramClientTest.java000066400000000000000000000031671521031247500343400ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-client/src/it/jpms-scram-client/src/test/java/test/scram/* * Copyright (c) 2024 OnGres, Inc. * SPDX-License-Identifier: BSD-2-Clause */ package test.scram; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import java.util.Arrays; import com.ongres.scram.client.ScramClient; import com.ongres.scram.common.ScramFunctions; import org.junit.jupiter.api.Test; class ScramClientTest { @Test void accessPublic() { assertEquals("com.ongres.scram.client", ScramClient.class.getModule().getName()); assertEquals("com.ongres.scram.common", ScramFunctions.class.getModule().getName()); } @Test void testBuildClient() { ScramClient scramSession = ScramClient.builder() .advertisedMechanisms(Arrays.asList("SCRAM-SHA-256", "SCRAM-SHA-256-PLUS")) .username("user") .password("pencil".toCharArray()) .nonceSupplier(() -> "rOprNGfwEbeRWgbNEkqO") .build(); assertEquals("SCRAM-SHA-256", scramSession.getScramMechanism().getName()); assertEquals("n,,n=user,r=rOprNGfwEbeRWgbNEkqO", scramSession.clientFirstMessage().toString()); assertDoesNotThrow( () -> scramSession.serverFirstMessage( "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0," + "s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096")); assertEquals( "c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0," + "p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=", scramSession.clientFinalMessage().toString()); assertDoesNotThrow( () -> scramSession.serverFinalMessage("v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=")); } } ongres-scram-f6bd6e2/scram-client/src/it/settings.xml000066400000000000000000000015761521031247500227710ustar00rootroot00000000000000 it-repo true local.central @localRepositoryUrl@ true true local.central @localRepositoryUrl@ true true ongres-scram-f6bd6e2/scram-client/src/main/000077500000000000000000000000001521031247500207065ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-client/src/main/java/000077500000000000000000000000001521031247500216275ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-client/src/main/java/com/000077500000000000000000000000001521031247500224055ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-client/src/main/java/com/ongres/000077500000000000000000000000001521031247500237025ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-client/src/main/java/com/ongres/scram/000077500000000000000000000000001521031247500250075ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-client/src/main/java/com/ongres/scram/client/000077500000000000000000000000001521031247500262655ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-client/src/main/java/com/ongres/scram/client/ChannelBindingException.java000066400000000000000000000016271521031247500336600ustar00rootroot00000000000000/* * Copyright (c) 2026 OnGres, Inc. * SPDX-License-Identifier: BSD-2-Clause */ package com.ongres.scram.client; /** * Signals that an error occurred during the negotiation, validation, or * enforcement of SASL SCRAM Channel Binding. * *

This exception typically indicates a mismatch between the client's configured * {@code ChannelBindingPolicy} and the server's advertised mechanisms, or a failure * in processing cryptographic channel binding data (e.g., TLS server endpoint data). * * @since 3.3 */ public class ChannelBindingException extends MechanismNegotiationException { private static final long serialVersionUID = 1L; /** * Constructs a new {@code ChannelBindingException} with the specified detail message. * * @param detail the detail message explaining the cause of the negotiation failure. */ public ChannelBindingException(String detail) { super(detail); } } ongres-scram-f6bd6e2/scram-client/src/main/java/com/ongres/scram/client/ChannelBindingPolicy.java000066400000000000000000000062171521031247500331610ustar00rootroot00000000000000/* * Copyright (c) 2026 OnGres, Inc. * SPDX-License-Identifier: BSD-2-Clause */ package com.ongres.scram.client; /** * Defines the client-side security policies for negotiating channel binding during a SCRAM * authentication execution. * *

Channel binding protects the authentication flow against active Machine-in-the-Middle (MITM) * and session hijacking attacks by cryptographically binding the outer security layer (such as TLS) * to the inner SASL/SCRAM authentication layer. * *

This policy dictates how the {@link ScramClient} evaluates available channel binding data * against the server's advertised mechanisms (e.g., {@code SCRAM-SHA-256} vs {@code SCRAM-SHA-256-PLUS}) * during the mechanism negotiation phase, ultimately determining the runtime wire-format * {@code gs2-cbind-flag} ('n', 'y', or 'p'). * * @see RFC 5802 Section 6: Channels and Channel Binding * * @since 3.3 */ public enum ChannelBindingPolicy { /** * Disables channel binding entirely. * *

The client will strictly use standard, non-channel-bound SCRAM mechanisms (e.g., * {@code SCRAM-SHA-256}), even if valid channel binding data or server-supported * {@code -PLUS} mechanisms are available. * *

This policy forces the internal protocol engine to output the {@code 'n'} * (Client does not support channel binding) GS2 flag. Use this policy primarily * for debugging, unencrypted connections, or working around proxy servers or middleboxes * that strip channel data. */ DISABLE, /** * Permits channel binding if supported by the server, but allows a secure fallback if it is not. * *

This is the default recommended behavior for general-purpose applications: *

    *
  • If the server advertises a {@code -PLUS} mechanism and channel binding data is provided, * the negotiation upgrades to channel binding and uses the {@code 'p'} GS2 flag.
  • *
  • If the server lacks {@code -PLUS} support, the client gracefully downgrades to standard * SCRAM but transmits the {@code 'y'} GS2 flag. This explicitly declares to the server * that the client possesses channel binding capabilities, allowing the server to catch * and terminate malicious downgrade attacks mid-flight.
  • *
*/ ALLOW, /** * Enforces strict, non-negotiable channel binding validation. * *

The authentication execution will fail immediately during the client initialization * phase (throwing an {@link ChannelBindingException}) if any of the following boundaries * are violated: *

    *
  • The server does not explicitly advertise a channel-bound {@code -PLUS} mechanism.
  • *
  • The client was built without required channel binding type or data (e.g., missing * the binding token).
  • *
* *

This policy forces the use of the {@code 'p'} GS2 flag and is intended for high-security * environments where channel verification is a mandatory operational requirement (e.g., * applications requiring strict compliance matching database configurations like PostgreSQL's * {@code channel_binding=require}). */ REQUIRE } ongres-scram-f6bd6e2/scram-client/src/main/java/com/ongres/scram/client/ClientFinalProcessor.java000066400000000000000000000122141521031247500332200ustar00rootroot00000000000000/* * Copyright (c) 2017 OnGres, Inc. * SPDX-License-Identifier: BSD-2-Clause */ package com.ongres.scram.client; import static com.ongres.scram.common.util.Preconditions.checkNotEmpty; import static com.ongres.scram.common.util.Preconditions.checkNotNull; import com.ongres.scram.common.ClientFinalMessage; import com.ongres.scram.common.ClientFirstMessage; import com.ongres.scram.common.ScramFunctions; import com.ongres.scram.common.ScramMechanism; import com.ongres.scram.common.ServerFinalMessage; import com.ongres.scram.common.ServerFirstMessage; import com.ongres.scram.common.StringPreparation; import com.ongres.scram.common.exception.ScramInvalidServerSignatureException; import com.ongres.scram.common.exception.ScramParseException; import com.ongres.scram.common.exception.ScramServerErrorException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * Processor that allows to generate the client-final-message, as well as process the * server-final-message and verify server's signature. Generate the processor by calling either * {@code ServerFirstProcessor#clientFinalProcessor(char[])} or * {@code ServerFirstProcessor#clientFinalProcessor(byte[], byte[])}. */ final class ClientFinalProcessor { private final byte[] clientKey; private final byte[] storedKey; private final byte[] serverKey; private final ScramMechanism scramMechanism; private final ClientFirstMessage clientFirstMessage; private final ServerFirstMessage serverFirstMessage; private String authMessage; private ClientFinalProcessor(ScramMechanism scramMechanism, byte[] clientKey, byte[] storedKey, byte[] serverKey, ClientFirstMessage clientFirstMessage, ServerFirstMessage serverFirstMessage) { this.scramMechanism = scramMechanism; this.clientKey = checkNotNull(clientKey, "clientKey"); this.storedKey = checkNotNull(storedKey, "storedKey"); this.serverKey = checkNotNull(serverKey, "serverKey"); this.clientFirstMessage = clientFirstMessage; this.serverFirstMessage = serverFirstMessage; } ClientFinalProcessor(ScramMechanism scramMechanism, byte[] clientKey, byte[] serverKey, ClientFirstMessage clientFirstMessage, ServerFirstMessage serverFirstMessage) { this(scramMechanism, clientKey, ScramFunctions.storedKey(scramMechanism, clientKey), serverKey, clientFirstMessage, serverFirstMessage); } ClientFinalProcessor(ScramMechanism scramMechanism, byte[] saltedPassword, ClientFirstMessage clientFirstMessage, ServerFirstMessage serverFirstMessage) { this( scramMechanism, ScramFunctions.clientKey(scramMechanism, saltedPassword), ScramFunctions.serverKey(scramMechanism, saltedPassword), clientFirstMessage, serverFirstMessage); } ClientFinalProcessor(ScramMechanism scramMechanism, StringPreparation stringPreparation, char[] password, byte[] salt, ClientFirstMessage clientFirstMessage, ServerFirstMessage serverFirstMessage) { this(scramMechanism, ScramFunctions.saltedPassword(scramMechanism, stringPreparation, password, salt, serverFirstMessage.getIterationCount()), clientFirstMessage, serverFirstMessage); } private void generateAndCacheAuthMessage(byte[] cbindData) { if (null == this.authMessage) { this.authMessage = ScramFunctions.authMessage(clientFirstMessage, serverFirstMessage, cbindData); } } /** * Generates the SCRAM representation of the client-final-message, including the given * channel-binding data. * * @param cbindData The bytes of the channel-binding data * @return The message */ @NotNull ClientFinalMessage clientFinalMessage(byte @Nullable [] cbindData) { generateAndCacheAuthMessage(cbindData); return new ClientFinalMessage( clientFirstMessage.getGs2Header(), cbindData, serverFirstMessage.getNonce(), ScramFunctions.clientProof( clientKey, ScramFunctions.clientSignature(scramMechanism, storedKey, authMessage))); } /** * Receive and process the server-final-message. Server SCRAM signatures is verified. * * @param serverFinalMessage The received server-final-message * @throws ScramParseException If the message is not a valid server-final-message * @throws ScramServerErrorException If the server-final-message contained an error * @throws ScramInvalidServerSignatureException If the server signature is invalid * @throws IllegalArgumentException If the message is null or empty */ @NotNull ServerFinalMessage receiveServerFinalMessage(@NotNull String serverFinalMessage) throws ScramParseException, ScramServerErrorException, ScramInvalidServerSignatureException { checkNotEmpty(serverFinalMessage, "serverFinalMessage"); ServerFinalMessage message = ServerFinalMessage.parseFrom(serverFinalMessage); if (message.isError()) { throw new ScramServerErrorException(message.getServerError()); } if (!ScramFunctions.verifyServerSignature( scramMechanism, serverKey, authMessage, message.getVerifier())) { throw new ScramInvalidServerSignatureException("Invalid SCRAM server signature"); } return message; } } MechanismNegotiationException.java000066400000000000000000000020161521031247500350340ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-client/src/main/java/com/ongres/scram/client/* * Copyright (c) 2026 OnGres, Inc. * SPDX-License-Identifier: BSD-2-Clause */ package com.ongres.scram.client; import com.ongres.scram.common.exception.ScramRuntimeException; /** * Signals that an error occurred during the SASL SCRAM mechanism negotiation, * capability handshake, or policy enforcement phase. * *

This exception typically indicates a structural mismatch between the mechanisms * advertised by the server and those supported or allowed by the client, such as * a failure to find a mutually acceptable authentication variant or a breakdown in * channel binding requirements. * * @since 3.3 */ public class MechanismNegotiationException extends ScramRuntimeException { private static final long serialVersionUID = 1L; /** * Constructs a new {@code MechanismNegotiationException} with the specified detail message. * * @param detail the detail message explaining the cause of the negotiation failure. */ public MechanismNegotiationException(String detail) { super(detail); } } ongres-scram-f6bd6e2/scram-client/src/main/java/com/ongres/scram/client/MessageFlow.java000066400000000000000000000015611521031247500313470ustar00rootroot00000000000000/* * Copyright (c) 2017 OnGres, Inc. * SPDX-License-Identifier: BSD-2-Clause */ package com.ongres.scram.client; import com.ongres.scram.common.ClientFinalMessage; import com.ongres.scram.common.ClientFirstMessage; import com.ongres.scram.common.ServerFinalMessage; import com.ongres.scram.common.ServerFirstMessage; import com.ongres.scram.common.exception.ScramException; import org.jetbrains.annotations.NotNull; interface MessageFlow { @NotNull ClientFirstMessage clientFirstMessage(); @NotNull ServerFirstMessage serverFirstMessage(@NotNull String serverFirstMessage) throws ScramException; @NotNull ClientFinalMessage clientFinalMessage(); @NotNull ServerFinalMessage serverFinalMessage(@NotNull String serverFinalMessage) throws ScramException; enum Stage { NONE, CLIENT_FIRST, SERVER_FIRST, CLIENT_FINAL, SERVER_FINAL; } } ongres-scram-f6bd6e2/scram-client/src/main/java/com/ongres/scram/client/ScramClient.java000066400000000000000000000572161521031247500313470ustar00rootroot00000000000000/* * Copyright (c) 2017 OnGres, Inc. * SPDX-License-Identifier: BSD-2-Clause */ package com.ongres.scram.client; import static com.ongres.scram.common.util.Preconditions.checkArgument; 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 java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.SecureRandom; import java.util.Arrays; import java.util.Collection; import java.util.function.Supplier; import com.ongres.scram.common.ClientFinalMessage; import com.ongres.scram.common.ClientFirstMessage; import com.ongres.scram.common.Gs2CbindFlag; import com.ongres.scram.common.ScramFunctions; import com.ongres.scram.common.ScramMechanism; import com.ongres.scram.common.ServerFinalMessage; import com.ongres.scram.common.ServerFirstMessage; import com.ongres.scram.common.StringPreparation; import com.ongres.scram.common.exception.ScramInvalidServerSignatureException; import com.ongres.scram.common.exception.ScramParseException; import com.ongres.scram.common.exception.ScramServerErrorException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * A class that represents a SCRAM client. Use this class to perform a SCRAM negotiation with a * SCRAM server. This class performs an authentication execution for a given user, and has state * related to it. Thus, it cannot be shared across users or authentication executions. * *

Example of usage: * *

{@code
 * ScramClient scramClient = ScramClient.builder()
 *     .advertisedMechanisms(Arrays.asList("SCRAM-SHA-256", "SCRAM-SHA-256-PLUS"))
 *     .username("user")
 *     .password("pencil".toCharArray())
 *     .channelBindingPolicy(ChannelBindingPolicy.REQUIRE) // client requires channel binding
 *     .channelBinding("tls-server-end-point", channelBindingData)
 *     .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 != null ? builder.saltedPassword.clone() : null; this.clientKey = builder.clientKey != null ? builder.clientKey.clone() : null; this.serverKey = builder.serverKey != null ? builder.serverKey.clone() : null; 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"); } try { if (password != null) { this.clientFinalProcessor = serverFirstProcessor.clientFinalProcessor(password); } else if (saltedPassword != null) { this.clientFinalProcessor = serverFirstProcessor.clientFinalProcessor(saltedPassword); } else if (clientKey != null && serverKey != null) { this.clientFinalProcessor = serverFirstProcessor.clientFinalProcessor(clientKey, serverKey); } } finally { // Wipe the sensitive data, even if an exception was thrown above if (password != null) { Arrays.fill(password, (char) 0); } if (saltedPassword != null) { Arrays.fill(saltedPassword, (byte) 0); } if (clientKey != null) { Arrays.fill(clientKey, (byte) 0); } if (serverKey != null) { Arrays.fill(serverKey, (byte) 0); } } 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 { /** * Sets the channel binding policy to determine how the client negotiates * security layers with the server. By default is ALLOW. * * @param policy the channel binding policy (DISABLE, ALLOW, REQUIRE) * @return {@code this} builder for use in a chained invocation */ FinalBuildStage channelBindingPolicy(@NotNull ChannelBindingPolicy policy); /** * 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 constructed {@link ScramClient} ready to start the message flow * with the server. * * @return a ScramClient instance configured with the specified parameters * @throws IllegalArgumentException if a parameter is null or empty * @throws MechanismNegotiationException if the local mechanism configuration is incompatible * with the client state engine or missing core fallback options * @throws ChannelBindingException if a channel binding policy mismatch or cryptographic * negotiation failure occurs */ ScramClient build(); } /** * Builds instances of type {@link 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 scramMechanisms; Gs2CbindFlag channelBinding = Gs2CbindFlag.CLIENT_NOT; ChannelBindingPolicy bindingPolicy = ChannelBindingPolicy.ALLOW; StringPreparation stringPreparation = StringPreparation.SASL_PREPARATION; int nonceLength = 24; String nonce; SecureRandom secureRandom; String username; char[] password; byte[] saltedPassword; byte[] clientKey; byte[] serverKey; String cbindType; byte[] cbindData; String authzid; Supplier nonceSupplier; private Builder() { // called from ScramClient.builder() } @Override public FinalBuildStage stringPreparation(@NotNull StringPreparation stringPreparation) { this.stringPreparation = checkNotNull(stringPreparation, "stringPreparation"); return this; } @Override public FinalBuildStage channelBindingPolicy(@NotNull ChannelBindingPolicy policy) { this.bindingPolicy = checkNotNull(policy, "policy"); return this; } @Override public FinalBuildStage channelBinding(@Nullable String cbindType, byte @Nullable [] cbindData) { this.cbindType = cbindType; this.cbindData = cbindData != null ? cbindData.clone() : null; return this; } @Override public FinalBuildStage authzid(@NotNull String authzid) { this.authzid = checkNotEmpty(authzid, "authzid"); return this; } @Override public PasswordBuildStage username(@NotNull String username) { this.username = checkNotEmpty(username, "username"); return this; } @Override public FinalBuildStage password(char @NotNull [] password) { this.password = checkNotEmpty(password, "password"); return this; } @Override public FinalBuildStage saltedPassword(byte @NotNull [] saltedPassword) { this.saltedPassword = checkNotNull(saltedPassword, "saltedPassword"); return this; } @Override public FinalBuildStage clientAndServerKey(byte @NotNull [] clientKey, byte @NotNull [] serverKey) { this.clientKey = checkNotNull(clientKey, "clientKey"); this.serverKey = checkNotNull(serverKey, "serverKey"); return this; } @Override public UsernameBuildStage advertisedMechanisms( @NotNull Collection<@NotNull String> scramMechanisms) { checkNotNull(scramMechanisms, "scramMechanisms"); checkArgument(!scramMechanisms.isEmpty(), "scramMechanisms"); this.scramMechanisms = scramMechanisms; return this; } @Override public FinalBuildStage nonceLength(int length) { this.nonceLength = gt0(length, "length"); return this; } @Override public FinalBuildStage nonceSupplier(@NotNull Supplier<@NotNull String> nonceSupplier) { this.nonceSupplier = checkNotNull(nonceSupplier, "nonceSupplier"); return this; } @Override public FinalBuildStage secureRandomAlgorithmProvider(@NotNull String algorithm, @Nullable String provider) { try { this.secureRandom = null == provider ? SecureRandom.getInstance(algorithm) : SecureRandom.getInstance(algorithm, provider); } catch (NoSuchAlgorithmException | NoSuchProviderException ex) { throw new IllegalArgumentException("Invalid algorithm or provider", ex); } return this; } @Override public ScramClient build() { final SecureRandom random = secureRandom != null ? secureRandom : new SecureRandom(); this.nonce = nonceSupplier != null ? nonceSupplier.get() : ScramFunctions.nonce(nonceLength, random); this.selectedScramMechanism = mechanismNegotiation(); return new ScramClient(this); } private ScramMechanism mechanismNegotiation() { final ScramMechanism cbind = selectMechanism(scramMechanisms, true); final ScramMechanism noncbind = selectMechanism(scramMechanisms, false); ScramMechanism mechanismNegotiation = cbind != null ? cbind : noncbind; if (mechanismNegotiation == null) { throw new MechanismNegotiationException("Either a bare or -PLUS mechanism must be present"); } // If explicitly DISABLED, strip any passed data immediately to enforce standard SCRAM if (bindingPolicy == ChannelBindingPolicy.DISABLE) { this.cbindType = null; this.cbindData = null; } // Check client capability constraints boolean serverSupportsPlus = cbind != null; boolean clientHasData = cbindType != null && cbindData != null && !cbindType.isEmpty() && cbindData.length > 0; // Strict Enforcement Policy if (bindingPolicy == ChannelBindingPolicy.REQUIRE) { if (!serverSupportsPlus) { throw new ChannelBindingException( "Channel binding is required, but the server does not support -PLUS mechanisms"); } if (!clientHasData) { throw new ChannelBindingException( "Channel binding is required, but no channel binding data or type was provided"); } this.channelBinding = Gs2CbindFlag.CHANNEL_BINDING_REQUIRED; mechanismNegotiation = cbind; } else if (bindingPolicy == ChannelBindingPolicy.ALLOW && serverSupportsPlus && clientHasData) { // Flexible Upgrade Policy this.channelBinding = Gs2CbindFlag.CHANNEL_BINDING_REQUIRED; mechanismNegotiation = cbind; } else { // Safe Downgrade if (noncbind == null) { throw new MechanismNegotiationException("A non-PLUS mechanism was not advertised by the server"); } // RFC 5802 Protection: If the client possesses data but is forced to fallback // because the server lacks -PLUS, it MUST emit 'y' to intercept mid-flight downgrade attacks. this.channelBinding = clientHasData ? Gs2CbindFlag.CLIENT_YES_SERVER_NOT : Gs2CbindFlag.CLIENT_NOT; this.cbindType = null; this.cbindData = null; mechanismNegotiation = noncbind; } return mechanismNegotiation; } /** * This method classifies SCRAM mechanisms by two properties: whether they support channel * binding; and a priority, which is higher for safer algorithms (like SHA-256 vs SHA-1). * * @param channelBinding True to select {@code -PLUS} mechanisms. * @param scramMechanisms The mechanisms supported by the other peer * @return The selected mechanism, or null if no mechanism matched */ private static @Nullable ScramMechanism selectMechanism( @NotNull Collection<@NotNull String> scramMechanisms, boolean channelBinding) { ScramMechanism selectedMechanism = null; for (String mechanism : scramMechanisms) { ScramMechanism candidateMechanism = ScramMechanism.byName(mechanism); if (candidateMechanism != null && candidateMechanism.isPlus() == channelBinding && (selectedMechanism == null || candidateMechanism.ordinal() > selectedMechanism.ordinal())) { selectedMechanism = candidateMechanism; } } return selectedMechanism; } } } ongres-scram-f6bd6e2/scram-client/src/main/java/com/ongres/scram/client/ServerFirstProcessor.java000066400000000000000000000102201521031247500333010ustar00rootroot00000000000000/* * Copyright (c) 2017 OnGres, Inc. * SPDX-License-Identifier: BSD-2-Clause */ package com.ongres.scram.client; 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.scram.common.ClientFirstMessage; import com.ongres.scram.common.ScramFunctions; import com.ongres.scram.common.ScramMechanism; import com.ongres.scram.common.ServerFirstMessage; import com.ongres.scram.common.StringPreparation; import com.ongres.scram.common.exception.ScramParseException; import org.jetbrains.annotations.NotNull; /** * Process a received server-first-message. Generate by calling * {@link ScramClient#receiveServerFirstMessage(String)}. */ final class ServerFirstProcessor { private final ScramMechanism scramMechanism; private final StringPreparation stringPreparation; private final ClientFirstMessage clientFirstMessage; private final ServerFirstMessage serverFirstMessage; ServerFirstProcessor(ScramMechanism scramMechanism, StringPreparation stringPreparation, @NotNull String receivedServerFirstMessage, @NotNull String nonce, @NotNull ClientFirstMessage clientFirstMessage) throws ScramParseException { this.scramMechanism = scramMechanism; this.stringPreparation = stringPreparation; this.serverFirstMessage = ServerFirstMessage.parseFrom(receivedServerFirstMessage, nonce); this.clientFirstMessage = clientFirstMessage; } @NotNull ServerFirstMessage getServerFirstMessage() { return serverFirstMessage; } /** * Generates a {@code ClientFinalProcessor}, that allows to generate the client-final-message and * also receive and parse the server-first-message. It is based on the user's password. * * @param password The user's password * @return The handler * @throws IllegalArgumentException If the message is null or empty */ ClientFinalProcessor clientFinalProcessor(char[] password) { return new ClientFinalProcessor( scramMechanism, stringPreparation, checkNotEmpty(password, "password"), Base64.getDecoder().decode(serverFirstMessage.getSalt().getBytes(UTF_8)), clientFirstMessage, serverFirstMessage); } /** * Generates a {@code ClientFinalProcessor}, that allows to generate the client-final-message and * also receive and parse the server-first-message. It is based on the clientKey and serverKey, * which, if available, provide an optimized path versus providing the original user's password. * * @param clientKey The client key, as per the SCRAM algorithm. It can be generated with: * {@link ScramFunctions#clientKey(ScramMechanism, byte[])} * @param serverKey The server key, as per the SCRAM algorithm. It can be generated with: * {@link ScramFunctions#serverKey(ScramMechanism, byte[])} * @return The handler * @throws IllegalArgumentException If the clientKey/serverKey is null */ ClientFinalProcessor clientFinalProcessor(byte[] clientKey, byte[] serverKey) { return new ClientFinalProcessor( scramMechanism, checkNotNull(clientKey, "clientKey"), checkNotNull(serverKey, "serverKey"), clientFirstMessage, serverFirstMessage); } /** * Generates a {@code ClientFinalProcessor}, that allows to generate the client-final-message and * also receive and parse the server-first-message. It is based on the saltedPassword, * which, if available, provide an optimized path versus providing the original user's password. * * @param saltedPassword The salted password, as per the SCRAM algorithm. It can be generated * with: * {@link ScramFunctions#saltedPassword(ScramMechanism, StringPreparation, char[], byte[], int)} * @return The handler * @throws IllegalArgumentException If the saltedPassword is null */ ClientFinalProcessor clientFinalProcessor(byte[] saltedPassword) { return new ClientFinalProcessor( scramMechanism, checkNotNull(saltedPassword, "saltedPassword"), clientFirstMessage, serverFirstMessage); } } ongres-scram-f6bd6e2/scram-client/src/main/java/com/ongres/scram/client/package-info.java000066400000000000000000000005511521031247500314550ustar00rootroot00000000000000/* * Copyright (c) 2017 OnGres, Inc. * SPDX-License-Identifier: BSD-2-Clause */ /** * This module expose the client implementation of Salted Challenge Response * Authentication Mechanism (SCRAM). It provides a high level and easy to use API to negotiate the * mechanism and the message flow used for authentication. */ package com.ongres.scram.client; ongres-scram-f6bd6e2/scram-client/src/main/java9/000077500000000000000000000000001521031247500217205ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-client/src/main/java9/module-info.java000066400000000000000000000003101521031247500247730ustar00rootroot00000000000000/* * Copyright (c) 2024 OnGres, Inc. * SPDX-License-Identifier: BSD-2-Clause */ module com.ongres.scram.client { requires transitive com.ongres.scram.common; exports com.ongres.scram.client; }ongres-scram-f6bd6e2/scram-client/src/test/000077500000000000000000000000001521031247500207415ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-client/src/test/java/000077500000000000000000000000001521031247500216625ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-client/src/test/java/com/000077500000000000000000000000001521031247500224405ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-client/src/test/java/com/example/000077500000000000000000000000001521031247500240735ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-client/src/test/java/com/example/ChannelBindingNegotiationTest.java000066400000000000000000000223471521031247500326520ustar00rootroot00000000000000/* * Copyright (c) 2017 OnGres, Inc. * SPDX-License-Identifier: BSD-2-Clause */ package com.example; 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 java.util.List; import com.ongres.scram.client.ChannelBindingException; import com.ongres.scram.client.ChannelBindingPolicy; import com.ongres.scram.client.MechanismNegotiationException; import com.ongres.scram.client.ScramClient; import com.ongres.scram.common.ClientFirstMessage; import com.ongres.scram.common.Gs2CbindFlag; import com.ongres.scram.common.Gs2Header; import com.ongres.scram.common.exception.ScramRuntimeException; import com.ongres.scram.common.util.TlsServerEndpoint; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.NullAndEmptySource; /** * Validates mechanism negotiation rules and the calculation of GS2 headers based on the * client's {@link ChannelBindingPolicy} configuration and server capabilities. * * @see RFC 5802: Channels and Channel Binding * @see RFC 7677: SCRAM-SHA-256-PLUS Selection */ @DisplayName("SCRAM Channel Binding Negotiation Tests") class ChannelBindingNegotiationTest { private static final byte[] VALID_CBIND_DATA = Base64.getDecoder().decode( "Dv4abLuK1TiHcq3tJXrHODILGFQuC1M4kfP4w7dyRvjadaqGq8D/Po1XeJZpzUqal+mAKXNGytneo5KPOsJnYA=="); private static final String TEST_NONCE = "rOprNGfwEbeRWgbNEkqO"; private static final List BARE_AND_PLUS = List.of("SCRAM-SHA-256-PLUS", "SCRAM-SHA-256"); private static final List BARE_ONLY = List.of("SCRAM-SHA-256"); private static final List PLUS_ONLY = List.of("SCRAM-SHA-256-PLUS"); @Nested @DisplayName("Successful Negotiation Profiles") class SuccessfulPathNegotiations { @Test @DisplayName("ALLOW: Active Downgrade Protection ('y' flag) when server lacks -PLUS support") void policyAllow_ServerNoPlus_EmitsYFlag() { ScramClient client = createBaseBuilder(BARE_ONLY) .channelBindingPolicy(ChannelBindingPolicy.ALLOW) .channelBinding(TlsServerEndpoint.TLS_SERVER_END_POINT, VALID_CBIND_DATA) .build(); assertEquals("SCRAM-SHA-256", client.getScramMechanism().getName()); assertFalse(client.getScramMechanism().isPlus()); Gs2Header gs2Header = getGs2Header(client); assertEquals(Gs2CbindFlag.CLIENT_YES_SERVER_NOT, gs2Header.getChannelBindingFlag()); } @Test @DisplayName("ALLOW: Seamless upgrade to Channel Bound ('p' flag) when both peers support it") void policyAllow_ServerSupportsPlus_UpgradesToPFlag() { ScramClient client = createBaseBuilder(BARE_AND_PLUS) .channelBindingPolicy(ChannelBindingPolicy.ALLOW) .channelBinding(TlsServerEndpoint.TLS_SERVER_END_POINT, VALID_CBIND_DATA) .build(); assertEquals("SCRAM-SHA-256-PLUS", client.getScramMechanism().getName()); assertTrue(client.getScramMechanism().isPlus()); Gs2Header gs2Header = getGs2Header(client); assertEquals(Gs2CbindFlag.CHANNEL_BINDING_REQUIRED, gs2Header.getChannelBindingFlag()); assertEquals(TlsServerEndpoint.TLS_SERVER_END_POINT, gs2Header.getChannelBindingName()); } @Test @DisplayName("REQUIRE: Successfully establishes bound session when prerequisites match") void policyRequire_ValidEnvironment_EstablishesPFlag() { ScramClient client = createBaseBuilder(BARE_AND_PLUS) .channelBindingPolicy(ChannelBindingPolicy.REQUIRE) .channelBinding(TlsServerEndpoint.TLS_SERVER_END_POINT, VALID_CBIND_DATA) .build(); assertEquals("SCRAM-SHA-256-PLUS", client.getScramMechanism().getName()); assertTrue(client.getScramMechanism().isPlus()); Gs2Header gs2Header = getGs2Header(client); assertEquals(Gs2CbindFlag.CHANNEL_BINDING_REQUIRED, gs2Header.getChannelBindingFlag()); assertEquals(TlsServerEndpoint.TLS_SERVER_END_POINT, gs2Header.getChannelBindingName()); } @Test @DisplayName("DISABLE: Rejects channel data and forces standard bare mechanism ('n' flag)") void policyDisable_WithChannelData_ForcesNFlag() { ScramClient client = createBaseBuilder(BARE_AND_PLUS) .channelBindingPolicy(ChannelBindingPolicy.DISABLE) .channelBinding(TlsServerEndpoint.TLS_SERVER_END_POINT, VALID_CBIND_DATA) .build(); assertEquals("SCRAM-SHA-256", client.getScramMechanism().getName()); assertFalse(client.getScramMechanism().isPlus()); Gs2Header gs2Header = getGs2Header(client); assertEquals(Gs2CbindFlag.CLIENT_NOT, gs2Header.getChannelBindingFlag()); } @Test @DisplayName("ALLOW: Falls back to standard bare mechanism ('n' flag) if client lacks channel data") void policyAllow_MissingClientData_FallsBackToNFlag() { ScramClient client = createBaseBuilder(BARE_AND_PLUS) .channelBindingPolicy(ChannelBindingPolicy.ALLOW) // No channelBinding() configuration provided .build(); assertEquals("SCRAM-SHA-256", client.getScramMechanism().getName()); assertFalse(client.getScramMechanism().isPlus()); Gs2Header gs2Header = getGs2Header(client); assertEquals(Gs2CbindFlag.CLIENT_NOT, gs2Header.getChannelBindingFlag()); } } @Nested @DisplayName("Malformed Parameter and Structural Fallbacks") class DataFallbackNegotiations { @ParameterizedTest(name = "ALLOW fallback to 'n' flag when type is: [{0}]") @NullAndEmptySource void policyAllow_InvalidBindingType_FallsBackToNFlag(String invalidType) { ScramClient client = createBaseBuilder(BARE_AND_PLUS) .channelBindingPolicy(ChannelBindingPolicy.ALLOW) .channelBinding(invalidType, VALID_CBIND_DATA) .build(); assertEquals("SCRAM-SHA-256", client.getScramMechanism().getName()); assertFalse(client.getScramMechanism().isPlus()); Gs2Header gs2Header = getGs2Header(client); assertEquals(Gs2CbindFlag.CLIENT_NOT, gs2Header.getChannelBindingFlag()); } @ParameterizedTest(name = "ALLOW fallback to 'n' flag when raw bytes are: {0}") @NullAndEmptySource void policyAllow_InvalidBindingBytes_FallsBackToNFlag(byte[] invalidData) { ScramClient client = createBaseBuilder(BARE_AND_PLUS) .channelBindingPolicy(ChannelBindingPolicy.ALLOW) .channelBinding(TlsServerEndpoint.TLS_SERVER_END_POINT, invalidData) .build(); assertEquals("SCRAM-SHA-256", client.getScramMechanism().getName()); assertFalse(client.getScramMechanism().isPlus()); Gs2Header gs2Header = getGs2Header(client); assertEquals(Gs2CbindFlag.CLIENT_NOT, gs2Header.getChannelBindingFlag()); } } @Nested @DisplayName("Strict Enforcement and Error Paths") class EnforcementFailureNegotiations { @Test @DisplayName("REQUIRE: Aborts initialization if the server fails to advertise a -PLUS mechanism") void policyRequire_ServerLacksPlus_ThrowsException() { ChannelBindingException ex = assertThrows(ChannelBindingException.class, () -> createBaseBuilder(BARE_ONLY) .channelBindingPolicy(ChannelBindingPolicy.REQUIRE) .channelBinding(TlsServerEndpoint.TLS_SERVER_END_POINT, VALID_CBIND_DATA) .build()); assertEquals("Channel binding is required, but the server does not support -PLUS mechanisms", ex.getMessage()); } @Test @DisplayName("REQUIRE: Aborts initialization if binding tokens/data are absent") void policyRequire_ClientLacksData_ThrowsException() { ChannelBindingException ex = assertThrows(ChannelBindingException.class, () -> createBaseBuilder(BARE_AND_PLUS) .channelBindingPolicy(ChannelBindingPolicy.REQUIRE) // Missing channel binding parameters .build()); assertEquals("Channel binding is required, but no channel binding data or type was provided", ex.getMessage()); } @Test @DisplayName("ALLOW: Aborts if server demands -PLUS exclusively but client possesses no channel data") void policyAllow_ServerMandatesPlus_ClientHasNoData_ThrowsException() { MechanismNegotiationException ex = assertThrows(MechanismNegotiationException.class, () -> createBaseBuilder(PLUS_ONLY) .channelBindingPolicy(ChannelBindingPolicy.ALLOW) // Missing channel data .build()); assertEquals("A non-PLUS mechanism was not advertised by the server", ex.getMessage()); } } private static ScramClient.FinalBuildStage createBaseBuilder(List mechanisms) { return ScramClient.builder() .advertisedMechanisms(mechanisms) .username("user") .password("pencil".toCharArray()) .nonceSupplier(() -> TEST_NONCE); } private static @NotNull Gs2Header getGs2Header(ScramClient client) { ClientFirstMessage msg = client.clientFirstMessage(); return msg.getGs2Header(); } } ongres-scram-f6bd6e2/scram-client/src/test/java/com/example/ScramClientTest.java000066400000000000000000000104301521031247500300000ustar00rootroot00000000000000/* * Copyright (c) 2017 OnGres, Inc. * SPDX-License-Identifier: BSD-2-Clause */ package com.example; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.Arrays; import java.util.Base64; import com.ongres.scram.client.ScramClient; import com.ongres.scram.common.ClientFinalMessage; import com.ongres.scram.common.util.TlsServerEndpoint; import org.junit.jupiter.api.Test; class ScramClientTest { private static final byte[] CBIND_DATA = Base64.getDecoder().decode("Dv4abLuK1TiHcq3tJXrHODILGF" + "QuC1M4kfP4w7dyRvjadaqGq8D/Po1XeJZpzUqal+mAKXNGytneo5KPOsJnYA=="); @Test void completeTest() { ScramClient scramSession = ScramClient.builder() .advertisedMechanisms(Arrays.asList("SCRAM-SHA-256", "SCRAM-SHA-256-PLUS")) .username("user") .password("pencil".toCharArray()) .channelBinding(TlsServerEndpoint.TLS_SERVER_END_POINT, CBIND_DATA) .nonceSupplier(() -> "rOprNGfwEbeRWgbNEkqO") .build(); assertEquals("SCRAM-SHA-256-PLUS", scramSession.getScramMechanism().getName()); assertEquals("p=tls-server-end-point,,n=user,r=rOprNGfwEbeRWgbNEkqO", scramSession.clientFirstMessage().toString()); assertDoesNotThrow( () -> scramSession.serverFirstMessage( "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0," + "s=W22ZaJ0SNY7soEsUEjb6gQ==," + "i=4096")); ClientFinalMessage clientFinalMessage = scramSession.clientFinalMessage(); assertEquals( "c=cD10bHMtc2VydmVyLWVuZC1wb2ludCwsDv4abLuK1TiHcq3tJ" + "XrHODILGFQuC1M4kfP4w7dyRvjadaqGq8D/Po1XeJZpzUqal+mAKXNGytneo5KPOsJnYA==" + ",r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0" + ",p=WIBtRXGH4I4R2CU1/tHa2YREwrJjLFa3/pKJQH/0Ofo=", clientFinalMessage.toString()); assertDoesNotThrow( () -> scramSession.serverFinalMessage("v=9k31qsYXd74d6BnbFf9jE+r9un6a8ou85FYeNxDAdqc=")); } @Test void completeTestWithoutChannelBinding() { ScramClient scramSession = ScramClient.builder() .advertisedMechanisms(Arrays.asList("SCRAM-SHA-256", "SCRAM-SHA-256-PLUS")) .username("user") .password("pencil".toCharArray()) .nonceSupplier(() -> "rOprNGfwEbeRWgbNEkqO") .build(); assertEquals("SCRAM-SHA-256", scramSession.getScramMechanism().getName()); assertEquals("n,,n=user,r=rOprNGfwEbeRWgbNEkqO", scramSession.clientFirstMessage().toString()); assertDoesNotThrow( () -> scramSession.serverFirstMessage( "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0," + "s=W22ZaJ0SNY7soEsUEjb6gQ==," + "i=4096")); ClientFinalMessage clientFinalMessage = scramSession.clientFinalMessage(); assertEquals( "c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0" + ",p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=", clientFinalMessage.toString()); assertDoesNotThrow( () -> scramSession.serverFinalMessage("v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=")); } @Test void iterationTest() { ScramClient scramSession = ScramClient.builder() .advertisedMechanisms(Arrays.asList("SCRAM-SHA-256")) .username("postgres") .password("pencil".toCharArray()) .channelBinding(TlsServerEndpoint.TLS_SERVER_END_POINT, CBIND_DATA) .nonceSupplier(() -> "1q^MGrWUi{etW+H7(#k431kB") .build(); assertEquals("SCRAM-SHA-256", scramSession.getScramMechanism().getName()); assertEquals("y,,n=postgres,r=1q^MGrWUi{etW+H7(#k431kB", scramSession.clientFirstMessage().toString()); assertDoesNotThrow( () -> scramSession.serverFirstMessage( "r=1q^MGrWUi{etW+H7(#k431kBdAr3CWX7B6houDP4f7Z2XEpZ," + "s=Fgh8JU2AlRjBHUsIU/GgtQ==," + "i=1000000")); ClientFinalMessage clientFinalMessage = scramSession.clientFinalMessage(); assertEquals( "c=eSws," + "r=1q^MGrWUi{etW+H7(#k431kBdAr3CWX7B6houDP4f7Z2XEpZ," + "p=vQ3IyYl3LvjWOlK2c0IP5QAi6XB7Dm0Axo0V51DcHZA=", clientFinalMessage.toString()); assertDoesNotThrow( () -> scramSession.serverFinalMessage("v=sz/isCwVSUn/TBWeYABz6WaoZIcfsui9NPaJCoxxAjY=")); } } ongres-scram-f6bd6e2/scram-client/src/test/java/com/ongres/000077500000000000000000000000001521031247500237355ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-client/src/test/java/com/ongres/scram/000077500000000000000000000000001521031247500250425ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-client/src/test/java/com/ongres/scram/JarFileCheckIT.java000066400000000000000000000044751521031247500304260ustar00rootroot00000000000000/* * 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.client", moduleDescriptor.name()); } } ongres-scram-f6bd6e2/scram-client/src/test/java/com/ongres/scram/client/000077500000000000000000000000001521031247500263205ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-client/src/test/java/com/ongres/scram/client/RfcExampleSha1.java000066400000000000000000000031331521031247500317260ustar00rootroot00000000000000/* * Copyright (c) 2017 OnGres, Inc. * SPDX-License-Identifier: BSD-2-Clause */ package com.ongres.scram.client; /** * Constants for examples of the RFC for SHA-1 tests. */ 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="; } ongres-scram-f6bd6e2/scram-client/src/test/java/com/ongres/scram/client/ScramBuilderTest.java000066400000000000000000000132141521031247500324000ustar00rootroot00000000000000/* * Copyright (c) 2017 OnGres, Inc. * SPDX-License-Identifier: BSD-2-Clause */ package com.ongres.scram.client; 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.assertThrows; import java.util.Arrays; import java.util.Base64; import com.ongres.scram.common.ClientFinalMessage; import com.ongres.scram.common.ScramFunctions; import com.ongres.scram.common.ScramMechanism; import com.ongres.scram.common.StringPreparation; import com.ongres.scram.common.exception.ScramRuntimeException; import org.junit.jupiter.api.Test; class ScramBuilderTest { @Test void getValid() { ScramClient client1 = ScramClient.builder() .advertisedMechanisms(Arrays.asList("SCRAM-SHA-1")) .username("*") .password("*".toCharArray()) .stringPreparation(StringPreparation.POSTGRESQL_PREPARATION) .build(); assertNotNull(client1); assertNotNull(client1.clientFirstMessage()); ScramClient client2 = ScramClient.builder() .advertisedMechanisms(Arrays.asList("SCRAM-SHA-1", "SCRAM-SHA-256-PLUS")) .username("*") .password("*".toCharArray()) .stringPreparation(StringPreparation.SASL_PREPARATION) .nonceLength(12) .channelBinding("tls-server-end-point", new byte[0]) .build(); assertNotNull(client2); assertNotNull(client2.clientFirstMessage()); ScramClient client3 = ScramClient.builder() .advertisedMechanisms(Arrays.asList("SCRAM-SHA-1", "SCRAM-SHA-1-PLUS")) .username("*") .password("*".toCharArray()) .stringPreparation(StringPreparation.NO_PREPARATION) .nonceLength(36) .build(); assertNotNull(client3); assertNotNull(client3.clientFirstMessage()); ScramClient client4 = ScramClient.builder() .advertisedMechanisms(Arrays.asList("SCRAM-SHA-1", "SCRAM-SHA-256-PLUS")) .username("*") .password("*".toCharArray()) .stringPreparation(StringPreparation.NO_PREPARATION) .nonceLength(64) .build(); assertNotNull(client4); assertNotNull(client4.clientFirstMessage()); ScramClient client5 = ScramClient.builder() .advertisedMechanisms(Arrays.asList("SCRAM-SHA-1", "SCRAM-SHA-256-PLUS")) .username("*") .password("*".toCharArray()) .stringPreparation(StringPreparation.NO_PREPARATION) // .secureRandomAlgorithmProvider("PKCS11", null) .nonceLength(64) .build(); assertNotNull(client5); assertNotNull(client5.clientFirstMessage()); ScramClient client6 = ScramClient.builder() .advertisedMechanisms(Arrays.asList("SCRAM-SHA-1")) .username("*") .password("*".toCharArray()) .build(); assertNotNull(client6); assertNotNull(client6.clientFirstMessage()); ScramClient client7 = ScramClient.builder() .advertisedMechanisms(Arrays.asList("SCRAM-SHA-256-PLUS")) .username("*") .password("*".toCharArray()) .channelBinding("tls-server-end-point", new byte[10]) .stringPreparation(StringPreparation.NO_PREPARATION) .build(); assertNotNull(client7); assertNotNull(client7.clientFirstMessage()); byte[] saltedPassword = ScramFunctions.saltedPassword(ScramMechanism.SCRAM_SHA_1, StringPreparation.SASL_PREPARATION, RfcExampleSha1.PASSWORD.toCharArray(), Base64.getDecoder().decode(RfcExampleSha1.SERVER_SALT), 4096); ScramClient client8 = ScramClient.builder() .advertisedMechanisms(Arrays.asList("SCRAM-SHA-1", "SCRAM-SHA-1-PLUS")) .username(RfcExampleSha1.USER) .saltedPassword(saltedPassword) .nonceSupplier(() -> RfcExampleSha1.CLIENT_NONCE) .build(); assertNotNull(client8); assertEquals("SCRAM-SHA-1", client8.getScramMechanism().getName()); assertNotNull(client8.clientFirstMessage()); assertThrows(IllegalStateException.class, () -> client8.clientFinalMessage()); assertDoesNotThrow(() -> client8.serverFirstMessage(RfcExampleSha1.SERVER_FIRST_MESSAGE)); assertThrows(IllegalStateException.class, () -> client8.clientFirstMessage()); assertThrows(IllegalStateException.class, () -> client8.serverFinalMessage(RfcExampleSha1.SERVER_FINAL_MESSAGE)); ClientFinalMessage clientFinalMessage = client8.clientFinalMessage(); assertEquals(RfcExampleSha1.CLIENT_FINAL_MESSAGE, clientFinalMessage.toString()); assertThrows(IllegalStateException.class, () -> client8.serverFirstMessage(RfcExampleSha1.SERVER_FIRST_MESSAGE)); assertDoesNotThrow( () -> client8.serverFinalMessage(RfcExampleSha1.SERVER_FINAL_MESSAGE)); } @Test void getInvalid() { MechanismNegotiationException assertThrows = assertThrows(MechanismNegotiationException.class, () -> ScramClient.builder() .advertisedMechanisms(Arrays.asList("SCRAM-SHA-1-PLUS")) .username("*") .password("*".toCharArray()) .stringPreparation(StringPreparation.NO_PREPARATION) .build()); assertEquals("A non-PLUS mechanism was not advertised by the server", assertThrows.getMessage()); } @Test void invalidMechanismsTests() { MechanismNegotiationException assertThrows = assertThrows(MechanismNegotiationException.class, () -> ScramClient.builder() .advertisedMechanisms(Arrays.asList("SCRAM-MD5-128")) .username("postgres") .password("pencil".toCharArray()) .build()); assertEquals("Either a bare or -PLUS mechanism must be present", assertThrows.getMessage()); } } ongres-scram-f6bd6e2/scram-common/000077500000000000000000000000001521031247500172055ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-common/pom.xml000066400000000000000000000026171521031247500205300ustar00rootroot00000000000000 4.0.0 com.ongres.scram scram-parent 3.3 ../scram-parent/pom.xml scram-common SCRAM - Common com.ongres.stringprep saslprep coverage org.jacoco jacoco-maven-plugin run-its org.apache.maven.plugins maven-failsafe-plugin org.apache.maven.plugins maven-invoker-plugin ongres-scram-f6bd6e2/scram-common/src/000077500000000000000000000000001521031247500177745ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-common/src/main/000077500000000000000000000000001521031247500207205ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-common/src/main/java/000077500000000000000000000000001521031247500216415ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-common/src/main/java/com/000077500000000000000000000000001521031247500224175ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/000077500000000000000000000000001521031247500237145ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/000077500000000000000000000000001521031247500250215ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/000077500000000000000000000000001521031247500263115ustar00rootroot00000000000000AbstractCharAttributeValue.java000066400000000000000000000023221521031247500343160ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/* * 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; /** * Construct and write generic CharAttribute-Value pairs. * *

Concrete sub-classes should also provide a static parse(String) creation method. */ abstract class AbstractCharAttributeValue extends StringWritable { private final char charAttribute; private final @Nullable String value; protected AbstractCharAttributeValue(@NotNull T charAttribute, @Nullable String value) { if (null != value && value.isEmpty()) { throw new IllegalArgumentException("Value should be either null or non-empty"); } this.charAttribute = checkNotNull(charAttribute, "attribute").getChar(); this.value = value; } public final char getChar() { return charAttribute; } public @Nullable String getValue() { return value; } @Override StringBuilder writeTo(StringBuilder sb) { sb.append(charAttribute); if (null != value) { sb.append('=').append(value); } return sb; } } ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/AbstractScramMessage.java000066400000000000000000000007701521031247500332160ustar00rootroot00000000000000/* * Copyright (c) 2017 OnGres, Inc. * SPDX-License-Identifier: BSD-2-Clause */ package com.ongres.scram.common; import org.jetbrains.annotations.NotNull; /** * Basic implementation of the StringWritable interface, that overrides the toString() method. */ abstract class AbstractScramMessage extends StringWritable { /** * String representation of the SCRAM message. */ @Override public final @NotNull String toString() { return writeTo(new StringBuilder(48)).toString(); } } ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/CharSupplier.java000066400000000000000000000005571521031247500315640ustar00rootroot00000000000000/* * Copyright (c) 2017 OnGres, Inc. * SPDX-License-Identifier: BSD-2-Clause */ package com.ongres.scram.common; /** * Represents an attribute (a key name) that is represented by a single char. */ interface CharSupplier { /** * Return the char used to represent this attribute. * * @return The character of the attribute */ char getChar(); } ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/ClientFinalMessage.java000066400000000000000000000115731521031247500326600ustar00rootroot00000000000000/* * 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 java.nio.charset.StandardCharsets; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * Constructs and parses client-final-messages. * * * * * * * * * * * * * * * * * * * *
Formal Syntax:
cbind-inputgs2-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-proofchannel-binding "," nonce ["," extensions]
client-final-messageclient-final-message-without-proof "," proof
* * @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))); } } ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/ClientFirstMessage.java000066400000000000000000000153141521031247500327130ustar00rootroot00000000000000/* * 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. * * * * * * * * * * * *
Formal Syntax:
client-first-message-bare[reserved-mext ","] username "," nonce ["," extensions]
client-first-messagegs2-header client-first-message-bare
* * @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); } } ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/CryptoUtil.java000066400000000000000000000171231521031247500312760ustar00rootroot00000000000000/* * 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.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.SecureRandom; import java.util.Arrays; import java.util.Locale; import javax.crypto.Mac; import javax.crypto.ShortBufferException; import javax.crypto.spec.SecretKeySpec; import com.ongres.scram.common.exception.ScramInterruptedException; import com.ongres.scram.common.exception.ScramRuntimeException; import org.jetbrains.annotations.NotNull; /** * Utility static methods for cryptography related tasks. */ final class CryptoUtil { /** * The interval at which the PBKDF2 loop checks for thread interruption. * *

Checking the thread state via {@link Thread#isInterrupted()} on every * iteration requires a native JVM call, which significantly degrades hashing * throughput. This stride value batches iterations, allowing the loop to execute * rapidly while remaining responsive to shutdown signals. * *

Note: This value MUST be a power of two. This allows the compiler * to use a highly optimized bitwise AND operation {@code (i & (STRIDE - 1))} * rather than a slower modulo operator. */ private static final int INTERRUPT_CHECK_STRIDE = 1024; 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 mac The Mac instance to use * @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 key for Mac algorithm, or if * thread is interrupted */ static byte[] hi(Mac mac, char[] password, byte[] salt, int iterationCount) { checkNotNull(mac, "mac"); checkNotNull(password, "password"); checkNotNull(salt, "salt"); checkArgument(salt.length != 0, "salt"); gt0(iterationCount, "iterationCount"); try { byte[] pwBytes = passwordToUtf8Bytes(password); try { mac.init(new SecretKeySpec(pwBytes, mac.getAlgorithm())); } finally { Arrays.fill(pwBytes, (byte) 0); } } catch (InvalidKeyException ex) { throw new ScramRuntimeException( String.format(Locale.ROOT, "Platform error: unsupported key for %s algorithm", mac.getAlgorithm()), ex); } mac.update(salt); // The 4-octet encoding of the integer 1 INT(1). mac.update((byte) 0); mac.update((byte) 0); mac.update((byte) 0); mac.update((byte) 1); byte[] ui = mac.doFinal(); byte[] result = Arrays.copyOf(ui, ui.length); boolean success = false; try { for (int i = 2; i <= iterationCount; i++) { if ((i & (INTERRUPT_CHECK_STRIDE - 1)) == 0 && Thread.currentThread().isInterrupted()) { throw new ScramInterruptedException("PBKDF2 computation interrupted at iteration " + i); } mac.update(ui); mac.doFinal(ui, 0); for (int j = 0; j < result.length; j++) { result[j] ^= ui[j]; } } success = true; return result; } catch (ShortBufferException e) { throw new AssertionError("Buffer sized by Mac.doFinal() is suddenly too short", e); } finally { Arrays.fill(ui, (byte) 0); if (!success) { Arrays.fill(result, (byte) 0); } } } /** * Convert password to UTF-8 bytes and secure clear the backing array. * * @param password The password to convert * @return The UTF-8 bytes of the password */ private static byte[] passwordToUtf8Bytes(char[] password) { ByteBuffer bb = StandardCharsets.UTF_8.encode(CharBuffer.wrap(password)); try { byte[] pwBytes = new byte[bb.remaining()]; bb.get(pwBytes); return pwBytes; } finally { // Wipe of the intermediate buffer if (bb.hasArray()) { Arrays.fill(bb.array(), bb.arrayOffset(), bb.arrayOffset() + bb.capacity(), (byte) 0); } else { // Fallback just in case a future JDK returns a DirectBuffer here bb.clear(); while (bb.hasRemaining()) { bb.put((byte) 0); } } } } /** * 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; } } ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/Gs2AttributeValue.java000066400000000000000000000025031521031247500324700ustar00rootroot00000000000000/* * 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 { Gs2AttributeValue(@NotNull Gs2Attributes attribute, @Nullable String value) { super(attribute, value); if (attribute.isRequiredValue()) { checkNotNull(value, "value"); } } /** * 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 */ @NotNull static Gs2AttributeValue parse(@NotNull String value) { if (value.isEmpty() || value.length() == 2 || value.length() > 2 && value.charAt(1) != '=') { throw new IllegalArgumentException("Invalid Gs2AttributeValue"); } Gs2Attributes byChar = Gs2Attributes.byChar(value.charAt(0)); String val = value.length() > 2 ? value.substring(2) : null; return new Gs2AttributeValue(byChar, val); } } ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/Gs2Attributes.java000066400000000000000000000034531521031247500316630ustar00rootroot00000000000000/* * Copyright (c) 2017 OnGres, Inc. * SPDX-License-Identifier: BSD-2-Clause */ package com.ongres.scram.common; import org.jetbrains.annotations.NotNull; /** * Possible values of a GS2 Attribute. * * @see [RFC5802] Formal Syntax */ enum Gs2Attributes implements CharSupplier { /** * Channel binding attribute. Client doesn't support channel binding. */ CLIENT_NOT(Gs2CbindFlag.CLIENT_NOT.getChar(), false), /** * Channel binding attribute. Client does support channel binding but thinks the server does not. */ CLIENT_YES_SERVER_NOT(Gs2CbindFlag.CLIENT_YES_SERVER_NOT.getChar(), false), /** * Channel binding attribute. Client requires channel binding. The selected channel binding * follows "p=". */ CHANNEL_BINDING_REQUIRED(Gs2CbindFlag.CHANNEL_BINDING_REQUIRED.getChar(), true), /** * SCRAM attribute. This attribute specifies an authorization identity. */ AUTHZID(ScramAttributes.AUTHZID.getChar(), true); private final char flag; private final boolean requiredValue; Gs2Attributes(char flag, boolean requiredValue) { this.flag = flag; this.requiredValue = requiredValue; } @Override public char getChar() { return flag; } boolean isRequiredValue() { return requiredValue; } @NotNull 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; default: throw new IllegalArgumentException("Invalid GS2Attribute character '" + c + "'"); } } @NotNull static Gs2Attributes byGs2CbindFlag(Gs2CbindFlag cbindFlag) { return byChar(cbindFlag.getChar()); } } ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/Gs2CbindFlag.java000066400000000000000000000024011521031247500313360ustar00rootroot00000000000000/* * Copyright (c) 2017 OnGres, Inc. * SPDX-License-Identifier: BSD-2-Clause */ package com.ongres.scram.common; import org.jetbrains.annotations.NotNull; /** * 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 CharSupplier { /** * 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; } @NotNull 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; default: throw new IllegalArgumentException("Invalid Gs2CbindFlag character '" + c + "'"); } } } ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/Gs2Header.java000066400000000000000000000150471521031247500307270ustar00rootroot00000000000000/* * 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.util.Preconditions; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * GS2 header for SCRAM. * * * * * * * * * * * * * * *
Formal Syntax:
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-headergs2-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
* * @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. * *

{@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; } } } ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/ScramAttributeValue.java000066400000000000000000000025411521031247500331040ustar00rootroot00000000000000/* * 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 { public ScramAttributeValue(@NotNull ScramAttributes attribute, @NotNull String value) { super(attribute, checkNotNull(value, "value")); } @Override public final @NotNull String getValue() { return castNonNull(super.getValue()); } /** * Parses a potential ScramAttributeValue String. * * @param value The string that contains the Attribute-Value pair. * @return The parsed class * @throws ScramParseException If the argument is empty or an invalid Attribute-Value */ public static @NotNull ScramAttributeValue parse(@NotNull String value) throws ScramParseException { if (value == null || value.length() < 3 || value.charAt(1) != '=') { throw new ScramParseException("Invalid ScramAttributeValue '" + value + "'"); } return new ScramAttributeValue(ScramAttributes.byChar(value.charAt(0)), value.substring(2)); } } ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/ScramAttributes.java000066400000000000000000000116131521031247500322720ustar00rootroot00000000000000/* * 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 com.ongres.scram.common.exception.ScramParseException; /** * SCRAM Attributes as defined in Section * 5.1 of the RFC. * *

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:

  • the GS2 header from the client's first message (recall * that the GS2 header contains a channel binding flag and an optional authzid). This header is * going to include channel binding type prefix (see [RFC5056]), if and only if the client is using * channel binding;
  • followed by the external channel's channel binding data, if and * only if the client is using channel binding.
*/ 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"); } } } ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/ScramFunctions.java000066400000000000000000000220041521031247500321100ustar00rootroot00000000000000/* * 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(); } } ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/ScramMechanism.java000066400000000000000000000213601521031247500320500ustar00rootroot00000000000000/* * 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.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.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. * *

{@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", "HmacSHA1"), /** * SCRAM-SHA-1-PLUS mechanism, defined in RFC-5802. */ SCRAM_SHA_1_PLUS("SCRAM-SHA-1-PLUS", "SHA-1", "HmacSHA1"), /** * SCRAM-SHA-224 mechanism, not defined in an RFC. */ SCRAM_SHA_224("SCRAM-SHA-224", "SHA-224", "HmacSHA224"), /** * SCRAM-SHA-224-PLUS mechanism, not defined in an RFC. */ SCRAM_SHA_224_PLUS("SCRAM-SHA-224-PLUS", "SHA-224", "HmacSHA224"), /** * SCRAM-SHA-256 mechanism, defined in RFC-7677. */ SCRAM_SHA_256("SCRAM-SHA-256", "SHA-256", "HmacSHA256"), /** * SCRAM-SHA-256-PLUS mechanism, defined in RFC-7677. */ SCRAM_SHA_256_PLUS("SCRAM-SHA-256-PLUS", "SHA-256", "HmacSHA256"), /** * SCRAM-SHA-384 mechanism, not defined in an RFC. */ SCRAM_SHA_384("SCRAM-SHA-384", "SHA-384", "HmacSHA384"), /** * SCRAM-SHA-384-PLUS mechanism, not defined in an RFC. */ SCRAM_SHA_384_PLUS("SCRAM-SHA-384-PLUS", "SHA-384", "HmacSHA384"), /** * SCRAM-SHA-512 mechanism. */ SCRAM_SHA_512("SCRAM-SHA-512", "SHA-512", "HmacSHA512"), /** * SCRAM-SHA-512-PLUS mechanism. */ SCRAM_SHA_512_PLUS("SCRAM-SHA-512-PLUS", "SHA-512", "HmacSHA512"), /** * SCRAM-SHA3-512 mechanism. */ SCRAM_SHA3_512("SCRAM-SHA3-512", "SHA3-512", "HmacSHA3-512"), /** * SCRAM-SHA3-512-PLUS mechanism. */ SCRAM_SHA3_512_PLUS("SCRAM-SHA3-512-PLUS", "SHA3-512", "HmacSHA3-512"); private static final @Unmodifiable Map BY_NAME_MAPPING = Arrays.stream(values()) .filter(ScramMechanism::isAlgorithmSupported) .collect(Collectors.collectingAndThen( Collectors.toMap(ScramMechanism::getName, Function.identity()), Collections::unmodifiableMap)); private static final @Unmodifiable List SUPPORTED_MECHANISMS = BY_NAME_MAPPING.keySet() .stream() .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); private final @NotNull String mechanismName; private final @NotNull String hashAlgorithmName; private final @NotNull String hmacAlgorithmName; private final boolean channelBinding; ScramMechanism(String name, String hashAlgorithmName, String hmacAlgorithmName) { this.mechanismName = checkNotNull(name, "name"); this.hashAlgorithmName = checkNotNull(hashAlgorithmName, "hashAlgorithmName"); this.hmacAlgorithmName = checkNotNull(hmacAlgorithmName, "hmacAlgorithmName"); this.channelBinding = name.endsWith("-PLUS"); } /** * Method that returns the name of the hash algorithm. It is protected since should be of no * interest for direct users. The instance is supposed to provide abstractions over the algorithm * names, and are not meant to be directly exposed. * * @return The name of the hash algorithm */ @NotNull String getHashAlgorithmName() { return hashAlgorithmName; } /** * Method that returns the name of the HMAC algorithm. It is protected since should be of no * interest for direct users. The instance is supposed to provide abstractions over the algorithm * names, and are not meant to be directly exposed. * * @return The name of the HMAC algorithm */ @NotNull String getHmacAlgorithmName() { return hmacAlgorithmName; } /** * The name of the mechanism. * *

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; } /** * 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( Mac.getInstance(hmacAlgorithmName), normalizedPassword, salt, iterationCount); } catch (NoSuchAlgorithmException ex) { throw new ScramRuntimeException( "Unsupported " + hmacAlgorithmName + " 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); return true; } catch (NoSuchAlgorithmException e) { return false; } } } ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/ScramStringFormatting.java000066400000000000000000000077771521031247500334650ustar00rootroot00000000000000/* * 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)); } } ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/ServerFinalMessage.java000066400000000000000000000126361521031247500327110ustar00rootroot00000000000000/* * 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}. * * * * * * * * * * * * * * * * * * * *
Formal Syntax:
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 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; } } ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/ServerFirstMessage.java000066400000000000000000000134321521031247500327420ustar00rootroot00000000000000/* * 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}. * * * * * * * * * * * * * * * *
Formal Syntax:
nonce"r=" c-nonce [s-nonce]
* ;; Second part provided by server.
salt"s=" base64
server-first-message[reserved-mext ","] nonce "," salt ",
* "iteration-count ["," 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. * * * * * * * * * * * * * * * *
Formal Syntax:
server-error"e=" server-error-value
verifier"v=" base64
* ;; base-64 encoded ServerSignature.
server-final-message(server-error / verifier)
* ["," 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))); } } ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/StringPreparation.java000066400000000000000000000060511521031247500326310ustar00rootroot00000000000000/* * 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. * *

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. * *

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; } } ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/StringWritable.java000066400000000000000000000007631521031247500321220ustar00rootroot00000000000000/* * 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); } ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/StringWritableCsv.java000066400000000000000000000075241521031247500326000ustar00rootroot00000000000000/* * 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); } } ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/UsAsciiUtils.java000066400000000000000000000033401521031247500315350ustar00rootroot00000000000000/* * 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)); } } ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/exception/000077500000000000000000000000001521031247500303075ustar00rootroot00000000000000ScramException.java000066400000000000000000000015051521031247500340200ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/exception/* * 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); } } ScramInterruptedException.java000066400000000000000000000026231521031247500362500ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/exception/* * Copyright (c) 2026 OnGres, Inc. * SPDX-License-Identifier: BSD-2-Clause */ package com.ongres.scram.common.exception; /** * Thrown when a SCRAM authentication process is interrupted, typically during * CPU-intensive PBKDF2 computation. * *

This class serves as a {@link ScramRuntimeException} wrapper for an * interruption signal. It is primarily used during cryptographic operations * where a checked {@link InterruptedException} cannot be propagated directly * due to API or functional interface constraints. * * @see Thread#isInterrupted() * @see InterruptedException * @see ScramRuntimeException * * @since 3.3 */ public class ScramInterruptedException extends ScramRuntimeException { private static final long serialVersionUID = 1L; /** * Constructs a new {@code ScramInterruptedException} with the specified detail message. * * @param detail the detail message explaining the context of the interruption. */ public ScramInterruptedException(String detail) { super(detail); } /** * Constructs a new {@code ScramInterruptedException} with the specified detail * message and the underlying cause. * * @param detail the detail message explaining the context. * @param ex the cause (which is saved for later retrieval by the {@link #getCause()} method). */ public ScramInterruptedException(String detail, Throwable ex) { super(detail, ex); } } ScramInvalidServerSignatureException.java000066400000000000000000000021261521031247500404000ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/exception/* * 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}. * *

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); } } ScramParseException.java000066400000000000000000000015161521031247500350150ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/exception/* * 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); } } ScramRuntimeException.java000066400000000000000000000015471521031247500353720ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/exception/* * 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); } } ScramServerErrorException.java000066400000000000000000000027541521031247500362300ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/exception/* * 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; } } ServerErrorValue.java000066400000000000000000000042571521031247500343600ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/exception/* * 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 ERROR_MESSAGE = initServerErrorValue(); private ServerErrorValue() { throw new IllegalStateException(); } private static ConcurrentMap initServerErrorValue() { ConcurrentMap map = new ConcurrentHashMap<>(); map.put("invalid-encoding", "The message format or encoding is incorrect"); map.put("extensions-not-supported", "Requested extensions are not recognized by the server"); map.put("invalid-proof", "The client-provided proof is invalid"); map.put("channel-bindings-dont-match", "Channel bindings sent by the client don't match those expected by the server."); map.put("server-does-support-channel-binding", "Server doesn't support channel binding at all."); map.put("channel-binding-not-supported", "Channel binding is not supported for this user"); map.put("unsupported-channel-binding-type", "The requested channel binding type is not supported."); map.put("unknown-user", "The specified username is not recognized"); map.put("invalid-username-encoding", "The username encoding is invalid (either invalid UTF-8 or SASLprep failure)"); map.put("no-resources", "The server lacks resources to process the request"); map.put("other-error", "A generic error occurred that doesn't fit into other categories"); return map; } /** * This get the error message used in a {@link ScramServerErrorException}. * * @param errorValue the {@code server-error-value} send by the server * @return String with a user friendly message about the error */ public static String getErrorMessage(String errorValue) { return ERROR_MESSAGE.get(errorValue); } } ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/exception/package-info.java000066400000000000000000000003621521031247500334770ustar00rootroot00000000000000/* * Copyright (c) 2017 OnGres, Inc. * SPDX-License-Identifier: BSD-2-Clause */ /** * This package expose the exceptions that can be throw by the client/server * implementations of SCRAM. */ package com.ongres.scram.common.exception; ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/package-info.java000066400000000000000000000004741521031247500315050ustar00rootroot00000000000000/* * Copyright (c) 2017 OnGres, Inc. * SPDX-License-Identifier: BSD-2-Clause */ /** * This package expose the messages used to implement a client/server of Salted Challenge Response * Authentication Mechanism (SCRAM). * * @implNote {@code extensions} are not supported. */ package com.ongres.scram.common; ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/util/000077500000000000000000000000001521031247500272665ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/util/Preconditions.java000066400000000000000000000103111521031247500327450ustar00rootroot00000000000000/* * Copyright (c) 2017 OnGres, Inc. * SPDX-License-Identifier: BSD-2-Clause */ package com.ongres.scram.common.util; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * Generic utility methods used to validate data. * * @apiNote This is not part of the public API of the SCRAM library, it's provided as a helper * utility and could be renamed or removed at any time. */ public final class Preconditions { private Preconditions() { throw new IllegalStateException("Utility class"); } /** * Checks that the argument is not null. * * @param value The value to be checked * @param valueName The name of the value that is checked in the method * @param The type of the value * @return The same value passed as argument * @throws IllegalArgumentException If value is null. */ public static @NotNull T checkNotNull(@Nullable T value, @NotNull String valueName) { if (null == value) { throw new IllegalArgumentException("Null value for '" + valueName + "'"); } return value; } @SuppressWarnings("null") public static @NotNull T castNonNull(@Nullable T ref) { assert ref != null : "Misuse of castNonNull: called with a null argument"; return (@NotNull T) ref; } /** * Checks that the String is not null and not empty. * * @param value The String to check * @param valueName The name of the value that is checked in the method * @return The same String passed as argument * @throws IllegalArgumentException If value is null or empty */ public static @NotNull String checkNotEmpty(@NotNull String value, @NotNull String valueName) { if (checkNotNull(value, valueName).isEmpty()) { throw new IllegalArgumentException("The value for '" + valueName + "' must not be empty"); } return value; } /** * Checks that the char[] is not null and not empty. * * @param value The String to check * @param valueName The name of the value that is checked in the method * @return The same String passed as argument * @throws IllegalArgumentException If value is null or empty */ public static char @NotNull [] checkNotEmpty(char @NotNull [] value, @NotNull String valueName) { if (checkNotNull(value, valueName).length == 0) { throw new IllegalArgumentException("The value for '" + valueName + "' must not be empty"); } return value; } /** * Checks that the argument is valid, based in a check boolean condition. * * @param check The boolean check * @param valueName The name of the value that is checked in the method * @throws IllegalArgumentException if check is not valid */ public static void checkArgument(boolean check, @NotNull String valueName) { if (!check) { throw new IllegalArgumentException("Argument '" + valueName + "' is not valid"); } } /** * Checks that the argument is valid, based in a check boolean condition. * * @param check The boolean check * @param valueName The name of the value that is checked in the method * @param errMsg Detail of the error message * @throws IllegalArgumentException if check is not valid */ public static void checkArgument(boolean check, @NotNull String valueName, @NotNull String errMsg) { if (!check) { throw new IllegalArgumentException("Argument '" + valueName + "' is not valid, " + errMsg); } } /** * Checks that the integer argument is positive. * * @param value The value to be checked * @param valueName The name of the value that is checked in the method * @return The same value passed as argument * @throws IllegalArgumentException If value is equal or less than 0 */ public static int gt0(int value, @NotNull String valueName) { if (value <= 0) { throw new IllegalArgumentException("'" + valueName + "' must be positive, was: " + value); } return value; } /** * Returns {@code true} if the given string is null or is the empty string. * * @param string a String reference to check * @return {@code true} if the string is null or the string is empty */ public static boolean isNullOrEmpty(@Nullable String string) { return string == null || string.isEmpty(); } } ongres-scram-f6bd6e2/scram-common/src/main/java/com/ongres/scram/common/util/TlsServerEndpoint.java000066400000000000000000000147551521031247500335770ustar00rootroot00000000000000/* * Copyright (c) 2017 OnGres, Inc. * SPDX-License-Identifier: BSD-2-Clause */ package com.ongres.scram.common.util; import java.io.IOException; import java.security.AlgorithmParameters; import java.security.GeneralSecurityException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.security.spec.PSSParameterSpec; import java.util.Locale; import org.jetbrains.annotations.NotNull; /** * Utilitiy for extracting the {@code "tls-server-end-point"} channel binding data. * * @apiNote This is not part of the public API of the SCRAM library, it's provided as a helper to * extract the channel-binding data and could be renamed or removed at any time. */ public final class TlsServerEndpoint { /** * The "tls-server-end-point" Channel Binding Type. */ public static final String TLS_SERVER_END_POINT = "tls-server-end-point"; private TlsServerEndpoint() { throw new IllegalStateException("Utility class"); } /** * Get the digest algorithm that would be used for a given signature algorithm name. * *

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 MessageDigest algorithm * @throws NoSuchAlgorithmException if the signature algorithm is unrecognized or unmapped * @see The tls-server-end-point * Channel Binding Type */ private static MessageDigest getDigestAlgorithm(final X509Certificate serverCert) throws NoSuchAlgorithmException { String sigAlgName = serverCert.getSigAlgName(); if (sigAlgName == null) { throw new NoSuchAlgorithmException("Certificate signature algorithm is null"); } // Normalize name to upper case sigAlgName = sigAlgName.toUpperCase(Locale.ROOT); String algorithmName; // Handle Parameterized Algorithms (Bury the hash in ASN.1 params) if ("RSASSA-PSS".equals(sigAlgName)) { byte[] sigAlgParams = serverCert.getSigAlgParams(); if (sigAlgParams == null) { throw new NoSuchAlgorithmException("RSASSA-PSS signature parameters are missing"); } try { AlgorithmParameters params = AlgorithmParameters.getInstance("RSASSA-PSS"); params.init(sigAlgParams); PSSParameterSpec spec = params.getParameterSpec(PSSParameterSpec.class); algorithmName = spec.getDigestAlgorithm().toUpperCase(Locale.ROOT); } catch (IOException | GeneralSecurityException e) { throw new NoSuchAlgorithmException("Could not extract hash from RSASSA-PSS parameters", e); } } else { // Handle Traditional Algorithms (Hash is explicitly in the name, e.g., "SHA256withRSA") int index = sigAlgName.indexOf("WITH"); if (index > 0) { algorithmName = sigAlgName.substring(0, index); // e.g., "SHA256" or "SHA3-512" } else { // Correctly mirrors PostgreSQL/OpenSSL behavior: Ed25519/Ed448 yield NID_undef. // We must fail here because the Postgres server will also fail. throw new NoSuchAlgorithmException( "Could not determine server certificate signature algorithm: " + serverCert.getSigAlgName()); } } // Normalize the hash name (Applies to both Traditional and RSASSA-PSS) switch (algorithmName) { // Enforce RFC 5929 Mandatory Upgrades (MD5 or SHA-1 must become SHA-256) case "MD5": case "SHA1": case "SHA-1": case "SHA256": case "SHA-256": algorithmName = "SHA-256"; break; case "SHA224": case "SHA-224": algorithmName = "SHA-224"; break; case "SHA384": case "SHA-384": algorithmName = "SHA-384"; break; case "SHA512": case "SHA-512": algorithmName = "SHA-512"; break; default: // Pass-through for valid modern algorithms like SHA3-512 break; } return MessageDigest.getInstance(algorithmName); } /** * The hash of the TLS server's certificate [RFC5280] as it appears, octet for octet, in the * server's Certificate message. * * @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. * @deprecated this method silently swallows {@link NoSuchAlgorithmException} and returns an * empty array. It is replaced by {@link #getChannelBindingHash(X509Certificate)} * and will be removed in a future release. */ @Deprecated public static byte @NotNull [] getChannelBindingData(final @NotNull X509Certificate serverCert) throws CertificateEncodingException { try { return getChannelBindingHash(serverCert); } catch (NoSuchAlgorithmException e) { // Preserve the old (and dangerous) silent-failure behavior for backward compatibility return new byte[0]; } } /** * Computes the hash of the TLS server's certificate [RFC5280] as it appears, octet for octet, in * the server's Certificate message, for use as {@code "tls-server-end-point"} channel binding. * *

The TLS server's certificate bytes need to be hashed with {@code SHA-256} if its signature * algorithm is {@code MD5} or {@code SHA-1} as per * RFC 5929 Section 4.1. If another * algorithm is used, the same hash function as the signature algorithm is applied. Unsupported * or unmapped signature structures throw a {@link NoSuchAlgorithmException}. * * @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 * @throws NoSuchAlgorithmException if the required digest algorithm cannot be determined or is * unsupported by the underlying security provider */ public static byte @NotNull [] getChannelBindingHash(final @NotNull X509Certificate serverCert) throws CertificateEncodingException, NoSuchAlgorithmException { MessageDigest digestAlgorithm = getDigestAlgorithm(serverCert); return digestAlgorithm.digest(serverCert.getEncoded()); } } ongres-scram-f6bd6e2/scram-common/src/main/java9/000077500000000000000000000000001521031247500217325ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-common/src/main/java9/module-info.java000066400000000000000000000004311521031247500250110ustar00rootroot00000000000000/* * 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; }ongres-scram-f6bd6e2/scram-common/src/test/000077500000000000000000000000001521031247500207535ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-common/src/test/java/000077500000000000000000000000001521031247500216745ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-common/src/test/java/com/000077500000000000000000000000001521031247500224525ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-common/src/test/java/com/ongres/000077500000000000000000000000001521031247500237475ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-common/src/test/java/com/ongres/scram/000077500000000000000000000000001521031247500250545ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-common/src/test/java/com/ongres/scram/JarFileCheckIT.java000066400000000000000000000044751521031247500304400ustar00rootroot00000000000000/* * 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()); } } ongres-scram-f6bd6e2/scram-common/src/test/java/com/ongres/scram/common/000077500000000000000000000000001521031247500263445ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-common/src/test/java/com/ongres/scram/common/ClientFinalMessageTest.java000066400000000000000000000010361521031247500335440ustar00rootroot00000000000000/* * 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()); } } ongres-scram-f6bd6e2/scram-common/src/test/java/com/ongres/scram/common/ClientFirstMessageTest.java000066400000000000000000000126441521031247500336110ustar00rootroot00000000000000/* * 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); } } ongres-scram-f6bd6e2/scram-common/src/test/java/com/ongres/scram/common/Gs2AttributeValueTest.java000066400000000000000000000043521521031247500333670ustar00rootroot00000000000000/* * 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.assertThrows; 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)); } } } ongres-scram-f6bd6e2/scram-common/src/test/java/com/ongres/scram/common/Gs2HeaderTest.java000066400000000000000000000046501521031247500316200ustar00rootroot00000000000000/* * 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 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])); } } } ongres-scram-f6bd6e2/scram-common/src/test/java/com/ongres/scram/common/HiFunctionTest.java000066400000000000000000000202431521031247500321160ustar00rootroot00000000000000/* * Copyright (c) 2026 OnGres, Inc. * SPDX-License-Identifier: BSD-2-Clause */ package com.ongres.scram.common; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; import java.util.Base64; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import javax.crypto.Mac; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import com.ongres.scram.common.exception.ScramInterruptedException; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; class HiFunctionTest { private static final SecureRandom SECURE_RANDOM = new SecureRandom(); // Character sets for different edge cases private static final String ALPHANUMERIC = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; private static final String SPECIAL = "!@#$%^&*()-_=+[{]}\\|;:'\",.<>/?`~ "; private static final String UNICODE_EMOJI = "🚀🔥🛡️🔑"; private static final String UNICODE_EXTENDED = "こんにちは世界"; // "Hello World" in Japanese private static final String COMBINED_POOL = ALPHANUMERIC + SPECIAL + UNICODE_EXTENDED + UNICODE_EMOJI; private static final int[] VALID_CODE_POINTS = COMBINED_POOL.codePoints().toArray(); private static char[] generateRandom(int length) { StringBuilder sb = new StringBuilder(length); for (int i = 0; i < length; i++) { int randomCodePoint = VALID_CODE_POINTS[SECURE_RANDOM.nextInt(VALID_CODE_POINTS.length)]; sb.appendCodePoint(randomCodePoint); } return sb.toString().toCharArray(); } /** * ScramMechanism x Random Passwords (of different lengths) x Iteration Counts. */ static Stream scramTestMatrix() { return Stream.of(ScramMechanism.values()) .filter(m -> !m.isPlus()) .flatMap(mechanism -> SECURE_RANDOM.ints(1, 200).limit(15).boxed() .flatMap(pwLength -> Stream.of(1, 4096, 10_000, 25_000) .map(iter -> Arguments.of(mechanism, pwLength, iter)))); } @ParameterizedTest(name = "{0} | PW Len: {1} | Iter: {2}") @MethodSource("scramTestMatrix") void testAlgorithmCorrectness(ScramMechanism mechanism, int pwLength, int iterations) { String hmacAlgorithm = mechanism.getHmacAlgorithmName(); char[] password = StringPreparation.POSTGRESQL_PREPARATION.normalize(generateRandom(pwLength)); int randomSaltSize = ThreadLocalRandom.current().nextInt(1, 200); byte[] salt = CryptoUtil.salt(randomSaltSize, SECURE_RANDOM); try { Mac mac = Mac.getInstance(hmacAlgorithm); byte[] actual = CryptoUtil.hi(mac, password, salt, iterations); // Verify against standard PBKDF2 (which 'hi' is based on) SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2With" + hmacAlgorithm); PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, mac.getMacLength() * 8); byte[] expected = factory.generateSecret(spec).getEncoded(); assertArrayEquals(expected, actual, "Hi mismatch for " + hmacAlgorithm); } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { Assumptions.abort("Skipping: " + hmacAlgorithm + " not supported by current Provider."); return; } } @Test void testNullHandling() throws Exception { Mac mac = Mac.getInstance("HmacSHA256"); char[] pass = "pw".toCharArray(); byte[] salt = CryptoUtil.salt(24, new SecureRandom()); assertAll("Null checks", () -> assertThrows(IllegalArgumentException.class, () -> CryptoUtil.hi(null, pass, salt, 1)), () -> assertThrows(IllegalArgumentException.class, () -> CryptoUtil.hi(mac, null, salt, 1)), () -> assertThrows(IllegalArgumentException.class, () -> CryptoUtil.hi(mac, pass, null, 1))); } @Test void testInvalidInputs() throws Exception { Mac mac = Mac.getInstance("HmacSHA256"); char[] pass = "pw".toCharArray(); byte[] salt = CryptoUtil.salt(16, new SecureRandom()); assertThrows(IllegalArgumentException.class, () -> CryptoUtil.hi(mac, pass, new byte[0], 100)); assertThrows(IllegalArgumentException.class, () -> CryptoUtil.hi(mac, pass, salt, 0)); } @Test void testInterruptionBehavior() throws Exception { Mac mac = Mac.getInstance("HmacSHA256"); char[] password = "long-running-task".toCharArray(); byte[] salt = CryptoUtil.salt(24, new SecureRandom()); int iterations = 1_000_000; try { Thread.currentThread().interrupt(); ScramInterruptedException exception = assertThrows(ScramInterruptedException.class, () -> CryptoUtil.hi(mac, password, salt, iterations)); assertTrue(exception.getMessage().contains("PBKDF2 computation interrupted"), "Expected interruption error but got: " + exception.getMessage()); } finally { // CRITICAL: Clear the interrupt flag so it doesn't bleed into other tests! // Thread.interrupted() returns the flag state AND resets it to false. Thread.interrupted(); } } @Test void testInterruptionMidExecution() throws Exception { Mac mac = Mac.getInstance("HmacSHA256"); char[] password = "long-running-task".toCharArray(); byte[] salt = CryptoUtil.salt(24, new SecureRandom()); int iterations = 50_000_000; // High enough to stay in the loop AtomicReference actualError = new AtomicReference<>(); CountDownLatch threadStarted = new CountDownLatch(1); CountDownLatch threadFinished = new CountDownLatch(1); Thread worker = new Thread(() -> { threadStarted.countDown(); // 1. Signal that we are alive try { CryptoUtil.hi(mac, password, salt, iterations); } catch (Throwable t) { actualError.set(t); // 4. Capture the exception safely } finally { threadFinished.countDown(); } }); worker.start(); // 2. Wait until the thread has actually started executing threadStarted.await(); // Give it a tiny fraction of a second to enter the PBKDF2 loop Thread.sleep(50); // 3. Fire the interrupt mid-flight worker.interrupt(); // 5. Wait for the thread to shut down (with a timeout so tests never hang forever) boolean finishedCleanly = threadFinished.await(2, TimeUnit.SECONDS); assertTrue(finishedCleanly, "The thread hung and did not respect the interrupt signal!"); // 6. Verify the captured exception Throwable caught = actualError.get(); assertNotNull(caught, "Expected an exception, but the task completed normally."); assertInstanceOf(ScramInterruptedException.class, caught, "Expected ScramInterruptedException but got: " + caught.getClass().getName()); } @Test void testHmacSHA3_512() throws Exception { // Derived key generated via "golang.org/x/crypto/pbkdf2" String expectedHex = "AVe/93um8jge6rPdfKvlV11zTpETcH56COQ+GToe4F3tv+99LYkIYYY4rLFbjbAz+6mdbZXVBbphDmgN2o3LSQ=="; byte[] expected = Base64.getDecoder().decode(expectedHex); // Gracefully skip on JVMs (like Java 8) that lack native SHA-3 support Mac mac; try { mac = Mac.getInstance("HmacSHA3-512"); } catch (NoSuchAlgorithmException e) { Assumptions.abort("Skipping test: JVM does not support HmacSHA3-512"); return; } char[] password = "my_super_secret_password".toCharArray(); byte[] salt = "my_random_salt_value".getBytes(StandardCharsets.UTF_8); int iterations = 100_000; byte[] actual = CryptoUtil.hi(mac, password, salt, iterations); assertArrayEquals(expected, actual, "Hi mismatch for HmacSHA3-512"); } } ongres-scram-f6bd6e2/scram-common/src/test/java/com/ongres/scram/common/RfcExampleSha1.java000066400000000000000000000031351521031247500317540ustar00rootroot00000000000000/* * 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="; } ongres-scram-f6bd6e2/scram-common/src/test/java/com/ongres/scram/common/RfcExampleSha256.java000066400000000000000000000032421521031247500321270ustar00rootroot00000000000000/* * 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="; } ongres-scram-f6bd6e2/scram-common/src/test/java/com/ongres/scram/common/SaslPrepTest.java000066400000000000000000000025401521031247500316010ustar00rootroot00000000000000/* * 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()); } } } ongres-scram-f6bd6e2/scram-common/src/test/java/com/ongres/scram/common/ScramAttributeValueTest.java000066400000000000000000000056211521031247500340010ustar00rootroot00000000000000/* * 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")); } } ongres-scram-f6bd6e2/scram-common/src/test/java/com/ongres/scram/common/ScramFunctionsTest.java000066400000000000000000000237201521031247500330110ustar00rootroot00000000000000/* * 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.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())); } } ongres-scram-f6bd6e2/scram-common/src/test/java/com/ongres/scram/common/ScramMechanismTest.java000066400000000000000000000070261521031247500327460ustar00rootroot00000000000000/* * 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.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.List; import javax.crypto.Mac; 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) throws NoSuchAlgorithmException { byte[] digest = scramMechanism.digest(new byte[0]); MessageDigest md = MessageDigest.getInstance(scramMechanism.getHashAlgorithmName()); assertNotNull(digest, "got a null digest"); assertEquals(md.getDigestLength(), digest.length); } @ParameterizedTest @MethodSource("provideSupportedMechanisms") void testHmacSupportedByJvm(@NotNull String mechanism) throws NoSuchAlgorithmException { ScramMechanism scramMechanism = ScramMechanism.byName(mechanism); String hmacAlgorithmName = scramMechanism.getHmacAlgorithmName(); Mac mac = Mac.getInstance(hmacAlgorithmName); assertNotNull(scramMechanism); byte[] hmac = scramMechanism.hmac(EMPTY_KEY, new byte[0]); assertNotNull(hmac, "got a null HMAC"); assertEquals(mac.getMacLength(), hmac.length); } private static @NotNull List<@NotNull String> provideSupportedMechanisms() { return ScramMechanism.supportedMechanisms(); } } ScramStringFormattingTest.java000066400000000000000000000037071521031247500342660ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-common/src/test/java/com/ongres/scram/common/* * 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"); } } ongres-scram-f6bd6e2/scram-common/src/test/java/com/ongres/scram/common/ServerFinalMessageTest.java000066400000000000000000000050011521031247500335700ustar00rootroot00000000000000/* * 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")); } } ongres-scram-f6bd6e2/scram-common/src/test/java/com/ongres/scram/common/ServerFirstMessageTest.java000066400000000000000000000017301521031247500336330ustar00rootroot00000000000000/* * 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()); } } ongres-scram-f6bd6e2/scram-common/src/test/java/com/ongres/scram/common/StringPreparationTest.java000066400000000000000000000146121521031247500335260ustar00rootroot00000000000000/* * 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 provideRandomStrings() { final SecureRandom srand = new SecureRandom(); return IntStream.iterate(0, i -> i) .limit(1000) .mapToObj(c -> srand.ints(32, 127) .filter(i -> (i >= 32) && (i <= 127)) .limit(128) .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) .toString()); } /* * Some simple random testing won't hurt. If a test would fail, create new test with the generated * word. */ @ParameterizedTest @MethodSource("provideRandomStrings") void doNormalizeValidAsciiRandom(String random) { for (StringPreparation stringPreparation : StringPreparation.values()) { assertArrayEquals(random.toCharArray(), stringPreparation.normalize(random.toCharArray()), "'" + random + "' is a printable ASCII string, should not be changed by normalize()"); } } @Test void doNormalizeNoPreparationEmptyAfterNormalization() { for (String s : ONLY_NON_PRINTABLE_STRINGS) { char[] charArray = s.toCharArray(); assertThrows(IllegalArgumentException.class, () -> StringPreparation.NO_PREPARATION.normalize(charArray)); } } @Test void doNormalizeNoPreparationNonEmptyAfterNormalization() { // No exception should be thrown for (String s : ONLY_NON_PRINTABLE_STRINGS) { StringPreparation.NO_PREPARATION.normalize((s + "a").toCharArray()); } } } ongres-scram-f6bd6e2/scram-common/src/test/java/com/ongres/scram/common/StringWritableCsvTest.java000066400000000000000000000072431521031247500334710ustar00rootroot00000000000000/* * 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 org.junit.jupiter.api.Test; class StringWritableCsvTest { private static final String[] ONE_ARG_VALUES = new String[] {"c=channel", "i=4096", "a=authzid", "n"}; private static final String SEVERAL_VALUES_STRING = "n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL"; @Test void writeToNullOrEmpty() { assertEquals(0, StringWritableCsv.writeTo(new StringBuilder()).length()); assertEquals(0, StringWritableCsv.writeTo(new StringBuilder(), new StringWritable[] {}).length()); } @Test void writeToOneArg() { StringWritable[] pairs = new StringWritable[] { new ScramAttributeValue(ScramAttributes.CHANNEL_BINDING, "channel"), new ScramAttributeValue(ScramAttributes.ITERATION, "" + 4096), new Gs2AttributeValue(Gs2Attributes.AUTHZID, "authzid"), new Gs2AttributeValue(Gs2Attributes.CLIENT_NOT, null) }; for (int i = 0; i < pairs.length; i++) { assertEquals(ONE_ARG_VALUES[i], StringWritableCsv.writeTo(new StringBuilder(), pairs[i]).toString()); } } @Test void writeToSeveralArgs() { assertEquals( SEVERAL_VALUES_STRING, StringWritableCsv.writeTo( new StringBuilder(), new Gs2AttributeValue(Gs2Attributes.CLIENT_NOT, null), null, new ScramAttributeValue(ScramAttributes.USERNAME, "user"), new ScramAttributeValue(ScramAttributes.NONCE, "fyko+d2lbbFgONRv9qkxdawL") ).toString()); } @Test void parseFromEmpty() { assertArrayEquals(new String[] {}, StringWritableCsv.parseFrom("")); } @Test void parseFromOneArgWithLimitsOffsets() { for (String s : ONE_ARG_VALUES) { assertArrayEquals(new String[] {s}, StringWritableCsv.parseFrom(s)); } int[] numberEntries = new int[] {0, 1}; for (int n : numberEntries) { for (String s : ONE_ARG_VALUES) { assertArrayEquals(new String[] {s}, StringWritableCsv.parseFrom(s, n)); } } for (String s : ONE_ARG_VALUES) { assertArrayEquals(new String[] {s, null, null}, StringWritableCsv.parseFrom(s, 3)); } for (int n : numberEntries) { for (String s : ONE_ARG_VALUES) { assertArrayEquals(new String[] {s}, StringWritableCsv.parseFrom(s, n, 0)); } } for (String s : ONE_ARG_VALUES) { assertArrayEquals(new String[] {s, null, null}, StringWritableCsv.parseFrom(s, 3, 0)); } for (int n : numberEntries) { for (String s : ONE_ARG_VALUES) { assertArrayEquals(new String[] {null}, StringWritableCsv.parseFrom(s, n, 1)); } } } @Test void parseFromSeveralArgsWithLimitsOffsets() { assertArrayEquals( new String[] {"n", "", "n=user", "r=fyko+d2lbbFgONRv9qkxdawL"}, StringWritableCsv.parseFrom(SEVERAL_VALUES_STRING)); assertArrayEquals( new String[] {"n", ""}, StringWritableCsv.parseFrom(SEVERAL_VALUES_STRING, 2)); assertArrayEquals( new String[] {"", "n=user"}, StringWritableCsv.parseFrom(SEVERAL_VALUES_STRING, 2, 1)); assertArrayEquals( new String[] {"r=fyko+d2lbbFgONRv9qkxdawL", null}, StringWritableCsv.parseFrom(SEVERAL_VALUES_STRING, 2, 3)); assertArrayEquals( new String[] {null, null}, StringWritableCsv.parseFrom(SEVERAL_VALUES_STRING, 2, 4)); assertArrayEquals( new String[] {"n", "", "n=user", "r=fyko+d2lbbFgONRv9qkxdawL", null}, StringWritableCsv.parseFrom(SEVERAL_VALUES_STRING, 5)); } } ongres-scram-f6bd6e2/scram-common/src/test/java/com/ongres/scram/common/UsAsciiUtilsTest.java000066400000000000000000000036171521031247500324370ustar00rootroot00000000000000/* * 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 java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.NullSource; import org.junit.jupiter.params.provider.ValueSource; class UsAsciiUtilsTest { @ParameterizedTest @NullSource void toPrintableNull(String value) { assertThrows(IllegalArgumentException.class, () -> UsAsciiUtils.toPrintable(value), () -> "Calling with null value must throw IllegalArgumentException"); } @ParameterizedTest @ValueSource(strings = {"abcdé", "ñ", "€", "Наташа", (char) 127 + ""}) void toPrintableNonASCII(String value) { assertThrows(IllegalArgumentException.class, () -> UsAsciiUtils.toPrintable(value), () -> "String(s) with non-ASCII characters not throwing IllegalArgumentException"); } @ParameterizedTest @CsvSource(value = {" u , u ", "a" + (char) 12 + ",a", (char) 0 + "ttt" + (char) 31 + ",ttt"}, ignoreLeadingAndTrailingWhitespace = false) void toPrintableNonPrintable(String original, String expected) { assertEquals(expected, UsAsciiUtils.toPrintable(original)); } @Test void toPrintableAllPrintable() { List values = new ArrayList(); values.addAll(Arrays.asList( new String[] {(char) 33 + "", "user", "!", "-,.=?", (char) 126 + ""})); for (int c = 33; c < 127; c++) { values.add("---" + (char) c + "---"); } for (String s : values) { assertEquals(s, UsAsciiUtils.toPrintable(s), "All printable String '" + s + "' not returning the same value"); } } } ongres-scram-f6bd6e2/scram-common/src/test/java/com/ongres/scram/common/util/000077500000000000000000000000001521031247500273215ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-common/src/test/java/com/ongres/scram/common/util/NonceTest.java000066400000000000000000000026321521031247500320710ustar00rootroot00000000000000/* * Copyright (c) 2017 OnGres, Inc. * SPDX-License-Identifier: BSD-2-Clause */ package com.ongres.scram.common.util; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; import java.security.SecureRandom; import com.ongres.scram.common.ScramFunctions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; class NonceTest { private static final SecureRandom SECURE_RANDOM = new SecureRandom(); @ParameterizedTest @ValueSource(ints = {0, -1, Integer.MIN_VALUE}) void nonceInvalidSize(int size) { assertThrows(IllegalArgumentException.class, () -> ScramFunctions.nonce(size, SECURE_RANDOM)); } @Test void nonceValid() { int nonces = 1000; int nonceMaxSize = 100; SecureRandom random = new SecureRandom(); // Some more random testing for (int i = 0; i < nonces; i++) { final int size = SECURE_RANDOM.nextInt(nonceMaxSize) + 1; final String nonce = ScramFunctions.nonce(size, random); for (int j = 0; j < nonce.length(); j++) { char c = nonce.charAt(j); if (c == ',' || c < (char) 33 || c > (char) 126) { fail("Character c='" + c + "' is not allowed on a nonce"); } } assertEquals(size, nonce.length()); } } } TlsServerEndpointTest.java000066400000000000000000000132761521031247500344100ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-common/src/test/java/com/ongres/scram/common/util/* * Copyright (c) 2026 OnGres, Inc. * SPDX-License-Identifier: BSD-2-Clause */ package com.ongres.scram.common.util; import static org.junit.jupiter.api.Assertions.assertArrayEquals; 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.assertThrows; import java.io.IOException; import java.io.InputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.stream.Stream; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; class TlsServerEndpointTest { /** * Provides valid certificates that SHOULD succeed in generating a channel binding hash. * Format: Arguments.of(Certificate, Expected Signature Algorithm, Expected Hash Algorithm) */ private static Stream provideValidCertificates() { return Stream.of( // RFC 5929 Mandatory Upgrades (MD5 -> SHA-256) Arguments.of(loadCertificate("/MD5withRSA.pem"), "MD5withRSA", "SHA-256"), // Standard explicit algorithms Arguments.of(loadCertificate("/SHA256withECDSA.pem"), "SHA256withECDSA", "SHA-256"), Arguments.of(loadCertificate("/SHA512withRSA.pem"), "SHA512withRSA", "SHA-512"), // Modern algorithms (Might be skipped on Java 8 if provider is missing) Arguments.of(loadCertificate("/SHA3-512withECDSA.pem"), "SHA3-512withECDSA", "SHA3-512"), // Parameterized RSASSA-PSS (The generated PEM uses SHA-384) Arguments.of(loadCertificate("/RSASSA-PSS.pem"), "RSASSA-PSS", "SHA-384")); } /** * Provides certificates that MUST fail to maintain parity with PostgreSQL / OpenSSL behavior. */ private static Stream provideUnsupportedCertificates() { return Stream.of( Arguments.of(loadCertificate("/Ed25519.pem"), "Ed25519"), Arguments.of(loadCertificate("/Ed448.pem"), "Ed448")); } @SuppressWarnings("deprecation") @ParameterizedTest(name = "Extracts correct binding hash for {1} -> expects {2}") @MethodSource("provideValidCertificates") void testValidCertificateChannelBinding(X509Certificate cert, String sigAlg, String expectedHashAlg) { // Ensure the generated PEM matches the test expectation assertEquals(sigAlg, cert.getSigAlgName(), "Loaded certificate does not match expected algorithm"); // 1. Check if the current JDK/Provider supports the required hash (e.g., SHA3-512 on Java 8) MessageDigest expectedDigest = getDigestOrSkipTest(expectedHashAlg); // 2. Calculate the expected hash manually byte[] expectedHash = expectedDigest.digest(assertDoesNotThrow(cert::getEncoded)); // 3. Test the primary method byte[] actualHash = assertDoesNotThrow(() -> TlsServerEndpoint.getChannelBindingHash(cert)); assertArrayEquals(expectedHash, actualHash, "getChannelBindingHash did not return the expected digest bytes"); // 4. Test the deprecated method byte[] actualDeprecatedData = assertDoesNotThrow(() -> TlsServerEndpoint.getChannelBindingData(cert)); assertArrayEquals(expectedHash, actualDeprecatedData, "Deprecated getChannelBindingData did not match expected digest bytes"); } @SuppressWarnings({ "deprecation" }) @ParameterizedTest(name = "Rejects unsupported/pure algorithm {1} (Postgres parity)") @MethodSource("provideUnsupportedCertificates") void testUnsupportedCertificatesFail(X509Certificate cert, String sigAlg) { // Ensure the generated PEM matches the test expectation assertEquals(sigAlg, cert.getSigAlgName(), "Loaded certificate does not match expected algorithm"); // 1. Ensure the modern method throws an exception NoSuchAlgorithmException exception = assertThrows(NoSuchAlgorithmException.class, () -> TlsServerEndpoint.getChannelBindingHash(cert)); assertEquals("Could not determine server certificate signature algorithm: " + sigAlg, exception.getMessage()); // 2. Ensure the deprecated method swallows the exception and returns an empty array byte[] deprecatedResult = assertDoesNotThrow(() -> TlsServerEndpoint.getChannelBindingData(cert)); assertArrayEquals(new byte[0], deprecatedResult, "Deprecated method must return byte[0] on unsupported algorithms"); } /** * Helper method to load a certificate from the classpath. */ private static X509Certificate loadCertificate(String pemFilePath) { try (InputStream inputStream = TlsServerEndpointTest.class.getResourceAsStream(pemFilePath)) { assertNotNull(inputStream); CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); X509Certificate cert = (X509Certificate) certFactory.generateCertificate(inputStream); assertNotNull(cert); return cert; } catch (CertificateException | IOException e) { throw new RuntimeException("Failed to load test certificate: " + pemFilePath, e); } } /** * Attempts to load the MessageDigest. If the JVM does not support it, it skips the test. */ private MessageDigest getDigestOrSkipTest(String digestAlgorithm) { try { return MessageDigest.getInstance(digestAlgorithm); } catch (NoSuchAlgorithmException e) { Assumptions.abort("Skipping test: Current JVM/Provider does not support " + digestAlgorithm); return null; // Unreachable } } } ongres-scram-f6bd6e2/scram-common/src/test/resources/000077500000000000000000000000001521031247500227655ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-common/src/test/resources/Ed25519.pem000066400000000000000000000007651521031247500244360ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIBRjCB+aADAgECAhRZeYrsaHrRqL9Ut7f4tCpkmBx8bDAFBgMrZXAwGTEXMBUG A1UEAwwORWQyNTUxOS1TZXJ2ZXIwHhcNMjYwNTE0MTI0ODMxWhcNMzYwNTExMTI0 ODMxWjAZMRcwFQYDVQQDDA5FZDI1NTE5LVNlcnZlcjAqMAUGAytlcAMhAEy60dNH 6iTQ6IHKp1EkZg+D5IiBgo+5+taD3G6R8Z21o1MwUTAdBgNVHQ4EFgQUE6HPl15V FDWCHN9yS71OZ8hjpLowHwYDVR0jBBgwFoAUE6HPl15VFDWCHN9yS71OZ8hjpLow DwYDVR0TAQH/BAUwAwEB/zAFBgMrZXADQQDZ4M7GEx4cjuM4LBhj/BSCZCxkUV79 NzKcLIOg1xAMae+eaDxQiW70TdofonjNR5zqhsIz1083UdeYnIANkLMA -----END CERTIFICATE----- ongres-scram-f6bd6e2/scram-common/src/test/resources/Ed448.pem000066400000000000000000000015021521031247500242560ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIICPDCCAbygAwIBAgIUQBlLjLTvfPDBKNOELbbqzuQdhOgwBQYDK2VxMG4xCzAJ BgNVBAYTAlVTMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEVMBMGA1UE CgwMT3JnYW5pemF0aW9uMRMwEQYDVQQLDApEZXBhcnRtZW50MRQwEgYDVQQDDAtl eGFtcGxlLmNvbTAeFw0yNjA1MTkyMDUwMDlaFw0zNjA1MTYyMDUwMDlaMG4xCzAJ BgNVBAYTAlVTMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEVMBMGA1UE CgwMT3JnYW5pemF0aW9uMRMwEQYDVQQLDApEZXBhcnRtZW50MRQwEgYDVQQDDAtl eGFtcGxlLmNvbTBDMAUGAytlcQM6AMDy/jp5KAZ7UBW+q44vgywwcAWvypvXnWeT RSC2wy4UckPmPBFS/ywzlVKg9N9C4DBDuMKoElPpgKNTMFEwHQYDVR0OBBYEFOaz IA75AyQhYUOFLox9gr3KxS+MMB8GA1UdIwQYMBaAFOazIA75AyQhYUOFLox9gr3K xS+MMA8GA1UdEwEB/wQFMAMBAf8wBQYDK2VxA3MAaVV5g/VwKSEBtoC3CMHF4coR tQtlJGLsZ6DgFAw5FyERu2zpJyXbNmpn7AUsJ+iXbc1WrSzDSzgA3UItPmgiI71h ZAU3coh9TP2MbHk1/kypJTJcc2fcyPldjIiLkgg6deaRZlWGojLbT3da9lCIzQEA -----END CERTIFICATE----- ongres-scram-f6bd6e2/scram-common/src/test/resources/MD5withRSA.pem000066400000000000000000000021431521031247500253170ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDETCCAfmgAwIBAgIUf28fcml2erbsapDRANVUbePWWhIwDQYJKoZIhvcNAQEE BQAwGDEWMBQGA1UEAwwNdGVzdC1tZDUtY2VydDAeFw0yNjA1MTkxNzA2NDlaFw0z NjA1MTYxNzA2NDlaMBgxFjAUBgNVBAMMDXRlc3QtbWQ1LWNlcnQwggEiMA0GCSqG SIb3DQEBAQUAA4IBDwAwggEKAoIBAQC0tAvZegAKHDGPagQobxiV4uuMGmRJf/k/ PVK+wbGa1n4HU1gtu7XCLKmtsQA9BaKwQnUctYm0o6D0LXvz59gfSazCMb6YwAJm Q0ubtllvndUL64JN91TXEWqOcIMTGD9VMmkweO318ZZw3AYvs6vqxWZ2TEyFsJT6 5qASHlkm6Jj2rQBNsu8pTJojmN7bIVdVIHGMJ2igJHL8khkhaulbCdATJ1s7aZ/t 8VU0oYtW4hmr/W0pK6d90QxZUmNKcG3kt7X+IQ2Wy3z1NW5TiwXvxfsYOJwVP+QR gSHhlQZNyYnlblhpTILByL5ytAuzQviMIxJUmN9ZyDVmG4NqSy2ZAgMBAAGjUzBR MB0GA1UdDgQWBBRB+RBIhhELCRpAzRYaYT4NZUA/DjAfBgNVHSMEGDAWgBRB+RBI hhELCRpAzRYaYT4NZUA/DjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBBAUA A4IBAQBpxmv1ZJQwHv+EUH0HDVXg1KsymLtDX5OUPrDwbEzKLLevoXk/r7gyxuvS x7tpItL3usFUbVW1AbjTcFzGeb7LYMS/mv6vQcaMkNrb/bjVBPvS/zU6s3jbGTqp xerM6uZJoAHfsJPhaIr0l8DxPcbhra/gF6WjJG9LM/PJrfm9NB1Cr4dCqAC6X3C1 4SBM5E1Cl6nYgULGXksRAL3ANajW+28WirRkkQNsL6RBtTAXdIznHEtGj77i7aRz WVq9H4/hoJm/cEDy5R541TkVtgLHNMDhTFSrSRTWReXm3gXnLqfWUt3ceUzosc3m Fqzc/zlD72M4xLaViOXe84p/8ODG -----END CERTIFICATE----- ongres-scram-f6bd6e2/scram-common/src/test/resources/RSASSA-PSS.pem000066400000000000000000000025121521031247500251270ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDujCCAm6gAwIBAgIUHwD1nOLZ3k+si8gTZxAx6Y2dBfgwQQYJKoZIhvcNAQEK MDSgDzANBglghkgBZQMEAgIFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgIF AKIDAgEwMCExHzAdBgNVBAMMFlJTQVNTQS1QU1MtU0hBMzg0LVRlc3QwHhcNMjYw NTE5MTcxNzQ5WhcNMzYwNTE2MTcxNzQ5WjAhMR8wHQYDVQQDDBZSU0FTU0EtUFNT LVNIQTM4NC1UZXN0MIIBUTA8BgkqhkiG9w0BAQowL6APMA0GCWCGSAFlAwQCAgUA oRwwGgYJKoZIhvcNAQEIMA0GCWCGSAFlAwQCAgUAA4IBDwAwggEKAoIBAQDhDHXn shANPAuD30jsvf8KGlSRzY3DTJLSBCpCJwePEqav2fHKJCuITN6yIdMx7VwnpGL7 mlssnzqP79fsMTDaxAiFaGJ+Qw+BmtXjBjR6bgJJMvTNAKfEDiEa0AuUWrZoqlbs TgXhARs6uGV+VXud2J3z5tTi3U6btZYXGJfBkvU2YnBG90vqGTfGpcm5fhGVQhd4 4EA5j4C9s+J4+s7ATltw8mumX2wUxkVrT38wMExUTg5nOPZkeiTLhNh5MFq/YVaP 8bpSNaHgGzjrrtmwSXHmiKl51HnUXNzl3Pe9BZd2S/PU9HGOnq0OK5FsxAoXHnkX n+V3yx+u8kJHSpBtAgMBAAGjUzBRMB0GA1UdDgQWBBSuOLIsiNOqL/SlYkk2fZF+ 8bve+zAfBgNVHSMEGDAWgBSuOLIsiNOqL/SlYkk2fZF+8bve+zAPBgNVHRMBAf8E BTADAQH/MEEGCSqGSIb3DQEBCjA0oA8wDQYJYIZIAWUDBAICBQChHDAaBgkqhkiG 9w0BAQgwDQYJYIZIAWUDBAICBQCiAwIBMAOCAQEAKxai2M9Ytw2XAC7NF38P3yEK guCNNsS2HJ2LSbnjC66TnX21vyVMOU4jl6FWcztycNrKNUm740I66GO5amy71RMJ /fch+4emrso4A/+geeotdsSdWuazvucDAUfvMeTZhBdI7Iyappn7Sn3CwVvzUA7x Xd6/ou1fePRz6lLMISaUJvx6cV+HbBlir0M12MQU7BF60Seh3Pu1VL6wgYMOCEas m/06+oPNhN2Us1nwGtY7wjjuJ5ZAba8pEreSvnJhZz2VZzUdyLNj8BDyXTgrLh/X NfjzLEh+8WIEMLqvv3c19f/B7XAPmAizcz+TohHoDIqJEOcU0iUUdoQo/2ETkg== -----END CERTIFICATE----- ongres-scram-f6bd6e2/scram-common/src/test/resources/SHA256withECDSA.pem000066400000000000000000000011231521031247500257710ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIBiTCCAS+gAwIBAgIUN9qiSNKdAkv8wGQYaroNzb6xHvAwCgYIKoZIzj0EAwIw GjEYMBYGA1UEAwwPdGVzdC1wcmltZTI1NnYxMB4XDTI2MDUxOTIxMDcwMFoXDTM2 MDUxNjIxMDcwMFowGjEYMBYGA1UEAwwPdGVzdC1wcmltZTI1NnYxMFkwEwYHKoZI zj0CAQYIKoZIzj0DAQcDQgAEGYeHczdQapKPswMTsRmiDe4sJD672CodoNo6aUx5 Z1htcRstXITX7U70VckmFqnGvKjBkVghZW2HQOS6wkj3q6NTMFEwHQYDVR0OBBYE FFhfo21qAA2rSWVaWvBtLNv9iAQCMB8GA1UdIwQYMBaAFFhfo21qAA2rSWVaWvBt LNv9iAQCMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIhAJ6sw2dc qcX6YjnsGGkn0oxA0FbmEyT5nTWJ3livOZ3LAiB2OehsPSCLruo0iOPeeddSRibi NvgkEjI5bJL46XelgQ== -----END CERTIFICATE----- ongres-scram-f6bd6e2/scram-common/src/test/resources/SHA3-512withECDSA.pem000066400000000000000000000016101521031247500261250ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIICbTCCAc2gAwIBAgIUF/r9sHKRKU0yh9d1vq6wCNdF2NswCwYJYIZIAWUDBAMM MEcxCzAJBgNVBAYTAkVTMQ8wDQYDVQQIDAZNYWRyaWQxEzARBgNVBAcMCkFsY29i ZW5kYXMxEjAQBgNVBAoMCU9uR3JlcyBTTDAeFw0yNDAzMjAxNTIzNDdaFw0zNDAz MTgxNTIzNDdaMEcxCzAJBgNVBAYTAkVTMQ8wDQYDVQQIDAZNYWRyaWQxEzARBgNV BAcMCkFsY29iZW5kYXMxEjAQBgNVBAoMCU9uR3JlcyBTTDCBmzAQBgcqhkjOPQIB BgUrgQQAIwOBhgAEAbdhZ97Wg6jLy+vwojByzoRwQOdPbY1Ye6CPY2bmDJ+At7j1 dw+a/lB3MkTMhIlCPgKO+hJty8dZBIdg/hCWNtqtAb6tSsv6RQw3nAv1pY5d5qWF nVQzYiAWLsZ5MzTWKMcy3xJTAW+YEBwCzT3Ov6mO9wfz20I5SNLjXLaixMjh4Mup o1MwUTAdBgNVHQ4EFgQUlhN7FIdCf4bBZS1NDmCLpJHOJWwwHwYDVR0jBBgwFoAU lhN7FIdCf4bBZS1NDmCLpJHOJWwwDwYDVR0TAQH/BAUwAwEB/zALBglghkgBZQME AwwDgYwAMIGIAkIByQyFB6lnc/Hp/eTjibbydSrGp/3ijwHFWkf44Bb9Xxsko5g4 97DtrgL/sRAD9HPnF6P4jvGQmKT4r40e4Z4xg5oCQgGxVvM9YeGFgAHWesH/Uz8A rEySKFQU6HWOg3VGk/i+h8Mf3Q3qOMw0Mq8NTHp0t4yNwwltmZDRpzZvdF3dINMa Cg== -----END CERTIFICATE----- ongres-scram-f6bd6e2/scram-common/src/test/resources/SHA512withRSA.pem000066400000000000000000000065161521031247500256050ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIJpzCCBZugAwIBAgIUWzmIIuqdxXzf3oQiJ/UhLS9r2kcwDQYJKoZIhvcNAQEN BQAwbzELMAkGA1UEBhMCRVMxDzANBgNVBAgMBk1hZHJpZDETMBEGA1UEBwwKQWxj b2JlbmRhczEUMBIGA1UECgwLT25HcmVzIEluYy4xDjAMBgNVBAsMBVNDUkFNMRQw EgYDVQQDDAtleGFtcGxlLmNvbTAeFw0yNDAzMTMxNzQxNTVaFw0zNDAzMTExNzQx NTVaMG8xCzAJBgNVBAYTAkVTMQ8wDQYDVQQIDAZNYWRyaWQxEzARBgNVBAcMCkFs Y29iZW5kYXMxFDASBgNVBAoMC09uR3JlcyBJbmMuMQ4wDAYDVQQLDAVTQ1JBTTEU MBIGA1UEAwwLZXhhbXBsZS5jb20wggQWMA0GCSqGSIb3DQEBAQUAA4IEAwAwggP+ AoID9QCtTBX88QlbjYb3FsXDDWPZ6P3qrNjXhdecKdpxv81K2I3W/iBz3tEpj+FT 4pfWu5jYAf85yrnkg2/eFD2zGrWq+qfQnsFE+6N0M/JW6t5hTVz3rAh2C58I0cMo WIWJt4808b7VLeIwMJwE6hcGSIgyuVOgEhEqrnSmXD3PIoAkrAvpiLpqOCAdG7xN QUXySOeq8yIC3uCVwTqMonkB488RcaTMlDO4+3GCuiIHhiqAjtVGlmn0xTaLZdA7 3hQPDs3qkqWHM7a2pLNrdPoJgF5BdsTq5Rbw5TzNe8lm9NjyNbUilOPloci0VNg5 qgvKuRRqd1285wzCMEb/FQzu6eTv57pzmOzWyzd5D0K53GZv6Cb+yaz7xx6sUTDF q2qQClllXMGu+P0kpkdH2qBIRuxrOiPvcZ2djKk+xa4fDbfLI0Hk1STpqZq1KjOR 1+eFYGCawo/JFjMsQfyq/CVQDqy5djnKKOrbCKl3ApEdSvTBcoBbpIroKMqMIssB owu0b8bqEOl/tVamGm8oELh6iDQMD5OKlg5ruihp0VTvGeGbDU6LTksx0dygvIen dtIUoiEfaP/7LfTUWQ6X2dc00FD8lRRSygKHqg/Rq7HJ5Jmrfz7e4ozOnAqemxv2 P0kNqT9qhgBdbNaNlwQsVf5ikbLeRQCSVvgKhcmB0rM57E+7cf3HtMZ5ECfbgF91 cK67bVfFnnlR6apEN1rbBxES/OHJu7X6yr8CJc+FRHyg48/hLIH1p6aylnqlZxOX kTSOxfM6sgUsR6Krt7FWD6Dah7WAWpzokQpxm+Kdr17WBJxXcr9HptBApap5CWP2 0dtg3Uz//ParEu2e9PWeU+Dt6oJPGg7FuTke+70w4jzE6IrBlGR2+uJBazH4ibC7 k2uiaTB6OSu00G5K7PBok2C+rFacM+3BA7xuk84zhz/VHBi1g3yVtiaRaZ/lXHNU M0M6Xmtzqe10XRHo22habv4sLa6KnC27gMNQ2U8D9G3JIs1cvOqf1rFY6nmU2V7f 3Lmi9jmxoSFHYHWYF6hgAiufVhEmbOAeA34usH2DKqLJdbVBfI9eTcfxjQZiHxKp 2dAyqGP6fRhlijLpCyDk2yGb8r5oMpOvf234VgVAflPnaeRUvTpX8jAgy6N2WYXc uYuSMsNz762NlwE2iCD88jAgKlOBj4Jl2/E6UDGBocdpETF+yW9MRyNYL+msL1Yx UunYHk2CQhAt2sQBjveJpPUE+lwZUJ7s2ItHalb7oiffGCaIMQkCuzoNgb7iu1JE /Nzl/OjUJVYnavVY5Wb/VqJvUAeWWb4u8a617ip72h/1xyOm9mw7w+VFe38aqjk8 89hH8J+3vzXjAgMBAAGjUzBRMB0GA1UdDgQWBBQoMJjtzt7sWYytC3HeG63r8/Z/ gzAfBgNVHSMEGDAWgBQoMJjtzt7sWYytC3HeG63r8/Z/gzAPBgNVHRMBAf8EBTAD AQH/MA0GCSqGSIb3DQEBDQUAA4ID9QCLpHpEX7LAwTevNucEwnw1N5leRCRleK0B zTdCQiLHNJlZx/I9JnKTymc87FStFXE/JODDWu3Ggo6XXYjDeeKKCMfM9YYaOvAX 2Rmyjzn0wU9dgSIcY7LuepIsfy+Ko9olyT3dTQfqdfl44qiMFwtRhI5Xv6XG7QWa icUScrG1zBfsQtplpHTM67clnCqytVaHSQBxkBl9G/+nTz1DjYz0avOWQ4A0Q7S1 N9duk5pMGW7CcP7UP646MMYLSKO8q8AHRhKCLgq2f4FB1jGu80l3VBc8kMCX0Osb mjF1dFtYcWgzZlAgbPOEhr22d7TdYFfpuyl9Td60VdlzFWKe8mK1w8cu0DxVYML5 4NP+QeL0SduPmzRNgfF+0kHiav7wwg7RXh6aILyt9w9nCubhYhbEyai+MRX//iVg J+DhBHmB7FgWlDx+wzIjDjRJI2BJB9EK5gIM97Cew6cELWiDvKY836vrAh+TZg2G A5GwFxr3kjbYs1G2sOMaSH+Fq8+VpIBr9cQ7v1WDbrsYzStnjYaFx+uqN+SjsPh8 EgT+8+SiaKioS2yOrctovcVXLiB5HJEQdkaTWApZ+KVluJQLF0XFqf1gp2mBJVjy IN2HpRV+JUX/CHGBXE8KeH5aOJPjWNq3k2vJy+3USjH7YvNysLOhekOvRuh5uNxU 1yVWmPWsv5ww0TJZT5L0t+KHTvoSZE2I12PnonDz1t4sGQFXexkB402tkbV2nxJB 6fnks5RMDhXjr5b9/Oca02PPWSuu35ypo36Sh5cBtQ/lRT30cRcW9v1Pkk1N5I+4 FXFEjr5kPantmd2zM5m1z+EfWapVINMv4T9B4bwR8jskM2Vm3a+1Gk1NY5cRxHdS T+EQY+Z40mWxieRNHe4ss5k7uefBq3GXw1FGUQQfHFctV/yKMhjgLXleQcyr82rh t+vsfiU6Z3fIvb8MZzzEfNkLcnUTUkbeHxFYDvEter/uVFVpWyzYg9o8pliuLMf7 ggA5RgA7y6MH5XxcgHEVNx572eHdpFx6q8signasHW8p9fps60rtJGVvF0x98vK1 VDr2CIbKgnqprIc31Xkoata3U7Xevubt6Pm/kyBOs9SnASIHdZvlLQqILOKxjaMf ZD2rJ6rNY+8V14NHXTaEAgefMrF42RXZ4RxGHTNDrxkjieyUDMc9dpnOdCxT50Ai 8NKdtfHT/K0yBNvDQtg4OoHDxy9ikFYyRKJrnVCCA2hfSbuMxiAg1fewrUBPVKy4 T98gvWxCQ9fdT/wjPgn9oMv1lEKtM0JKdM0SshK7EA3PWaQfsOfvd4v00dZaDxRN W4XrAMUHzJ4YICoubSNVl/gXCgqF8kcEAiJY -----END CERTIFICATE----- ongres-scram-f6bd6e2/scram-parent/000077500000000000000000000000001521031247500172065ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-parent/javadoc/000077500000000000000000000000001521031247500206155ustar00rootroot00000000000000ongres-scram-f6bd6e2/scram-parent/javadoc/style.css000066400000000000000000000011751521031247500224730ustar00rootroot00000000000000table { border-collapse: collapse; font-size: 100%; margin: 1em 1em; } caption { font-weight: bold; text-align: left; font-style: italic; padding-bottom: 0.5em; } td { font-family: 'Courier New', Courier, monospace; font-size: small; border: 1px solid #EEEEEE; padding: 0.5em 1em; vertical-align: top; } blockquote { margin-left: 10px; padding: 10px 20px; background-color: #f5f5f5; border-left: 5px solid #ddd; font-style: italic; font-size: 1.1em; } blockquote::before { content: open-quote; color: #ccc; margin-left: -0.5em; } blockquote::after { content: close-quote; color: #ccc; }ongres-scram-f6bd6e2/scram-parent/pom.xml000066400000000000000000000661011521031247500205270ustar00rootroot00000000000000 4.0.0 com.ongres.scram scram-parent 3.3 pom SCRAM - Parent Java Implementation of the Salted Challenge Response Authentication Mechanism (SCRAM) https://github.com/ongres/scram 2017 OnGres, Inc https://www.ongres.com BSD 2-Clause "Simplified" License https://spdx.org/licenses/BSD-2-Clause repo com.ongres.aht Álvaro Hernández Tortosa aht@ongres.com com.ongres.matteom Matteo Melli matteom@ongres.com com.ongres.jorsol Jorge Solórzano jorsol@ongres.com scm:git:https://github.com/ongres/scram.git scm:git:git@github.com:ongres/scram.git 3.3 https://github.com/ongres/scram GitHub https://github.com/ongres/scram/issues UTF-8 UTF-8 8 ${base.java.version} ${base.java.version} ${base.java.version} 17 17 17 2026-06-04T15:00:00Z 26.1.0 6.1.0 2.4 3.15.0 3.5.0 3.4.0 3.12.0 3.5.0 3.5.0 3.5.6 3.5.6 3.6.3 3.1.4 0.10.0 3.10.1 3.2.8 0.8.14 1.7.3 3.2.0 4.0.0 2.9.1 13.5.0 3.6.0 2.49.0 4.9.8 4.9.8.3 1.14.0 7.25.0 3.28.0 3.10 ${rootDirectory}/checks ${checks.location}/checkstyle.xml ${checks.location}/checkstyle-suppressions.xml ${checks.location}/checkstyle-header.txt ${checks.location}/spotbugs-exclude.xml ${checks.location}/pmd-ruleset.xml com.ongres.scram scram-common ${project.version} com.ongres.scram scram-client ${project.version} com.ongres.stringprep saslprep ${saslprep.version} org.junit junit-bom ${junit5.version} pom import org.junit.jupiter junit-jupiter test org.jetbrains annotations ${jetbrains-annotations.version} provided true org.apache.maven.plugins maven-compiler-plugin ${compiler-plugin.version} true true true java9-module compile none 9 ${project.basedir}/src/main/java9 true org.apache.maven.plugins maven-jar-plugin ${jar-plugin.version} true true org.apache.maven.plugins maven-source-plugin ${source-plugin.version} attach-sources jar-no-fork org.apache.maven.plugins maven-javadoc-plugin ${javadoc-plugin.version} en_US true false ${rootDirectory}/scram-parent/javadoc true ${project.groupId}:* true false com.ongres.scram.common.util style.css apiNote a API Note: implSpec a Implementation Requirements: implNote a Implementation Note: attach-javadocs jar org.apache.maven.plugins maven-resources-plugin ${resources-plugin.version} add-license copy-resources generate-resources ${project.build.outputDirectory}/META-INF ${rootDirectory} LICENSE false org.apache.maven.plugins maven-invoker-plugin ${invoker-plugin.version} false ${project.build.directory}/it */pom.xml ${project.build.directory}/local-repo src/it/settings.xml integration-test install integration-test verify org.apache.maven.plugins maven-surefire-plugin ${surefire-plugin.version} org.apache.maven.plugins maven-failsafe-plugin ${failsafe-plugin.version} **/*Test.java **/*IT.java ${project.build.directory}/${project.build.finalName}.jar integration-test verify org.apache.maven.plugins maven-install-plugin ${install-plugin.version} org.sonatype.central central-publishing-maven-plugin ${central-publishing-maven-plugin.version} true central org.apache.maven.plugins maven-clean-plugin ${clean-plugin.version} org.apache.maven.plugins maven-enforcer-plugin ${enforcer-plugin.version} enforce-versions enforce [3.9.9,) [21,) org.jacoco jacoco-maven-plugin ${jacoco-plugin.verson} prepare-agent prepare-agent process-test-classes org.codehaus.mojo flatten-maven-plugin ${flatten-maven-plugin.version} ossrh all flatten flatten process-resources flatten-clean clean clean org.apache.maven.plugins maven-gpg-plugin ${gpg-plugin.version} sign-artifacts sign verify org.cyclonedx cyclonedx-maven-plugin ${cyclonedx-plugin.version} true false false false false makeAggregateBom org.apache.maven.plugins maven-enforcer-plugin checks org.apache.maven.plugins maven-compiler-plugin ${compiler-plugin.version} true true -Xlint:all -XDcompilePolicy=simple -XDaddTypeAnnotationsToSymbol=true --should-stop=ifError=FLOW -Xplugin:ErrorProne -XepAllErrorsAsWarnings -XepDisableWarningsInGeneratedCode -J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED -J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED -J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED com.google.errorprone error_prone_core ${errorprone.version} de.thetaphi forbiddenapis ${forbiddenapis.version} jdk-unsafe jdk-deprecated jdk-internal jdk-non-portable jdk-reflection jdk-system-out ${checks.location}/forbiddenapis.txt check testCheck org.apache.maven.plugins maven-jdeps-plugin ${jdeps-plugin.version} 9 jdkinternals org.apache.maven.plugins maven-checkstyle-plugin ${checkstyle-plugin.version} com.puppycrawl.tools checkstyle ${checkstyle.version} style check verify error true true true false true false com.github.spotbugs spotbugs-maven-plugin ${spotbugs-plugin.version} Max Low true true true com.h3xstream.findsecbugs findsecbugs-plugin ${findsecbugs.version} com.github.spotbugs spotbugs ${spotbugs.version} scan check verify org.apache.maven.plugins maven-pmd-plugin ${pmd-plugin.version} 5 true true false ${pmd.ruleset} net.sourceforge.pmd pmd-core ${pmd.version} net.sourceforge.pmd pmd-java ${pmd.version} pmd-scan check verify com.github.ekryd.sortpom sortpom-maven-plugin ${sortpom-plugin.version} sort verify release org.codehaus.mojo flatten-maven-plugin org.apache.maven.plugins maven-source-plugin org.apache.maven.plugins maven-javadoc-plugin org.apache.maven.plugins maven-gpg-plugin org.cyclonedx cyclonedx-maven-plugin org.sonatype.central central-publishing-maven-plugin compile-java9 [9,) ${base.java.version} org.apache.maven.plugins maven-compiler-plugin java9-module compile