Skip to content

Commit

Permalink
Improve API a bit
Browse files Browse the repository at this point in the history
  • Loading branch information
ptoffy committed Nov 12, 2024
1 parent d105e80 commit abc2d0d
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 43 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ A native, dependency and Foundation free Swift implementation of the bcrypt pass
import Bcrypt

let password = "password"
let hash = try Hasher(version: .v2a).hash(password: password)
let hash = try Bcrypt.hash(password: password)
let isValid = try Bcrypt.verify(password: password, hash: hash)
```

1 change: 1 addition & 0 deletions Sources/Bcrypt/Bcrypt.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
enum Bcrypt: Sendable {}
6 changes: 0 additions & 6 deletions Sources/Bcrypt/Hasher+String.swift

This file was deleted.

68 changes: 45 additions & 23 deletions Sources/Bcrypt/Hasher.swift
Original file line number Diff line number Diff line change
@@ -1,30 +1,48 @@
public struct Hasher {
extension Bcrypt {
@usableFromInline static let cipherText = Array("OrpheanBeholderScryDoubt".utf8)
@usableFromInline static let maxSalt = 16
@usableFromInline static let saltSpace = 22
@usableFromInline static let words = 6
@usableFromInline static let hashSpace = 60

@usableFromInline let version: BcryptVersion

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

/// Encrypts a password using the bcrypt algorithm.
/// Hashes a password using the bcrypt algorithm.
/// - Parameters:
/// - cost: number of rounds to apply the key derivation function, used as log2(cost)
/// - password: the password to hash
/// - Throws:
/// - Returns:
/// - password: the password to hash.
/// - cost: number of rounds to apply the key derivation function, used as log2(cost). Must be between 4 and 31.
/// - version: the version of the bcrypt algorithm to use. Defaults to `v2b`.
/// - Throws: ``BcryptError``
/// - Returns: the hashed password.
@inlinable
public func hash(password: [UInt8], cost: Int) throws -> [UInt8] {
try hash(password: password, cost: cost, salt: Hasher.generateRandomSalt())
public static func hash(password: [UInt8], cost: Int = 10, version: BcryptVersion = .v2b) throws -> [UInt8] {
try hash(password: password, cost: cost, salt: Self.generateRandomSalt(), version: version)
}

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

Expand All @@ -47,12 +65,12 @@ public struct Hasher {

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

var cData = [UInt32](repeating: 0, count: Hasher.words)
var cData = [UInt32](repeating: 0, count: Self.words)

var i = 0
var j = 0
while i < Hasher.words {
cData[i] = EksBlowfish.stream2word(data: Hasher.cipherText, j: &j)
while i < Self.words {
cData[i] = EksBlowfish.stream2word(data: Self.cipherText, j: &j)
i &+= 1
}

Expand All @@ -61,7 +79,7 @@ public struct Hasher {
var j = 0
var xl: UInt32 = 0
var xr: UInt32 = 0
while j < Hasher.words / 2 {
while j < Self.words / 2 {
xl = cData[j * 2]
xr = cData[j * 2 + 1]
EksBlowfish.encipher(xl: &xl, xr: &xr, p: p, s: s)
Expand All @@ -72,9 +90,9 @@ public struct Hasher {
i &+= 1
}

var cipherText = Hasher.cipherText
var cipherText = Self.cipherText
i = 0
while i < Hasher.words {
while i < Self.words {
cipherText[4 * i + 3] = UInt8(cData[i] & 0xff)
cipherText[4 * i + 2] = UInt8((cData[i] &>> 8) & 0xff)
cipherText[4 * i + 1] = UInt8((cData[i] &>> 16) & 0xff)
Expand Down Expand Up @@ -109,15 +127,19 @@ public struct Hasher {
var salt = [UInt8](repeating: 0, count: saltSpace)

var cSalt = [UInt8](repeating: 0, count: maxSalt)
for i in 0..<maxSalt {
var i = 0
while i < maxSalt {
cSalt[i] = UInt8.random(in: .min ... .max)
i &+= 1
}

let encodedSalt = Base64.encode(cSalt, count: Self.hashSpace)
for (i, byte) in encodedSalt.enumerated() {
i = 0
while i < encodedSalt.count {
if i < saltSpace {
salt[i] = byte
salt[i] = encodedSalt[i]
}
i &+= 1
}

return salt
Expand Down
25 changes: 20 additions & 5 deletions Sources/Bcrypt/Verifier.swift
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
struct Verifier {
extension Bcrypt {
/// Verifies a password against a hash.
/// - Parameters:
/// - password: the password to verify.
/// - hash: the hash to verify against.
/// - Throws: ``BcryptError``
/// - Returns: `true` if the password matches the hash, `false` otherwise.
@inlinable
public func verify(password: [UInt8], hash goodHash: [UInt8]) throws -> Bool {
public static func verify(password: String, hash: String) throws -> Bool {
try verify(password: Array(password.utf8), hash: Array(hash.utf8))
}

/// Verifies a password against a hash.
/// - Parameters:
/// - password: the password to verify.
/// - hash: the hash to verify against.
/// - Throws: ``BcryptError``
/// - Returns: `true` if the password matches the hash, `false` otherwise.
@inlinable
public static func verify(password: [UInt8], hash goodHash: [UInt8]) throws -> Bool {
let prefix = goodHash.prefix(7)

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

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

let hasher = Hasher(version: version)
let newHash = try hasher.hash(password: password, cost: cost, salt: salt)
let newHash = try Bcrypt.hash(password: password, cost: cost, salt: salt, version: version)

return newHash == goodHash
}

}
17 changes: 9 additions & 8 deletions Tests/BcryptTests/BcryptTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,14 @@ struct BcryptTests {
]

for testVector in testVectors {
let hash = try Hasher(version: .v2a)
.hash(password: Array(testVector.password.utf8), cost: testVector.cost, salt: Array(testVector.salt.utf8))
let hash = try Bcrypt.hash(
password: Array(testVector.password.utf8), cost: testVector.cost, salt: Array(testVector.salt.utf8), version: .v2a
)

#expect(
hash == Array(testVector.expectedHash.utf8),
"Expected: \(testVector.expectedHash), got: \(String(decoding: hash, as: UTF8.self))")
"Expected: \(testVector.expectedHash), got: \(String(decoding: hash, as: UTF8.self))"
)
}
}

Expand All @@ -70,15 +72,14 @@ struct BcryptTests {
let password = "password"
let cost = 12

let hash = try Hasher().hash(password: Array(password.utf8), cost: cost)
let verifier = Verifier()
#expect(try verifier.verify(password: Array(password.utf8), hash: hash))
let hash = try Bcrypt.hash(password: password, cost: cost)

#expect(try Bcrypt.verify(password: password, hash: hash))
}

@Test("Correct Version")
func correctVersion() throws {
let hash = try Hasher(version: .v2b)
.hash(password: "password", cost: 6)
let hash = try Bcrypt.hash(password: "password", cost: 6)

#expect(hash.hasPrefix("$2b$06$"))
}
Expand Down

0 comments on commit abc2d0d

Please sign in to comment.