Skip to content

Instantly share code, notes, and snippets.

@commonsguy
Created February 24, 2018 00:19
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save commonsguy/7ac7006e9a5e3ca0ab138e3da9f6c096 to your computer and use it in GitHub Desktop.
Save commonsguy/7ac7006e9a5e3ca0ab138e3da9f6c096 to your computer and use it in GitHub Desktop.
OkHttp/RxJava API for PwnedPasswords V2

PwnedPasswords API

This class indicates if a password is found in the "Have I Been Pwned?" collection of passwords, using the V2 API. Read more about this database and API in this blog post.

Specifically, this code uses the range portion of the API, so the password itself is not sent over the Interwebs. Only the first 5 characters of the SHA-1 has is sent to the server, which sends back a list of possible matches. This way, the password itself is not leaked.

Dependencies

io.reactivex.rxjava2:rxjava:2.1.7 (or compatible), com.squareup.okhttp3:okhttp:3.9.1 (or compatible), and the INTERNET permission (on Android).

Usage Notes

Create a PwnedCheck instance, supplying an OkHttpClient configured as you like. Call validate(), supplying the passphrase to check, and observe the result:

PwnedCheck checker=new PwnedCheck(new OkHttpClient());

checker.validate("password")
  .subscribeOn(Schedulers.io())
  .observeOn(AndroidSchedulers.mainThread())
  .subscribe(count -> ..., throwable -> ...);

0 indicates that the password hash did not match anything in the collection. A positive integer is the number of times that password was found in the collection, so higher numbers means a more popularly-used password. For example, password appears 3303003 times, as of when I wrote this code snippet.

License

This code is licensed under the Apache License, Version 2.0.

Support

Subscribe to the Warescription and ask me in an office hours chat.

Or, try Stack Overflow.

package com.commonsware.android.pwnedcheck;
import java.io.IOException;
import java.security.MessageDigest;
import io.reactivex.Observable;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class PwnedCheck {
final OkHttpClient okHttpClient;
public PwnedCheck(OkHttpClient okHttpClient) {
this.okHttpClient=okHttpClient;
}
public Observable<Integer> validate(String passphrase) {
return Observable.just(passphrase)
.map(PwnedCheck::getSha1Hex)
.flatMap(this::fetchCandidates)
.map(PwnedCheck::findCount);
}
// based on https://stackoverflow.com/a/33260623/115145
private static String getSha1Hex(String original) throws Exception {
MessageDigest messageDigest=MessageDigest.getInstance("SHA-1");
messageDigest.update(original.getBytes("UTF-8"));
byte[] bytes=messageDigest.digest();
StringBuilder buffer=new StringBuilder();
for (byte b : bytes) {
buffer.append(Integer.toString((b & 0xff)+0x100, 16).substring(1));
}
return buffer.toString();
}
private Observable<FetchResult> fetchCandidates(String sha1) throws IOException {
String url="https://api.pwnedpasswords.com/range/"+sha1.substring(0, 5);
Request request=new Request.Builder().url(url).build();
return Observable.fromCallable(
() -> new FetchResult(okHttpClient.newCall(request).execute(), sha1));
}
private static int findCount(FetchResult fetch) throws IOException {
String candidates=fetch.response.body().string();
String suffix=fetch.sha1.substring(5).toUpperCase();
for (String line : candidates.split("\r\n")) {
if (line.startsWith(suffix)) {
return(Integer.parseInt(line.split(":")[1]));
}
}
return(0);
}
private static class FetchResult {
final Response response;
final String sha1;
private FetchResult(Response response, String sha1) {
this.response=response;
this.sha1=sha1;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment