Skip to content

Commit abc2d0d

Browse files
committed
Improve API a bit
1 parent d105e80 commit abc2d0d

File tree

6 files changed

+77
-43
lines changed

6 files changed

+77
-43
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ A native, dependency and Foundation free Swift implementation of the bcrypt pass
1818
import Bcrypt
1919

2020
let password = "password"
21-
let hash = try Hasher(version: .v2a).hash(password: password)
21+
let hash = try Bcrypt.hash(password: password)
22+
let isValid = try Bcrypt.verify(password: password, hash: hash)
2223
```
2324

Sources/Bcrypt/Bcrypt.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
enum Bcrypt: Sendable {}

Sources/Bcrypt/Hasher+String.swift

Lines changed: 0 additions & 6 deletions
This file was deleted.

Sources/Bcrypt/Hasher.swift

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,48 @@
1-
public struct Hasher {
1+
extension Bcrypt {
22
@usableFromInline static let cipherText = Array("OrpheanBeholderScryDoubt".utf8)
33
@usableFromInline static let maxSalt = 16
44
@usableFromInline static let saltSpace = 22
55
@usableFromInline static let words = 6
66
@usableFromInline static let hashSpace = 60
77

8-
@usableFromInline let version: BcryptVersion
9-
10-
public init(version: BcryptVersion = .v2a) {
11-
self.version = version
8+
/// Hashes a password using the bcrypt algorithm.
9+
/// - Parameters:
10+
/// - password: the password to hash.
11+
/// - cost: number of rounds to apply the key derivation function, used as log2(cost). Must be between 4 and 31.
12+
/// - version: the version of the bcrypt algorithm to use. Defaults to `v2b`.
13+
/// - Throws: ``BcryptError``
14+
/// - Returns: the hashed password.
15+
@inlinable
16+
public static func hash(password: String, cost: Int = 10, version: BcryptVersion = .v2b) throws -> String {
17+
String(
18+
decoding: try hash(password: Array(password.utf8), cost: cost, salt: Self.generateRandomSalt(), version: version),
19+
as: UTF8.self
20+
)
1221
}
1322

14-
/// Encrypts a password using the bcrypt algorithm.
23+
/// Hashes a password using the bcrypt algorithm.
1524
/// - Parameters:
16-
/// - cost: number of rounds to apply the key derivation function, used as log2(cost)
17-
/// - password: the password to hash
18-
/// - Throws:
19-
/// - Returns:
25+
/// - password: the password to hash.
26+
/// - cost: number of rounds to apply the key derivation function, used as log2(cost). Must be between 4 and 31.
27+
/// - version: the version of the bcrypt algorithm to use. Defaults to `v2b`.
28+
/// - Throws: ``BcryptError``
29+
/// - Returns: the hashed password.
2030
@inlinable
21-
public func hash(password: [UInt8], cost: Int) throws -> [UInt8] {
22-
try hash(password: password, cost: cost, salt: Hasher.generateRandomSalt())
31+
public static func hash(password: [UInt8], cost: Int = 10, version: BcryptVersion = .v2b) throws -> [UInt8] {
32+
try hash(password: password, cost: cost, salt: Self.generateRandomSalt(), version: version)
2333
}
2434

35+
/// Hashes a password using the bcrypt algorithm.
36+
/// - Parameters:
37+
/// - password: the password to hash.
38+
/// - cost: number of rounds to apply the key derivation function, used as log2(cost). Must be between 4 and 31.
39+
/// - salt: the salt to use for the hash.
40+
/// - version: the version of the bcrypt algorithm to use. Defaults to `v2b`.
41+
/// - Throws: ``BcryptError``
42+
/// - Returns: the hashed password.
2543
@inlinable
26-
public func hash(password: [UInt8], cost: Int, salt: [UInt8]) throws -> [UInt8] {
27-
guard (salt.count * 3 / 4) - 1 < Hasher.maxSalt else {
44+
public static func hash(password: [UInt8], cost: Int = 10, salt: [UInt8], version: BcryptVersion = .v2b) throws -> [UInt8] {
45+
guard (salt.count * 3 / 4) - 1 < Self.maxSalt else {
2846
throw BcryptError.invalidSaltLength
2947
}
3048

@@ -47,12 +65,12 @@ public struct Hasher {
4765

4866
let (p, s) = EksBlowfish.setup(password: password, salt: cSalt, cost: cost)
4967

50-
var cData = [UInt32](repeating: 0, count: Hasher.words)
68+
var cData = [UInt32](repeating: 0, count: Self.words)
5169

5270
var i = 0
5371
var j = 0
54-
while i < Hasher.words {
55-
cData[i] = EksBlowfish.stream2word(data: Hasher.cipherText, j: &j)
72+
while i < Self.words {
73+
cData[i] = EksBlowfish.stream2word(data: Self.cipherText, j: &j)
5674
i &+= 1
5775
}
5876

@@ -61,7 +79,7 @@ public struct Hasher {
6179
var j = 0
6280
var xl: UInt32 = 0
6381
var xr: UInt32 = 0
64-
while j < Hasher.words / 2 {
82+
while j < Self.words / 2 {
6583
xl = cData[j * 2]
6684
xr = cData[j * 2 + 1]
6785
EksBlowfish.encipher(xl: &xl, xr: &xr, p: p, s: s)
@@ -72,9 +90,9 @@ public struct Hasher {
7290
i &+= 1
7391
}
7492

75-
var cipherText = Hasher.cipherText
93+
var cipherText = Self.cipherText
7694
i = 0
77-
while i < Hasher.words {
95+
while i < Self.words {
7896
cipherText[4 * i + 3] = UInt8(cData[i] & 0xff)
7997
cipherText[4 * i + 2] = UInt8((cData[i] &>> 8) & 0xff)
8098
cipherText[4 * i + 1] = UInt8((cData[i] &>> 16) & 0xff)
@@ -109,15 +127,19 @@ public struct Hasher {
109127
var salt = [UInt8](repeating: 0, count: saltSpace)
110128

111129
var cSalt = [UInt8](repeating: 0, count: maxSalt)
112-
for i in 0..<maxSalt {
130+
var i = 0
131+
while i < maxSalt {
113132
cSalt[i] = UInt8.random(in: .min ... .max)
133+
i &+= 1
114134
}
115135

116136
let encodedSalt = Base64.encode(cSalt, count: Self.hashSpace)
117-
for (i, byte) in encodedSalt.enumerated() {
137+
i = 0
138+
while i < encodedSalt.count {
118139
if i < saltSpace {
119-
salt[i] = byte
140+
salt[i] = encodedSalt[i]
120141
}
142+
i &+= 1
121143
}
122144

123145
return salt

Sources/Bcrypt/Verifier.swift

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,32 @@
1-
struct Verifier {
1+
extension Bcrypt {
2+
/// Verifies a password against a hash.
3+
/// - Parameters:
4+
/// - password: the password to verify.
5+
/// - hash: the hash to verify against.
6+
/// - Throws: ``BcryptError``
7+
/// - Returns: `true` if the password matches the hash, `false` otherwise.
28
@inlinable
3-
public func verify(password: [UInt8], hash goodHash: [UInt8]) throws -> Bool {
9+
public static func verify(password: String, hash: String) throws -> Bool {
10+
try verify(password: Array(password.utf8), hash: Array(hash.utf8))
11+
}
12+
13+
/// Verifies a password against a hash.
14+
/// - Parameters:
15+
/// - password: the password to verify.
16+
/// - hash: the hash to verify against.
17+
/// - Throws: ``BcryptError``
18+
/// - Returns: `true` if the password matches the hash, `false` otherwise.
19+
@inlinable
20+
public static func verify(password: [UInt8], hash goodHash: [UInt8]) throws -> Bool {
421
let prefix = goodHash.prefix(7)
522

623
let version = BcryptVersion(identifier: Array(prefix[1...2]))
724
let cost = prefix[4...5].reduce(0) { $0 * 10 + Int($1 - 48) }
825

926
let salt = Array(goodHash[7...28])
1027

11-
let hasher = Hasher(version: version)
12-
let newHash = try hasher.hash(password: password, cost: cost, salt: salt)
28+
let newHash = try Bcrypt.hash(password: password, cost: cost, salt: salt, version: version)
1329

1430
return newHash == goodHash
1531
}
16-
1732
}

Tests/BcryptTests/BcryptTests.swift

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,14 @@ struct BcryptTests {
5656
]
5757

5858
for testVector in testVectors {
59-
let hash = try Hasher(version: .v2a)
60-
.hash(password: Array(testVector.password.utf8), cost: testVector.cost, salt: Array(testVector.salt.utf8))
59+
let hash = try Bcrypt.hash(
60+
password: Array(testVector.password.utf8), cost: testVector.cost, salt: Array(testVector.salt.utf8), version: .v2a
61+
)
6162

6263
#expect(
6364
hash == Array(testVector.expectedHash.utf8),
64-
"Expected: \(testVector.expectedHash), got: \(String(decoding: hash, as: UTF8.self))")
65+
"Expected: \(testVector.expectedHash), got: \(String(decoding: hash, as: UTF8.self))"
66+
)
6567
}
6668
}
6769

@@ -70,15 +72,14 @@ struct BcryptTests {
7072
let password = "password"
7173
let cost = 12
7274

73-
let hash = try Hasher().hash(password: Array(password.utf8), cost: cost)
74-
let verifier = Verifier()
75-
#expect(try verifier.verify(password: Array(password.utf8), hash: hash))
75+
let hash = try Bcrypt.hash(password: password, cost: cost)
76+
77+
#expect(try Bcrypt.verify(password: password, hash: hash))
7678
}
7779

7880
@Test("Correct Version")
7981
func correctVersion() throws {
80-
let hash = try Hasher(version: .v2b)
81-
.hash(password: "password", cost: 6)
82+
let hash = try Bcrypt.hash(password: "password", cost: 6)
8283

8384
#expect(hash.hasPrefix("$2b$06$"))
8485
}

0 commit comments

Comments
 (0)