Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
34 changes: 32 additions & 2 deletions ballerina/hash.bal
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,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 +177,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, string 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;
137 changes: 137 additions & 0 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;
string algorithm;
string expectedError;
|};

@test:Config {}
isolated function testHashCrc32() {
Expand Down Expand Up @@ -336,6 +341,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.includes("$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, "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, "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, "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("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, "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);

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,
"All hashes should verify successfully for: " + data.password);
}

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

test:assertTrue(hash.startsWith("$pbkdf2-" + algorithm.toLowerAscii() + "$"),
"Hash should start with correct algorithm identifier");

boolean result = check verifyPbkdf2(password, hash);
test:assertTrue(result, "Password verification failed for algorithm: " + algorithm);
}

// data Providers for password tests
isolated function complexPasswordsDataProvider() returns ComplexPassword[][] {
return [
Expand Down Expand Up @@ -429,3 +537,32 @@ 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: 10000, algorithm: "MD5", expectedError: "Unsupported algorithm. Must be one of: SHA1, SHA256, SHA512"}],
[{iterations: 10000, algorithm: "invalid-alg", expectedError: "Unsupported algorithm. Must be one of: SHA1, SHA256, SHA512"}],
[{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 string[][] {
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
47 changes: 47 additions & 0 deletions docs/proposals/pbkdf2-hashing-apis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Proposal: Introduce PBKDF2-Based Password Hashing to Ballerina Crypto Module

_Authors_: @randilt
_Reviewers_:
_Created_: 2025/03/18
_Updated_: 2025/03/18
_Issues_: [#43926](https://github.com/ballerina-platform/ballerina-lang/issues/43926)

## Summary
The Ballerina crypto module currently lacks built-in support for PBKDF2 password hashing, a widely used key derivation function for secure password storage. This proposal introduces two new APIs to provide PBKDF2-based password hashing and verification.

## Goals
- Introduce PBKDF2 password hashing support with configurable parameters
- Provide a verification function to check hashed passwords against user inputs
- Support common HMAC algorithms (`SHA1`, `SHA256`, `SHA512`) and iteration count customization

## Motivation
Password hashing is a fundamental security requirement for authentication systems. PBKDF2 is a widely recognized key derivation function that enhances security by applying multiple iterations of a cryptographic hash function. By integrating PBKDF2 support, the Ballerina crypto module will offer a standardized and secure method for password storage and verification.

## Description
This proposal aims to introduce secure PBKDF2 password hashing and verification capabilities in the Ballerina crypto module.

### API Additions

#### PBKDF2 Hashing Function
A new API will be introduced to generate PBKDF2 hashes with configurable parameters:

```ballerina
public isolated function hashPbkdf2(
string password,
int iterations = 10000,
string algorithm = "SHA256"
) returns string|Error;
```

#### PBKDF2 Verification Function
A corresponding API will be introduced to verify a password against a PBKDF2 hash:

```ballerina
public isolated function verifyPbkdf2(
string password,
string hashedPassword
) returns boolean|Error;
```

These functions will allow developers to securely hash and verify passwords using PBKDF2 with customizable parameters for increased security.

39 changes: 39 additions & 0 deletions docs/spec/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ The conforming implementation of the specification is released and included in t
10. [Password hashing](#10-password-hashing)
* 10.1 [BCrypt](#101-bcrypt)
* 10.2 [Argon2](#102-argon2)
* 10.3 [PBKDF2](#103-pbkdf2)


## 1. [Overview](#1-overview)
Expand Down Expand Up @@ -1184,3 +1185,41 @@ string hashedPassword1 = check crypto:hashArgon2(password);
string hashedPassword2 = check crypto:hashArgon2(password, iterations = 4, memory = 131072, parallelism = 8);
boolean isValid = check crypto:verifyArgon2(password, hashedPassword1);
```
### 10.3 [PBKDF2](#103-pbkdf2)

Implements the PBKDF2 (Password-Based Key Derivation Function 2) algorithm for password hashing.

```ballerina
public isolated function hashPbkdf2(string password, int iterations = 10000,
Copy link
Member

@MohamedSabthar MohamedSabthar Apr 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's update the spec and proposal with the enum change

string algorithm = "SHA256") returns string|Error
```

Parameters:
- `password`: The plain text password to hash
- `iterations`: Optional number of iterations (default: 10000)
- `algorithm`: Optional HMAC algorithm (SHA1, SHA256, SHA512). Default is SHA256

Example:
```ballerina
string password = "mySecurePassword123";
// Hash with default parameters
string hashedPassword = check crypto:hashPbkdf2(password);
// Hash with custom parameters
string customHashedPassword = check crypto:hashPbkdf2(password, iterations = 15000, algorithm = "SHA512");
```

```ballerina
public isolated function verifyPbkdf2(string password, string hashedPassword) returns boolean|Error
```

Parameters:
- `password`: The plain text password to verify
- `hashedPassword`: PBKDF2 hashed password to verify against

Example:
```ballerina
string password = "mySecurePassword123";
string hashedPassword = "$pbkdf2-sha256$i=10000$salt$hash";
// Verify the hashed password
boolean isValid = check crypto:verifyPbkdf2(password, hashedPassword);
```
Loading
Loading