Skip to content

Instantly share code, notes, and snippets.

@insidegui
Last active September 30, 2018 11:42
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save insidegui/c78e648409e824dff0c404637b12d639 to your computer and use it in GitHub Desktop.
Save insidegui/c78e648409e824dff0c404637b12d639 to your computer and use it in GitHub Desktop.
Uses the pwnedpasswords API to verify password integrity
/*
By default, this class requires https://github.com/idrougge/sha1-swift to work,
you can replace the default SHA1 implementation by setting the hash property to a function
that takes a String and returns an optional String (the SHA1 hex string of the input)
*/
/// Uses the pwnedpasswords API to verify password integrity
public final class PwnageVerifier {
/// The base URL for the pwnedpasswords service (a default is provided by the initializer)
public let baseURL: URL
/// Initialize a new pwnage verifier with a base URL for the pwnedpasswords service (or the default one)
public init(baseURL: URL = URL(string: "https://api.pwnedpasswords.com/range")!) {
self.baseURL = baseURL
}
/// Hashing function to use when hashing passwords for pwnedpasswords, you can set this to your own implementation if you don't want to use the implementation from sha1-swift. You have to define USE_CUSTOM_HASHING for this to work
public var hash: ((String) -> String?)?
/// Errors returned in the verify completion handler
public enum Failure: Error {
/// The hashing failed
case hashing
/// An HTTP error occurred (includes the error code)
case http(Int)
/// A low-level networking error occurred at the URLSession level
case networking(Error)
/// Verifier failed to parse the data returned from the service
case parsing
public var localizedDescription: String {
switch self {
case .hashing: return "Failed to hash the input password"
case .http(let code): return "HTTP error \(code)"
case .networking(let err): return "Connection failed with error: \(err.localizedDescription)"
case .parsing: return "Failed to parse results returned from the server"
}
}
}
/// Represents a pwnage verification result
public enum Result: CustomStringConvertible {
/// Failed to verify password
case error(Failure)
/// The password has been pwned (includes count of times pwned)
case pwned(Int)
/// The password has not been pwned
case safe
public var description: String {
switch self {
case .safe:
return "This password has not appeared in any known data breaches"
case .pwned(let count):
return "This password has appeared in data breaches \(count) times"
case .error(let failure):
return failure.localizedDescription
}
}
}
private func hashRanges(from password: String) -> (String, String)? {
#if USE_CUSTOM_HASHING
guard let hashFunc = self.hash else {
fatalError("USE_CUSTOM_HASHING is defined but hash property has not been set!")
}
guard let hash = hashFunc(password) else {
return nil
}
#else
guard let hash = SHA1.hexString(from: password) else {
return nil
}
#endif
let effectiveHash = hash.replacingOccurrences(of: " ", with: "")
let endIndex = effectiveHash.index(effectiveHash.startIndex, offsetBy: 5)
let k = String(effectiveHash[effectiveHash.startIndex..<endIndex])
let r = String(effectiveHash[endIndex...])
return (k, r)
}
/// Performs a pwnage verification for the input password, the completion handler is called on the main queue
public func verify(password: String, completion: @escaping (Result) -> Void) {
guard let (verifyRange, matchRange) = hashRanges(from: password) else {
completion(.error(.hashing))
return
}
let url = baseURL.appendingPathComponent(verifyRange)
let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
guard let `self` = self else { return }
var result: Result
defer {
DispatchQueue.main.async {
completion(result)
}
}
if let error = error {
result = .error(.networking(error))
return
}
guard let response = response as? HTTPURLResponse else {
result = .error(.parsing)
return
}
guard response.statusCode == 200 else {
result = .error(.http(response.statusCode))
return
}
guard let data = data else {
result = .error(.parsing)
return
}
guard let contents = String(data: data, encoding: .utf8) else {
result = .error(.parsing)
return
}
result = self.process(response: contents, for: matchRange)
}
task.resume()
}
private func process(response: String, for hash: String) -> Result {
let lines = response.components(separatedBy: "\r\n")
if let match = lines.first(where: { $0.contains(hash) }) {
let components = match.components(separatedBy: ":")
guard components.count > 1 else { return .error(.parsing) }
return .pwned(Int(components[1]) ?? -1)
} else {
return .safe
}
}
}
/* USAGE EXAMPLE:
let verifier = PwnageVerifier()
verifier.verify(password: "abc123") { result in
switch result {
case .pwned(let count):
print("Pwned \(count) times")
case .safe:
print("Not pwned (yet)")
case .error(let err):
print("Failed to check: \(result.description)")
}
}
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment