For the latest version of the code, please check the project at
Github.
Introduction
Adobe, Snapchat, Sony, these are just some of the many high profile data breaches in recent years. Have you ever wondered if your account has been compromised in one of these data breaches?
In this article, we will walk through the creation of Pwnage Checker. Pwnage checker is an iOS app built using Swift. It allows users to easily check whether an account has been compromised in a known data breach on any iOS device. It leverages the https://haveibeenpwned.com API created by Troy Hunt, which aggregates public leaked data from breaches and makes them readily searchable.
Core Functionality
Search Function
The main function of the app is to search whether an account is found in a known data breach, therefore the home screen presents this function. The search has the following work flow.
- User begins a search by specifying the account to search for in one of two ways:
- User chooses "Enter an account" to begin a search by entering an account.
- User chooses "Select from contact" to begin a search by selecting the email address of an existing contact. This is useful if you want to check for someone else, e.g.. a less technical friend or elders in your family.
- User is presented with the search result.
- If no breach is found, a screen with green background and a reassuring smiley face will be presented.
- If the account is found in one or more data breaches, a screen with red background and a sad face will be presented to draw attention. The screen will also list out the breaches the account is found in.
- If the account is found in any data breach, user can drill down to view the detail of the data breach.
Browse Function
The app allows the users to browse all breaches that are loaded into the service. User can drill down to view the detail information of each breach, such as the number of accounts that were leaked and the types of data that were leaked.
Subscribe Function
The app allows users to subscribe to breach notifications on their account. The subscribe feature is not available via the https://haveibeenpwned.com API, but it is available on the website. This screen simply hosts a webview which loads the subscribe page from the site. This is provided as a convenience for the user.
Using the code
Listing 1: SearchViewController
class
class SearchViewController: UIViewController, CNContactPickerDelegate {
The SearchViewController handles the interaction on the home screen. It implements the CNContactPickerDelegate protocol because it uses CNContactPickerViewController to let user select a contact.
Listing 2: SearchViewController.enterAccountButtonTouch
method
@IBAction func enterAccountButtonTouch(sender: AnyObject) {
var inputTextField: UITextField?
let prompt = UIAlertController(title: "Enter an account", message: "", preferredStyle: UIAlertControllerStyle.Alert)
prompt.addAction(UIAlertAction(title: "Check", style: UIAlertActionStyle.Default, handler: { (action) -> Void in
if let account = inputTextField?.text {
self.checkAccount(account)
}
}))
prompt.addAction(UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Default, handler: nil))
prompt.addTextFieldWithConfigurationHandler({(textField: UITextField!) in
textField.placeholder = "Enter email address or username"
inputTextField = textField
})
presentViewController(prompt, animated: true, completion: nil)
}
The "Enter an account" button's touch action is connected to the enterAccountButtonTouch
method. When the button is touched, it creates an UIAlertController
to prompt user for an account. The controls on the UIAlertController are dynamically added. The addAction
method is used to add the check button and the cancel button with the appropriate handler code. The addTextFieldWithConfigurationHandler
method is used to add and configure the text field.
Listing 3: SearchViewController.selectContactButtonTouch
method
@IBAction func selectContactButtonTouch(sender: AnyObject) {
let contactPickerViewController = CNContactPickerViewController()
contactPickerViewController.predicateForEnablingContact = NSPredicate(format: "emailAddresses.@count > 0")
contactPickerViewController.predicateForSelectionOfContact = NSPredicate(format: "emailAddresses.@count == 1")
contactPickerViewController.displayedPropertyKeys = [CNContactEmailAddressesKey]
contactPickerViewController.delegate = self
presentViewController(contactPickerViewController, animated: true, completion: nil)
}
The "Select from contact" button's touch action is connected to the selectContactButtonTouch
method. When the button is touched, it will create and present a CNContactPickerViewController
to allow user to pick an email from a contact.
predicateForEnablingContact
property is used to set a condition for when a contact should be enabled. We only want a contact to be enabled if it has at least one email address, and we set it like so:
contactPickerViewController.predicateForEnablingContact = NSPredicate(format: "emailAddresses.@count > 0")
In the screen shot below, notice the David Taylor contact is disabled in the picker, it's because it has no email address.
predicateForSelectionOfContact
property is used to set the condition on whether a selected contact should be returned to our code, or to display the detail of the selected contact. We want the selected contact returned immediately if it has only one email address, otherwise we want to display the detail view so that the user can select one of the multiple email addresses. We setup the predicate like so:
contactPickerViewController.predicateForSelectionOfContact = NSPredicate(format: "emailAddresses.@count == 1")
displayedPropertyKeys
property is used to limit what properties will be shown on the contact detail view. For our app, we only need the email address. We set it up like so:
contactPickerViewController.displayedPropertyKeys = [CNContactEmailAddressesKey]
Below screen shot shows what happens when a contact with multiple email addresses is selected. The contact detail view is show. User can select from one of the email addresses.
Listing 4: SearchViewController.checkAccount
method
func checkAccount(account: String) {
LoadingIndicatorView.show("checking")
HaveIBeenPwnedClient.sharedInstance().getBreachesForAccount(account) {
(hasBreaches, result, error) in
dispatch_async(dispatch_get_main_queue()) {
if (error != nil) {
LoadingIndicatorView.hide()
ViewHelper.showError("Unable to check account. ")
print(error)
}
else {
let controller = self.storyboard?.instantiateViewControllerWithIdentifier("SearchResultViewController") as! SearchResultsViewController
controller.account = account
controller.hasBreaches = hasBreaches
controller.apiResult = result
self.navigationController?.pushViewController(controller, animated: true)
LoadingIndicatorView.hide()
}
}
}
}
The checkAccount
method will initiate the search using the HaveIBeenPwnedClient
and presents the search result with SearchResultViewController
. The method is called when user enters an account in account prompt, or when user selects an email from the contact picker.
Listing 5: SearchResultsViewController
class
class SearchResultsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
SearchResultsViewController
presents a search result. It uses UITableView
to present a list breaches that the account is found in, and therefore implements the related UITableViewDataSource
, UITableViewDelegate
protocols.
Listing 6: SearchResultsViewController.showBreach
method
func showBreach() {
let breachArray = apiResult as! [[String:AnyObject]]
for breachItem in breachArray {
let breach = Breach(apiBreachResult: breachItem, context: tempContext)
breaches.append(breach)
print(breach.title)
}
print(breaches.count)
view.backgroundColor = UIColor(red: 229/255, green: 000/255, blue: 0/255, alpha: 1)
headerView.backgroundColor = UIColor(red: 229/255, green: 000/255, blue: 0/255, alpha: 1)
iconLabel.text = "\u{e403}"
titleLabel.text = "Oh no — pwned! Pwned on \(breaches.count) breached sites"
subtitleLabel.text = "A \"breach\" is an incident where a site's data has been illegally accessed by hackers and then released publicly. Review the types of data that were compromised (email addresses, passwords, credit cards etc.) and take appropriate action, such as changing passwords."
tableView.hidden = false
tableView.reloadData()
}
showBreach
method is called when an account is found in one or more data breach. It updates the screen background and labels. It also sets up the breachArray and reloads the UITableView
to show all the breaches the account are found in. The following screen shot shows what it looks like.
Listing 7: SearchResultsViewController.showNoBreach
method
func showNoBreach() {
view.backgroundColor = UIColor(red: 0/255, green: 100/255, blue: 0/255, alpha: 1)
headerView.backgroundColor = UIColor(red: 0/255, green: 100/255, blue: 0/255, alpha: 1)
iconLabel.text = "\u{e415}"
titleLabel.text = "Good news — no pwnage found!"
subtitleLabel.text = "No breached accounts"
tableView.hidden = true
}
showNoBreach
method is called when the account is not found in any of the data breach in the system. It updates the screen background and labels. The following screen shot shows what it looks like.
Listing 8: BreachTableViewController
class
class BreachTableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, NSFetchedResultsControllerDelegate
The BreachTableViewController
is responsible for showing all the breaches that are loaded into the service. It shows the data breaches in a UITableView
and therefore implements the related UITableViewDataSource
and UITableViewDelegate
protocols. It also implements the NSFetchedResultsControllerDelegate
protocol to manage the update of the UITableView
when the underlying data changes. Following is a sample screen shot.
Listing 9: BreachViewController
class
class BreachViewController: UIViewController {
var breach : Breach!
@IBOutlet weak var logoImageView: UIImageView!
@IBOutlet weak var descriptionLabel: UILabel!
@IBOutlet weak var compromisedDataLabel: UILabel!
@IBOutlet weak var pwnCountLabel: UILabel!
@IBOutlet weak var breachDateLabel: UILabel!
@IBOutlet weak var isSensitiveSwitch: UISwitch!
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = breach.title
compromisedDataLabel.text = breach.dataClasses ?? "N/A"
pwnCountLabel.text = breach.pwnCount?.stringValue ?? "N/A"
breachDateLabel.text = breach.breachDate ?? "N/A"
isSensitiveSwitch.on = breach.isSensitive?.integerValue > 0
var text = ""
if let desc = breach.desc?.dataUsingEncoding(NSUTF8StringEncoding) {
do {
text = try NSAttributedString(data: desc,
options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType,
NSCharacterEncodingDocumentAttribute:NSUTF8StringEncoding],
documentAttributes: nil).string
} catch let error as NSError {
print(error.localizedDescription)
}
}
descriptionLabel.text = text
if let image = ImageCache.sharedInstance().imageWithName(breach.domain!) {
logoImageView.image = image
}
}
}
The BreachViewController
is responsible for showing the detail information of a data breach. To Following is a screen shot.
Listing 10: NotifyViewController
class
class NotifyViewController: UIViewController {
@IBOutlet weak var webView: UIWebView!
override func viewDidLoad() {
webView.loadRequest(NSURLRequest(URL: NSURL(string: "https://haveibeenpwned.com/NotifyMe")!))
self.navigationController?.navigationBarHidden = true
}
}
The NotifyViewController
class is responsible for loading and showing the subscribe page from the haveibeenpwned.com website. It's
Listing 11: HaveIBeenPwnedClient
class
class HaveIBeenPwnedClient : NSObject {
func getBreachesForAccount(emailOrUsername : String, completionHandler : (hasBreaches: Bool, result: AnyObject!, error: String?)->Void) {
....
}
func refreshBreachesInBackground(completionHandler: (error: String?)->Void)->Void {
...
}
...
The HaveIBeenPwnedClient
class is responsible for making api request to the haveibeenpwned.com api.
Listing 12: ClearbitClient
class
class ClearbitClient : NSObject {
func getImage(domain: String, completionHandler: (imageData: NSData?, error: String?)->Void) -> Void {
...
}
...
The ClearbitClient
class is responsible for making api request to the Clearbit Logo api for retrieving logo images of the companies in a data breach.
Listing 13: HttpClient
class
class HttpClient: NSObject {
func httpGet(baseUrl: String, method: String, urlParams: [String:AnyObject]?, headerParams: [String:AnyObject]?, completionHandler: (result: NSData?, code: Int?, error: String?) -> Void) {
....
}
func httpPost(baseUrl: String, method: String, urlParams: [String:AnyObject]?, headerParams: [String:AnyObject]?, jsonBody: [String:AnyObject], completionHandler: (result: NSData?, code: Int?, error: String?) -> Void) {
....
}
func httpPut(baseUrl: String, method: String, urlParams: [String:AnyObject]?, headerParams: [String:AnyObject]?, jsonBody: [String:AnyObject], completionHandler: (result: NSData?, code: Int?, error: String?) -> Void) {
....
}
...
The HttpClient
class is a helper that that provides utility methods for making http requests easier. Both the HaveIBeenPwnedClient
and ClearbitClient
uses it for making api requests.
Points of Interest
The App's application logic is relatively simple. Most of the time was actually spent on making the app more polished. This involves trying out different UI layouts and finding image resources that the app can use. In my quest for making the App more attractive, I find the following useful:
Google Material icons are a set of beautifully crafted and easy to use icons licensed under creative common that you can use in your web, Android, and iOS projects. When you download the icon, you have several options. You can choose the density dependent pixels in 18dp, 24dp, 36dp, or 48dp. You can choose white or dark background. Moreover, you can choose the format as svg, pngs, or icon font. The icons used in the app are all from the Google Material Icons.
The Clearbit Logo Api is allows you to quickly retrieve a company's logo image given its domain. In the breach data returned from haveibeenpwned.com api, each item contains the company domain, but no company image. With the clearbit api, the app is able to show the image along with the breach data. See the image below. The aesthetic difference in terms an all text list and a text + image list is huge.
Using emojicon as image
Emojicons are widley supported on different platforms. It looks attractive and has a wide selection. To use them as 'fake" image, you simply have to create a label and set its text to the desired emojico's code. You adjust the size by changing the font size. In this app, the search result screen make use of two emojicons.
iconLabel.text = "\u{e403}"
When breach is found, it shows the sad face emojicon. As in the following image
iconLabel.text = "\u{e415}"
When no breach is found, it shows the smiley face emojicon. As in the following image