diff --git a/.github/workflows/github-packages-publish.yml b/.github/workflows/github-packages-publish.yml index 28c409e..c083358 100644 --- a/.github/workflows/github-packages-publish.yml +++ b/.github/workflows/github-packages-publish.yml @@ -1,28 +1,100 @@ -# 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. -# This workflow will build a package using Gradle and then publish it to GitHub packages when a release is created -# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#Publishing-using-gradle +# This workflow publishes one or more modules to Maven Central when a version tag is pushed +# to the main branch. +# +# Supported tag formats: +# /v — publish a single module (e.g. tuple/v3.3.1) +# +/v — publish multiple modules (e.g. tuple+crypto-toolbox/v3.3.1) +# v — publish all modules (e.g. v3.4.0) +# +# Valid module names: common-toolbox, tuple, identity-generator, crypto-toolbox, math-toolbox, version-catalogue -name: Publish Packages to GitHub Packages with Gradle +name: Publish Packages to Maven Central on: - release: - types: - - published + push: + tags: + - 'v[0-9]*.[0-9]*.[0-9]*' + - '*/v[0-9]*.[0-9]*.[0-9]*' jobs: - build: + publish: name: Build and Publish runs-on: ubuntu-latest permissions: contents: read - packages: write steps: - name: Checkout uses: actions/checkout@v4.2.2 + with: + fetch-depth: 0 + + - name: Verify Tag is on Main Branch + run: | + if ! git merge-base --is-ancestor HEAD origin/main; then + echo "::error::Tag ${{ github.ref_name }} does not point to a commit on the main branch" + echo "Tags must be pushed after the commit is merged to main." + exit 1 + fi + echo "✓ Tag ${{ github.ref_name }} is on main" + + - name: Parse Tag + id: parse-tag + run: | + declare -A MODULE_PROPS=( + ["common-toolbox"]="commonToolboxVersion" + ["tuple"]="tupleVersion" + ["identity-generator"]="identityGeneratorVersion" + ["crypto-toolbox"]="cryptoToolboxVersion" + ["math-toolbox"]="mathToolboxVersion" + ["version-catalogue"]="versionCatalogueVersion" + ) + + TAG="${{ github.ref_name }}" + echo "Tag: ${TAG}" + + # [+...]/v — one or more specific modules + if [[ "${TAG}" =~ ^([a-z][a-z0-9-]+(\+[a-z][a-z0-9-]+)*)/v?([0-9]+\.[0-9]+\.[0-9]+.*)$ ]]; then + IFS='+' read -ra MODULES <<< "${BASH_REMATCH[1]}" + VERSION="${BASH_REMATCH[3]}" + + # v — all modules + else + MODULES=("common-toolbox" "tuple" "identity-generator" "crypto-toolbox" "math-toolbox" "version-catalogue") + VERSION="${TAG#v}" + fi + + # Validate all modules + for m in "${MODULES[@]}"; do + if [ -z "${MODULE_PROPS[$m]}" ]; then + echo "::error::Unknown module: ${m}" + echo "Valid modules: ${!MODULE_PROPS[*]}" + exit 1 + fi + done + + echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "count=${#MODULES[@]}" >> $GITHUB_OUTPUT + + for m in "${MODULES[@]}"; do + echo "→ ${m} @ ${VERSION}" + done + + # Store module list as a multi-line output + { + echo "modules<> $GITHUB_OUTPUT + + # Store property mappings + { + echo "props<> $GITHUB_OUTPUT - name: Setup GPG TTY run: export GPG_TTY=$(tty) @@ -61,19 +133,48 @@ jobs: run: chmod +x ./gradlew - name: Build with Gradle - # Overwrite artefactVersion with tag name - run: ./gradlew build -PartefactVersion=${{ github.event.release.tag_name }} + env: + MODULES: ${{ steps.parse-tag.outputs.modules }} + PROPS: ${{ steps.parse-tag.outputs.props }} + VERSION: ${{ steps.parse-tag.outputs.version }} + run: | + declare -A MODULE_PROPS + while IFS='=' read -r key value; do + MODULE_PROPS[$key]="$value" + done <<< "$PROPS" + + while IFS= read -r MODULE; do + echo "::group::Building ${MODULE}" + PROP="${MODULE_PROPS[$MODULE]}" + ./gradlew ":${MODULE}:build" "-P${PROP}=${VERSION}" + echo "::endgroup::" + done <<< "$MODULES" - name: List Output Items run: ls -l ./**/build/libs - name: Publish to Maven Central - run: ./gradlew publish + env: + MODULES: ${{ steps.parse-tag.outputs.modules }} + PROPS: ${{ steps.parse-tag.outputs.props }} + VERSION: ${{ steps.parse-tag.outputs.version }} + run: | + declare -A MODULE_PROPS + while IFS='=' read -r key value; do + MODULE_PROPS[$key]="$value" + done <<< "$PROPS" + + while IFS= read -r MODULE; do + echo "::group::Publishing ${MODULE}" + PROP="${MODULE_PROPS[$MODULE]}" + ./gradlew ":${MODULE}:publish" "-P${PROP}=${VERSION}" + echo "::endgroup::" + done <<< "$MODULES" - name: Create Deployment on Central Publisher Portal run: | curl --fail -X 'POST' \ - 'https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/com.onixbyte?publishing_type=user_managed' \ - -H 'accept: */*' \ - -H 'Authorization: Bearer ${{ secrets.MAVEN_PORTAL_TOKEN }}' \ - -d '' + 'https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/com.onixbyte?publishing_type=user_managed' \ + -H 'accept: */*' \ + -H 'Authorization: Bearer ${{ secrets.MAVEN_PORTAL_TOKEN }}' \ + -d '' diff --git a/build.gradle.kts b/build.gradle.kts index a60c427..6449daa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,9 +20,6 @@ * SOFTWARE. */ -val artefactVersion: String by project - subprojects { group = "com.onixbyte" - version = artefactVersion } diff --git a/common-toolbox/build.gradle.kts b/common-toolbox/build.gradle.kts index 7866663..542f580 100644 --- a/common-toolbox/build.gradle.kts +++ b/common-toolbox/build.gradle.kts @@ -29,7 +29,9 @@ plugins { id("signing") } -val artefactVersion: String by project +val commonToolboxVersion: String by project + +version = commonToolboxVersion val projectUrl: String by project val projectGithubUrl: String by project val licenseName: String by project @@ -70,7 +72,7 @@ publishing { create("commonToolbox") { groupId = group.toString() artifactId = "common-toolbox" - version = artefactVersion + version = commonToolboxVersion pom { name = "OnixByte Common Toolbox" diff --git a/common-toolbox/src/test/java/com/onixbyte/common/util/HashUtilTest.java b/common-toolbox/src/test/java/com/onixbyte/common/util/HashUtilTest.java deleted file mode 100644 index 9bb1fef..0000000 --- a/common-toolbox/src/test/java/com/onixbyte/common/util/HashUtilTest.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (C) 2024-2025 OnixByte. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.onixbyte.common.util; - -import org.junit.jupiter.api.Test; - -import java.nio.charset.StandardCharsets; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -class HashUtilTest { - - // Test MD2 hashing with explicit charset and default charset - @Test - void testMd2() { - String input = "test"; - // Known MD2 hash of "test" with UTF-8 - String expectedHash = "dd34716876364a02d0195e2fb9ae2d1b"; - assertEquals(expectedHash, HashUtil.md2(input, StandardCharsets.UTF_8)); - assertEquals(expectedHash, HashUtil.md2(input)); - // Test null charset fallback to UTF-8 - assertEquals(expectedHash, HashUtil.md2(input, null)); - } - - // Test MD5 hashing with explicit charset and default charset - @Test - void testMd5() { - String input = "test"; - // Known MD5 hash of "test" - String expectedHash = "098f6bcd4621d373cade4e832627b4f6"; - assertEquals(expectedHash, HashUtil.md5(input, StandardCharsets.UTF_8)); - assertEquals(expectedHash, HashUtil.md5(input)); - assertEquals(expectedHash, HashUtil.md5(input, null)); - } - - // Test SHA-1 hashing with explicit charset and default charset - @Test - void testSha1() { - String input = "test"; - // Known SHA-1 hash of "test" - String expectedHash = "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"; - assertEquals(expectedHash, HashUtil.sha1(input, StandardCharsets.UTF_8)); - assertEquals(expectedHash, HashUtil.sha1(input)); - assertEquals(expectedHash, HashUtil.sha1(input, null)); - } - - // Test SHA-224 hashing with explicit charset and default charset - @Test - void testSha224() { - String input = "test"; - // Known SHA-224 hash of "test" - String expectedHash = "90a3ed9e32b2aaf4c61c410eb925426119e1a9dc53d4286ade99a809"; - assertEquals(expectedHash, HashUtil.sha224(input, StandardCharsets.UTF_8)); - assertEquals(expectedHash, HashUtil.sha224(input)); - assertEquals(expectedHash, HashUtil.sha224(input, null)); - } - - // Test SHA-256 hashing with explicit charset and default charset - @Test - void testSha256() { - String input = "test"; - // Known SHA-256 hash of "test" - String expectedHash = "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"; - assertEquals(expectedHash, HashUtil.sha256(input, StandardCharsets.UTF_8)); - assertEquals(expectedHash, HashUtil.sha256(input)); - assertEquals(expectedHash, HashUtil.sha256(input, null)); - } - - // Test SHA-384 hashing with explicit charset and default charset - @Test - void testSha384() { - String input = "test"; - // Known SHA-384 hash of "test" - String expectedHash = "768412320f7b0aa5812fce428dc4706b3cae50e02a64caa16a782249bfe8efc4b7ef1ccb126255d196047dfedf17a0a9"; - assertEquals(expectedHash, HashUtil.sha384(input, StandardCharsets.UTF_8)); - assertEquals(expectedHash, HashUtil.sha384(input)); - assertEquals(expectedHash, HashUtil.sha384(input, null)); - } - - // Test SHA-512 hashing with explicit charset and default charset - @Test - void testSha512() { - String input = "test"; - // Known SHA-512 hash of "test" - String expectedHash = "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff"; - // remove all whitespace in expected to match format generated - expectedHash = expectedHash.replaceAll("\\s+", ""); - assertEquals(expectedHash, HashUtil.sha512(input, StandardCharsets.UTF_8)); - assertEquals(expectedHash, HashUtil.sha512(input)); - assertEquals(expectedHash, HashUtil.sha512(input, null)); - } - - // Test empty string input - @Test - void testEmptyString() { - String input = ""; - // MD5 hash of empty string - String expectedMd5 = "d41d8cd98f00b204e9800998ecf8427e"; - assertEquals(expectedMd5, HashUtil.md5(input)); - // SHA-256 hash of empty string - String expectedSha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; - assertEquals(expectedSha256, HashUtil.sha256(input)); - } - - // Test null charset fallback for one algorithm as a sample - @Test - void testNullCharsetFallsBackToUtf8() { - String input = "abc"; - String hashWithNull = HashUtil.md5(input, null); - String hashWithUtf8 = HashUtil.md5(input, StandardCharsets.UTF_8); - assertEquals(hashWithUtf8, hashWithNull); - } -} diff --git a/crypto-toolbox/build.gradle.kts b/crypto-toolbox/build.gradle.kts index 1be5763..ed4b40b 100644 --- a/crypto-toolbox/build.gradle.kts +++ b/crypto-toolbox/build.gradle.kts @@ -29,7 +29,9 @@ plugins { id("signing") } -val artefactVersion: String by project +val cryptoToolboxVersion: String by project + +version = cryptoToolboxVersion val projectUrl: String by project val projectGithubUrl: String by project val licenseName: String by project @@ -73,7 +75,7 @@ publishing { create("cryptoToolbox") { groupId = group.toString() artifactId = "crypto-toolbox" - version = artefactVersion + version = cryptoToolboxVersion pom { name = "OnixByte Crypto Toolbox" diff --git a/crypto-toolbox/src/main/java/com/onixbyte/crypto/util/CryptoUtil.java b/crypto-toolbox/src/main/java/com/onixbyte/crypto/util/CryptoUtil.java index 8e28961..cf42471 100644 --- a/crypto-toolbox/src/main/java/com/onixbyte/crypto/util/CryptoUtil.java +++ b/crypto-toolbox/src/main/java/com/onixbyte/crypto/util/CryptoUtil.java @@ -22,6 +22,12 @@ package com.onixbyte.crypto.util; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + /** * Utility class for cryptographic operations. * @@ -57,6 +63,36 @@ public final class CryptoUtil { return pemKeyText .replaceAll("-----BEGIN ((EC )|(RSA ))?(PRIVATE|PUBLIC) KEY-----", "") .replaceAll("-----END ((EC )|(RSA ))?(PRIVATE|PUBLIC) KEY-----", "") - .replaceAll("\n", ""); + .replace("\n", ""); + } + + /** + * Computes a Hash-based Message Authentication Code (HMAC) using the SHA-256 algorithm. + *

