Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6240e8a
[Automated] Update the native jar versions
randilt Mar 17, 2025
f11b98a
Move password hashing methods to Password class removing PasswordArgo…
randilt Mar 17, 2025
d5744d5
Add PBKDF2 configuration and validation methods in PasswordUtils
randilt Mar 17, 2025
719fa07
Implement PBKDF2 hashing and verification methods in the crypto module
randilt Mar 18, 2025
8dd8018
Update PBKDF2 parameters and validation in tests and PasswordUtils
randilt Mar 18, 2025
6a83815
Remove generateSaltPbkdf2 function and related tests from hash module
randilt Mar 18, 2025
37188f0
Add PBKDF2 password hashing and verification support to changelog
randilt Mar 18, 2025
3c667ff
Fix changelog, move the PBKDF2 support change to unreleased
randilt Mar 18, 2025
095e270
Add proposal for PBKDF2-based password hashing APIs in Ballerina cryp…
randilt Mar 18, 2025
af9ef35
Add PBKDF2 password hashing and verification documentation to the spe…
randilt Mar 18, 2025
b0afd37
Add PBKDF2 algorithm implementation to the specification
randilt Mar 18, 2025
be53244
Update ballerina/hash.bal
randilt Mar 19, 2025
49781ca
Enhance PBKDF2 hashing function to have an enum for algorithm param
randilt Apr 3, 2025
fbeaadd
Merge branch 'PBKDF2-support' of https://github.com/randilt/module-ba…
randilt Apr 3, 2025
096e08e
Update PBKDF2 hashing proposal hashPbkdf2 func
randilt Apr 3, 2025
75e1ca2
Add HmacAlgorithm enum for PBKDF2 and update related functions
randilt Apr 3, 2025
92b2dba
Fix algorithm parameter documentation and update default value in has…
randilt Apr 3, 2025
94dbed3
Rename validatePBKDF2Algorithm method to validatePbkdf2Algorithm for …
randilt Apr 3, 2025
176b244
Add minimum memory cost constant for PBKDF2 and update validation logic
randilt Apr 3, 2025
85a6f0f
Fix formatting in error message for minimum memory cost in PBKDF2 val…
randilt Apr 3, 2025
709dbc3
Refactor error messages in Argon2 and Bcrypt tests to use string inte…
randilt Apr 3, 2025
ce4da8e
Add password hashing constants and refactor formatting methods for BC…
randilt Apr 3, 2025
0e29066
Update ballerina/hash.bal
randilt Apr 4, 2025
19b5260
Introduce HmacAlgorithm enum for PBKDF2 hashing API
randilt Apr 4, 2025
1d832b1
Update the spec for Pbkdf2 APIs
randilt Apr 4, 2025
2029369
Remove default parameters for BCrypt, Argon2, and PBKDF2 in PasswordU…
randilt Apr 11, 2025
3a5bac0
Update spec for PBKDF2
randilt Apr 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions ballerina/Ballerina.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
org = "ballerina"
name = "crypto"
version = "2.9.0"
version = "2.9.1"
authors = ["Ballerina"]
keywords = ["security", "hash", "hmac", "sign", "encrypt", "decrypt", "private key", "public key"]
repository = "https://github.com/ballerina-platform/module-ballerina-crypto"
Expand All @@ -15,8 +15,8 @@ graalvmCompatible = true
[[platform.java21.dependency]]
groupId = "io.ballerina.stdlib"
artifactId = "crypto-native"
version = "2.9.0"
path = "../native/build/libs/crypto-native-2.9.0.jar"
version = "2.9.1"
path = "../native/build/libs/crypto-native-2.9.1-SNAPSHOT.jar"

[[platform.java21.dependency]]
groupId = "org.bouncycastle"
Expand Down
2 changes: 1 addition & 1 deletion ballerina/Dependencies.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ distribution-version = "2201.12.0"
[[package]]
org = "ballerina"
name = "crypto"
version = "2.9.0"
version = "2.9.1"
dependencies = [
{org = "ballerina", name = "io"},
{org = "ballerina", name = "jballerina.java"},
Expand Down
41 changes: 39 additions & 2 deletions ballerina/hash.bal
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@

import ballerina/jballerina.java;

# Supported HMAC algorithms for PBKDF2
public enum HmacAlgorithm {
SHA1,
SHA256,
SHA512
}

# Returns the MD5 hash of the given data.
# ```ballerina
# string dataString = "Hello Ballerina";
Expand Down Expand Up @@ -162,7 +169,7 @@ public isolated function verifyBcrypt(string password, string hashedPassword) re
# + return - Argon2id hashed password string or Error if hashing fails
public isolated function hashArgon2(string password, int iterations = 3, int memory = 65536, int parallelism = 4) returns string|Error = @java:Method {
name: "hashPasswordArgon2",
'class: "io.ballerina.stdlib.crypto.nativeimpl.PasswordArgon2"
'class: "io.ballerina.stdlib.crypto.nativeimpl.Password"
} external;

# Verifies if a password matches an Argon2id hashed password.
Expand All @@ -177,5 +184,35 @@ public isolated function hashArgon2(string password, int iterations = 3, int mem
# + return - Boolean indicating if password matches or Error if verification fails
public isolated function verifyArgon2(string password, string hashedPassword) returns boolean|Error = @java:Method {
name: "verifyPasswordArgon2",
'class: "io.ballerina.stdlib.crypto.nativeimpl.PasswordArgon2"
'class: "io.ballerina.stdlib.crypto.nativeimpl.Password"
} external;

# Returns a PBKDF2 hash of the given password with optional parameters.
# ```ballerina
# string password = "mySecurePassword123";
# string|crypto:Error hash = crypto:hashPbkdf2(password);
# ```
#
# + password - Password string to be hashed
# + iterations - Optional number of iterations. Default is 10000
# + algorithm - Optional HMAC algorithm (`SHA1`, `SHA256`, `SHA512`). Default is SHA256
# + return - PBKDF2 hashed password string or Error if hashing fails
public isolated function hashPbkdf2(string password, int iterations = 10000, HmacAlgorithm algorithm = SHA256) returns string|Error = @java:Method {
name: "hashPasswordPBKDF2",
'class: "io.ballerina.stdlib.crypto.nativeimpl.Password"
} external;

# Verifies if a password matches a PBKDF2 hashed password.
# ```ballerina
# string password = "mySecurePassword123";
# string hashedPassword = "$pbkdf2-sha256$i=10000$salt$hash";
# boolean|crypto:Error matches = crypto:verifyPbkdf2(password, hashedPassword);
# ```
#
# + password - Password string to verify
# + hashedPassword - PBKDF2 hashed password to verify against
# + return - Boolean indicating if password matches or Error if verification fails
public isolated function verifyPbkdf2(string password, string hashedPassword) returns boolean|Error = @java:Method {
name: "verifyPasswordPBKDF2",
'class: "io.ballerina.stdlib.crypto.nativeimpl.Password"
} external;
167 changes: 150 additions & 17 deletions ballerina/tests/hash_test.bal
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ type InvalidHash record {|
string expectedError;
|};

type InvalidPbkdf2Params record {|
int iterations;
HmacAlgorithm algorithm;
string expectedError;
|};

@test:Config {}
isolated function testHashCrc32() {
Expand Down Expand Up @@ -134,7 +139,6 @@ isolated function testHashSha512WithSalt() {
test:assertEquals(hashSha512(input, salt).toBase16(), expectedSha512Hash);
}


@test:Config {}
isolated function testHashKeccak256() {
byte[] input = "Ballerina test".toBytes();
Expand Down Expand Up @@ -167,7 +171,7 @@ isolated function testHashPasswordArgon2ComplexPasswords(ComplexPassword data) r
string hash = check hashArgon2(data.password);
test:assertTrue(hash.startsWith("$argon2id$v=19$"));
boolean result = check verifyArgon2(data.password, hash);
test:assertTrue(result, "Password verification failed for: " + data.password);
test:assertTrue(result, string `Password verification failed for: ${data.password}`);
}

@test:Config {
Expand All @@ -188,7 +192,7 @@ isolated function testHashPasswordArgon2InvalidParams(InvalidArgon2Params data)
isolated function testVerifyPasswordArgon2Success(ValidPassword data) returns error? {
string hash = check hashArgon2(data.password);
boolean result = check verifyArgon2(data.password, hash);
test:assertTrue(result, "Password verification failed for: " + data.password);
test:assertTrue(result, string `Password verification failed for: ${data.password}`);
}

@test:Config {
Expand All @@ -197,7 +201,7 @@ isolated function testVerifyPasswordArgon2Success(ValidPassword data) returns er
isolated function testVerifyPasswordArgon2Failure(PasswordPair data) returns error? {
string hash = check hashArgon2(data.correctPassword);
boolean result = check verifyArgon2(data.wrongPassword, hash);
test:assertFalse(result, "Should fail for wrong password: " + data.wrongPassword);
test:assertFalse(result, string `Should fail for wrong password: ${data.wrongPassword}`);
}

@test:Config {
Expand All @@ -207,7 +211,7 @@ isolated function testVerifyPasswordArgon2InvalidHashFormat(InvalidHash data) {
string password = "Ballerina@123";
boolean|Error result = verifyArgon2(password, data.hash);
if result !is Error {
test:assertFail("Should fail with invalid hash: " + data.hash);
test:assertFail(string `Should fail with invalid hash: ${data.hash}`);
}
test:assertTrue(result.message().startsWith("Invalid Argon2 hash format"));
}
Expand All @@ -220,16 +224,16 @@ isolated function testArgon2PasswordHashUniqueness(ValidPassword data) returns e
string hash2 = check hashArgon2(data.password);
string hash3 = check hashArgon2(data.password);

test:assertNotEquals(hash1, hash2, "Hashes should be unique for: " + data.password);
test:assertNotEquals(hash2, hash3, "Hashes should be unique for: " + data.password);
test:assertNotEquals(hash1, hash3, "Hashes should be unique for: " + data.password);
test:assertNotEquals(hash1, hash2, string `Hashes should be unique for: ${data.password}`);
test:assertNotEquals(hash2, hash3, string `Hashes should be unique for: ${data.password}`);
test:assertNotEquals(hash1, hash3, string `Hashes should be unique for: ${data.password}`);

boolean verify1 = check verifyArgon2(data.password, hash1);
boolean verify2 = check verifyArgon2(data.password, hash2);
boolean verify3 = check verifyArgon2(data.password, hash3);

test:assertTrue(verify1 && verify2 && verify3,
"All hashes should verify successfully for: " + data.password);
string `All hashes should verify successfully for: ${data.password}`);
}

// tests for Bcrypt
Expand Down Expand Up @@ -258,7 +262,7 @@ isolated function testHashPasswordBcryptComplexPasswords(ComplexPassword data) r
test:assertTrue(hash.length() > 50);

boolean result = check verifyBcrypt(data.password, hash);
test:assertTrue(result, "Password verification failed for: " + data.password);
test:assertTrue(result, string `Password verification failed for: ${data.password}`);
}

@test:Config {
Expand All @@ -279,7 +283,7 @@ isolated function testHashPasswordBcryptInvalidWorkFactor(InvalidWorkFactor data
isolated function testVerifyPasswordBcryptSuccess(ValidPassword data) returns error? {
string hash = check hashBcrypt(data.password);
boolean result = check verifyBcrypt(data.password, hash);
test:assertTrue(result, "Password verification failed for: " + data.password);
test:assertTrue(result, string `Password verification failed for: ${data.password}`);
}

@test:Config {
Expand All @@ -288,7 +292,7 @@ isolated function testVerifyPasswordBcryptSuccess(ValidPassword data) returns er
isolated function testVerifyPasswordBcryptFailure(PasswordPair data) returns error? {
string hash = check hashBcrypt(data.correctPassword);
boolean result = check verifyBcrypt(data.wrongPassword, hash);
test:assertFalse(result, "Should fail for wrong password: " + data.wrongPassword);
test:assertFalse(result, string `Should fail for wrong password: ${data.wrongPassword}`);
}

@test:Config {
Expand All @@ -298,7 +302,7 @@ isolated function testVerifyPasswordBcryptInvalidHashFormat(InvalidHash data) {
string password = "Ballerina@123";
boolean|Error result = verifyBcrypt(password, data.hash);
if result !is Error {
test:assertFail("Should fail with invalid hash: " + data.hash);
test:assertFail(string `Should fail with invalid hash: ${data.hash}`);
}
test:assertEquals(result.message(), data.expectedError);
}
Expand All @@ -311,16 +315,16 @@ isolated function testBcryptPasswordHashUniqueness(ValidPassword data) returns e
string hash2 = check hashBcrypt(data.password);
string hash3 = check hashBcrypt(data.password);

test:assertNotEquals(hash1, hash2, "Hashes should be unique for: " + data.password);
test:assertNotEquals(hash2, hash3, "Hashes should be unique for: " + data.password);
test:assertNotEquals(hash1, hash3, "Hashes should be unique for: " + data.password);
test:assertNotEquals(hash1, hash2, string `Hashes should be unique for: ${data.password}`);
test:assertNotEquals(hash2, hash3, string `Hashes should be unique for: ${data.password}`);
test:assertNotEquals(hash1, hash3, string `Hashes should be unique for: ${data.password}`);

boolean verify1 = check verifyBcrypt(data.password, hash1);
boolean verify2 = check verifyBcrypt(data.password, hash2);
boolean verify3 = check verifyBcrypt(data.password, hash3);

test:assertTrue(verify1 && verify2 && verify3,
"All hashes should verify successfully for: " + data.password);
string `All hashes should verify successfully for: ${data.password}`);
}

// common tests for both algorithms
Expand All @@ -336,6 +340,109 @@ isolated function testEmptyPasswordError(string algorithm) returns error? {
test:assertEquals(hash.message(), "Password cannot be empty");
}

// tests for PBKDF2
@test:Config {}
isolated function testHashPasswordPbkdf2Default() returns error? {
string password = "Ballerina@123";
string hash = check hashPbkdf2(password);
test:assertTrue(hash.startsWith("$pbkdf2-sha256$i=10000$"));
test:assertTrue(hash.length() > 50);
}

@test:Config {}
isolated function testHashPasswordPbkdf2Custom() returns error? {
string password = "Ballerina@123";
string hash = check hashPbkdf2(password, 15000, SHA512);
test:assertTrue(hash.startsWith(string `$pbkdf2-sha512$i=15000$`));
test:assertTrue(hash.length() > 50);
}

@test:Config {
dataProvider: complexPasswordsDataProvider
}
isolated function testHashPasswordPbkdf2ComplexPasswords(ComplexPassword data) returns error? {
string hash = check hashPbkdf2(data.password);
test:assertTrue(hash.startsWith("$pbkdf2-sha256$i=10000$"));
boolean result = check verifyPbkdf2(data.password, hash);
test:assertTrue(result, string `Password verification failed for: ${data.password}`);
}

@test:Config {
dataProvider: invalidPbkdf2ParamsDataProvider
}
isolated function testHashPasswordPbkdf2InvalidParams(InvalidPbkdf2Params data) {
string password = "Ballerina@123";
string|Error hash = hashPbkdf2(password, data.iterations, data.algorithm);
if hash !is Error {
test:assertFail(string `Should fail with invalid parameters: iterations=${data.iterations}, algorithm=${data.algorithm}`);
}
test:assertEquals(hash.message(), data.expectedError);
}

@test:Config {
dataProvider: validPasswordsDataProvider
}
isolated function testVerifyPasswordPbkdf2Success(ValidPassword data) returns error? {
string hash = check hashPbkdf2(data.password);
boolean result = check verifyPbkdf2(data.password, hash);
test:assertTrue(result, string `Password verification failed for: ${data.password}`);
}

@test:Config {
dataProvider: wrongPasswordsDataProvider
}
isolated function testVerifyPasswordPbkdf2Failure(PasswordPair data) returns error? {
string hash = check hashPbkdf2(data.correctPassword);
boolean result = check verifyPbkdf2(data.wrongPassword, hash);
test:assertFalse(result, string `Should fail for wrong password: ${data.wrongPassword}`);
}

@test:Config {
dataProvider: invalidPbkdf2HashesDataProvider
}
isolated function testVerifyPasswordPbkdf2InvalidHashFormat(InvalidHash data) {
string password = "Ballerina@123";
boolean|Error result = verifyPbkdf2(password, data.hash);
if result !is Error {
test:assertFail(string `Should fail with invalid hash: ${data.hash}`);
}
test:assertTrue(result.message().startsWith(data.expectedError));
}

@test:Config {
dataProvider: uniquenessPasswordsDataProvider
}
isolated function testPbkdf2PasswordHashUniqueness(ValidPassword data) returns error? {
string hash1 = check hashPbkdf2(data.password);
string hash2 = check hashPbkdf2(data.password);
string hash3 = check hashPbkdf2(data.password);

test:assertNotEquals(hash1, hash2, string `Hashes should be unique for: ${data.password}`);
test:assertNotEquals(hash2, hash3, string `Hashes should be unique for: ${data.password}`);
test:assertNotEquals(hash1, hash3, string `Hashes should be unique for: ${data.password}`);

boolean verify1 = check verifyPbkdf2(data.password, hash1);
boolean verify2 = check verifyPbkdf2(data.password, hash2);
boolean verify3 = check verifyPbkdf2(data.password, hash3);

test:assertTrue(verify1 && verify2 && verify3,
string `All hashes should verify successfully for: ${data.password}`);
}

@test:Config {
dataProvider: pbkdf2AlgorithmsDataProvider
}
isolated function testPbkdf2DifferentAlgorithms(HmacAlgorithm algorithm) returns error? {
string password = "Ballerina@123";
string hash = check hashPbkdf2(password, 10000, algorithm);

test:assertTrue(hash.startsWith(string `$pbkdf2-${algorithm.toString().toLowerAscii()}$`),
string `Hash should start with correct algorithm identifier: ${algorithm.toString()}`);

boolean result = check verifyPbkdf2(password, hash);
test:assertTrue(result, string `Password verification failed for algorithm: ${algorithm.toString()}`);
}

// data Providers for password tests
isolated function complexPasswordsDataProvider() returns ComplexPassword[][] {
return [
Expand Down Expand Up @@ -429,3 +536,29 @@ isolated function hashingAlgorithmsDataProvider() returns string[][] {
["bcrypt"]
];
}

// Additional data providers for PBKDF2 tests
isolated function invalidPbkdf2ParamsDataProvider() returns InvalidPbkdf2Params[][] {
return [
[{iterations: 0, algorithm: SHA256, expectedError: "Iterations must be at least 10000"}],
[{iterations: 500, algorithm: SHA256, expectedError: "Iterations must be at least 10000"}],
[{iterations: -100, algorithm: SHA256, expectedError: "Iterations must be at least 10000"}]
];
}

isolated function invalidPbkdf2HashesDataProvider() returns InvalidHash[][] {
return [
[{hash: "invalid_hash_format", expectedError: "Invalid PBKDF2 hash format"}],
[{hash: "$pbkdf2-sha256$invalid", expectedError: "Invalid PBKDF2 hash format"}],
[{hash: "$pbkdf2$i=10000$salt$hash", expectedError: "Invalid PBKDF2 hash format"}],
[{hash: "$pbkdf2-md5$i=10000$salt$hash", expectedError: "Error occurred while verifying password: Unsupported algorithm: MD5"}]
];
}

isolated function pbkdf2AlgorithmsDataProvider() returns HmacAlgorithm[][] {
return [
[SHA1],
[SHA256],
[SHA512]
];
}
3 changes: 3 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Added
- [Introduce support for PBKDF2 password hashing and verification](https://github.com/ballerina-platform/ballerina-lang/issues/43926)

### Changed
- [Update OIDS of NIST approved post quantum algorithms](https://github.com/ballerina-platform/ballerina-library/issues/7678)

Expand Down
Loading
Loading