Skip to content

Commit 3e8ee09

Browse files
committed
Add some tests
1 parent 2394d36 commit 3e8ee09

4 files changed

Lines changed: 70 additions & 7 deletions

File tree

Sources/Bcrypt/EksBlowfish.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
static func setup(password: [UInt8], salt: [UInt8], cost: Int) -> (p: [UInt32], s: [UInt32]) {
1212
assert(cost >= 4 && cost <= 31, "Cost must be between 4 and 31, is \(cost)")
1313
assert(salt.count == 16, "Salt must be 16 bytes long, is \(salt.count)")
14-
assert(password.count > 0 && password.count <= 72, "Password must be between 1 and 72 bytes long, is \(password.count)")
14+
assert(
15+
password.count > 0 && password.count <= 73,
16+
"Password must be between 1 and 73 bytes long counting the NULL terminator, is \(password.count)")
1517

1618
var (p, s) = (Self.initialP, Self.initialS)
1719

Sources/Bcrypt/Hasher.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ extension Bcrypt {
1313
/// - Throws: ``BcryptError``
1414
/// - Returns: the hashed password.
1515
@inlinable
16-
public static func hash(password: String, cost: Int = 10, version: BcryptVersion = .v2b) throws -> String {
16+
public static func hash(password: String, cost: Int = 10, version: BcryptVersion = .v2b) throws(BcryptError) -> String {
1717
String(
1818
decoding: try hash(password: Array(password.utf8), cost: cost, salt: Self.generateRandomSalt(), version: version),
1919
as: UTF8.self
@@ -28,7 +28,7 @@ extension Bcrypt {
2828
/// - Throws: ``BcryptError``
2929
/// - Returns: the hashed password.
3030
@inlinable
31-
public static func hash(password: [UInt8], cost: Int = 10, version: BcryptVersion = .v2b) throws -> [UInt8] {
31+
public static func hash(password: [UInt8], cost: Int = 10, version: BcryptVersion = .v2b) throws(BcryptError) -> [UInt8] {
3232
try hash(password: password, cost: cost, salt: Self.generateRandomSalt(), version: version)
3333
}
3434

@@ -41,7 +41,8 @@ extension Bcrypt {
4141
/// - Throws: ``BcryptError``
4242
/// - Returns: the hashed password.
4343
@inlinable
44-
public static func hash(password: [UInt8], cost: Int = 10, salt: [UInt8], version: BcryptVersion = .v2b) throws -> [UInt8] {
44+
public static func hash(password: [UInt8], cost: Int = 10, salt: [UInt8], version: BcryptVersion = .v2b) throws(BcryptError) -> [UInt8]
45+
{
4546
guard (salt.count * 3 / 4) - 1 < Self.maxSalt else {
4647
throw BcryptError.invalidSaltLength
4748
}
@@ -62,7 +63,7 @@ extension Bcrypt {
6263
switch version {
6364
case .v2a: break
6465
case .v2b:
65-
guard password.count <= 72 else {
66+
guard password.count <= 73 else { // 72 + 1 because of the NULL terminator
6667
throw BcryptError.passwordTooLong
6768
}
6869
}

Sources/Bcrypt/Verifier.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ extension Bcrypt {
66
/// - Throws: ``BcryptError``
77
/// - Returns: `true` if the password matches the hash, `false` otherwise.
88
@inlinable
9-
public static func verify(password: String, hash: String) throws -> Bool {
9+
public static func verify(password: String, hash: String) throws(BcryptError) -> Bool {
1010
try verify(password: Array(password.utf8), hash: Array(hash.utf8))
1111
}
1212

@@ -17,7 +17,7 @@ extension Bcrypt {
1717
/// - Throws: ``BcryptError``
1818
/// - Returns: `true` if the password matches the hash, `false` otherwise.
1919
@inlinable
20-
public static func verify(password: [UInt8], hash goodHash: [UInt8]) throws -> Bool {
20+
public static func verify(password: [UInt8], hash goodHash: [UInt8]) throws(BcryptError) -> Bool {
2121
let prefix = goodHash.prefix(7)
2222

2323
let version = BcryptVersion(identifier: Array(prefix[1...2]))

Tests/BcryptTests/BcryptTests.swift

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,64 @@ struct BcryptTests {
3131

3232
#expect(hash.hasPrefix("$2b$06$"))
3333
}
34+
35+
@Test("Empty password")
36+
func emptyPassword() throws {
37+
#expect(throws: Error.self) {
38+
try Bcrypt.hash(password: "", cost: 6)
39+
}
40+
}
41+
42+
@Test("Maximum length password (72 bytes)")
43+
func maximumLengthPassword() throws {
44+
let password = String(repeating: "a", count: 72)
45+
let hash = try Bcrypt.hash(password: password, cost: 6)
46+
#expect(try Bcrypt.verify(password: password, hash: hash))
47+
}
48+
49+
@Test("Password too long")
50+
func passwordTooLong() throws {
51+
let password = String(repeating: "a", count: 73)
52+
#expect(throws: BcryptError.passwordTooLong) {
53+
try Bcrypt.hash(password: password, cost: 6)
54+
}
55+
}
56+
57+
@Test("Different passwords produce different hashes")
58+
func differentPasswordsDifferentHashes() throws {
59+
let hash1 = try Bcrypt.hash(password: "password1", cost: 6)
60+
let hash2 = try Bcrypt.hash(password: "password2", cost: 6)
61+
62+
#expect(hash1 != hash2)
63+
}
64+
65+
@Test("Same password with different salts produces different hashes")
66+
func samePwDifferentSalts() throws {
67+
let password = "test"
68+
let hash1 = try Bcrypt.hash(password: password, cost: 6)
69+
let hash2 = try Bcrypt.hash(password: password, cost: 6)
70+
71+
// Different salts should produce different hashes
72+
#expect(hash1 != hash2)
73+
74+
// But both should verify
75+
#expect(try Bcrypt.verify(password: password, hash: hash1))
76+
#expect(try Bcrypt.verify(password: password, hash: hash2))
77+
}
78+
79+
@Test("Unicode password handling")
80+
func unicodePassword() throws {
81+
let passwords = ["πάσσω", "密码", "🔐🔑", "Ñoño"]
82+
83+
for password in passwords {
84+
let hash = try Bcrypt.hash(password: password, cost: 4)
85+
#expect(try Bcrypt.verify(password: password, hash: hash))
86+
}
87+
}
88+
89+
@Test("Wrong password fails verification")
90+
func wrongPasswordFails() throws {
91+
let hash = try Bcrypt.hash(password: "correct", cost: 6)
92+
#expect(try !Bcrypt.verify(password: "wrong", hash: hash))
93+
}
3494
}

0 commit comments

Comments
 (0)