+ * The input payload and secret key are both processed using the {@code UTF-8} charset + * to guarantee consistent results across different operating system environments. + * The final byte array output is converted into a lower-case hexadecimal string. + * + * @param payload the raw string data or message content to be authenticated + * @param secret the secret key used to sign the payload + * @return a lower-case hexadecimal string representing the computed HMAC-SHA256 signature + * @throws NoSuchAlgorithmException if the HmacSHA256 algorithm is not available in the environment + * @throws InvalidKeyException if the provided secret key is inappropriate for + * initialising the MAC + */ + public static String hmacSha256( + String payload, + String secret + ) throws NoSuchAlgorithmException, InvalidKeyException { + var secretKeySpec = new SecretKeySpec( + secret.getBytes(StandardCharsets.UTF_8), + "HmacSHA256" + ); + + var mac = Mac.getInstance("HmacSHA256"); + mac.init(secretKeySpec); + + var rawHmac = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)); + return EncodingUtil.bytesToHex(rawHmac); } } diff --git a/crypto-toolbox/src/main/java/com/onixbyte/crypto/util/EncodingUtil.java b/crypto-toolbox/src/main/java/com/onixbyte/crypto/util/EncodingUtil.java new file mode 100644 index 0000000..b105409 --- /dev/null +++ b/crypto-toolbox/src/main/java/com/onixbyte/crypto/util/EncodingUtil.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024-2026 OnixByte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.onixbyte.crypto.util; + +import java.util.HexFormat; + +/** + * Utility class for handling various data encoding and formatting operations. + *

+ * This class provides helper methods to convert raw binary data into standardised + * string representations (such as hexadecimal format) commonly required for + * cryptographic verification, logging, and data transmission. + *

+ * This utility class is stateless and thread-safe. + * + * @author siujamo + */ +public class EncodingUtil { + + /** + * Converts an array of bytes into its corresponding hexadecimal string representation. + *

+ * Each byte is converted to a two-digit hex string. If the resulting hex value + * is a single digit (i.e., less than 16), a leading '0' is automatically prepended + * to ensure a consistent and uniform format throughout the output string. + * + * @param bytes the byte array to be converted, typically the raw output of a cryptographic hash + * @return a lower-case hexadecimal string representing the input bytes + */ + public static String bytesToHex(byte[] bytes) { + return HexFormat.of().formatHex(bytes); + } +} diff --git a/common-toolbox/src/main/java/com/onixbyte/common/util/HashUtil.java b/crypto-toolbox/src/main/java/com/onixbyte/crypto/util/HashUtil.java similarity index 99% rename from common-toolbox/src/main/java/com/onixbyte/common/util/HashUtil.java rename to crypto-toolbox/src/main/java/com/onixbyte/crypto/util/HashUtil.java index 5d5aa96..e1edfc0 100644 --- a/common-toolbox/src/main/java/com/onixbyte/common/util/HashUtil.java +++ b/crypto-toolbox/src/main/java/com/onixbyte/crypto/util/HashUtil.java @@ -20,7 +20,7 @@ * SOFTWARE. */ -package com.onixbyte.common.util; +package com.onixbyte.crypto.util; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -66,7 +66,7 @@ import java.util.Optional; * * @author zihluwang * @version 3.0.0 - * @see java.security.MessageDigest + * @see MessageDigest */ public final class HashUtil { diff --git a/gradle.properties b/gradle.properties index 6dcdc54..5209411 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,6 +21,12 @@ # artefactVersion=3.3.0 +commonToolboxVersion=3.3.0 +tupleVersion=3.3.0 +identityGeneratorVersion=3.3.0 +cryptoToolboxVersion=3.3.0 +mathToolboxVersion=3.3.0 +versionCatalogueVersion=3.3.0 projectUrl=https://onixbyte.com/projects/onixbyte-toolbox projectGithubUrl=https://github.com/onixbyte/onixbyte-toolbox licenseName=MIT diff --git a/identity-generator/build.gradle.kts b/identity-generator/build.gradle.kts index c53908d..d076fc5 100644 --- a/identity-generator/build.gradle.kts +++ b/identity-generator/build.gradle.kts @@ -29,7 +29,9 @@ plugins { id("signing") } -val artefactVersion: String by project +val identityGeneratorVersion: String by project + +version = identityGeneratorVersion val projectUrl: String by project val projectGithubUrl: String by project val licenseName: String by project @@ -70,7 +72,7 @@ publishing { create("identityGenerator") { groupId = group.toString() artifactId = "identity-generator" - version = artefactVersion + version = identityGeneratorVersion pom { name = "OnixByte Identity Generator" diff --git a/math-toolbox/build.gradle.kts b/math-toolbox/build.gradle.kts index db75ca6..3a44116 100644 --- a/math-toolbox/build.gradle.kts +++ b/math-toolbox/build.gradle.kts @@ -29,7 +29,9 @@ plugins { id("signing") } -val artefactVersion: String by project +val mathToolboxVersion: String by project + +version = mathToolboxVersion val projectUrl: String by project val projectGithubUrl: String by project val licenseName: String by project @@ -70,7 +72,7 @@ publishing { create("mathToolbox") { groupId = group.toString() artifactId = "math-toolbox" - version = artefactVersion + version = mathToolboxVersion pom { name = "OnixByte Math Toolbox" diff --git a/tuple/build.gradle.kts b/tuple/build.gradle.kts index 5f01dcd..870bd1a 100644 --- a/tuple/build.gradle.kts +++ b/tuple/build.gradle.kts @@ -29,7 +29,9 @@ plugins { id("signing") } -val artefactVersion: String by project +val tupleVersion: String by project + +version = tupleVersion val projectUrl: String by project val projectGithubUrl: String by project val licenseName: String by project @@ -70,7 +72,7 @@ publishing { create("tuple") { groupId = group.toString() artifactId = "tuple" - version = artefactVersion + version = tupleVersion pom { name = "OnixByte Tuple" diff --git a/version-catalogue/build.gradle.kts b/version-catalogue/build.gradle.kts index c74df5e..90f945b 100644 --- a/version-catalogue/build.gradle.kts +++ b/version-catalogue/build.gradle.kts @@ -28,7 +28,14 @@ plugins { id("signing") } -val artefactVersion: String by project +val commonToolboxVersion: String by project +val identityGeneratorVersion: String by project +val cryptoToolboxVersion: String by project +val mathToolboxVersion: String by project +val tupleVersion: String by project +val versionCatalogueVersion: String by project + +version = versionCatalogueVersion val projectUrl: String by project val projectGithubUrl: String by project val licenseName: String by project @@ -40,11 +47,11 @@ repositories { dependencies { constraints { - api("com.onixbyte:common-toolbox:$artefactVersion") - api("com.onixbyte:identity-generator:$artefactVersion") - api("com.onixbyte:crypto-toolbox:$artefactVersion") - api("com.onixbyte:math-toolbox:$artefactVersion") - api("com.onixbyte:tuple:$artefactVersion") + api("com.onixbyte:common-toolbox:$commonToolboxVersion") + api("com.onixbyte:identity-generator:$identityGeneratorVersion") + api("com.onixbyte:crypto-toolbox:$cryptoToolboxVersion") + api("com.onixbyte:math-toolbox:$mathToolboxVersion") + api("com.onixbyte:tuple:$tupleVersion") } } @@ -53,7 +60,7 @@ publishing { create("versionCatalogue") { groupId = group.toString() artifactId = "version-catalogue" - version = artefactVersion + version = versionCatalogueVersion pom { name = "OnixByte Version Catalogue"