October 15, 2020

929 words 5 mins read

Zxcvbn for Dart/Flutter

Zxcvbn for Dart/Flutter

Introducing our Dart port of DropBox's Zxcvbn Password Strength Library. Available now on pub.dev!

We want to help our users keep their account secure. One part of that is ensuring our users have good passwords that cannot easily be cracked. We wanted to give the users a simple rating of their password as they entered it, to encourage choosing a stronger password.

This isn’t a new problem - it’s one most web services grapple with. Thankfully, DropBox had already developed the very nice Zxcvbn library. We put this to use in CareApp web quite easily. However, there was a problem: Zxcvbn did not exist for Dart, so we ported it ourselves.

Get it now from pub.dev or GitHub. Pull requests welcome!

Usage

Zxcvbn accepts a password, with an optional dictionary of user-related words to include in dictionary attacks (eg their name, email). It returns a score between 0-4, as well as suggestions for making the password stronger.

Here’s an example:

import 'package:zxcvbn/zxcvbn.dart';

void main() {
  final zxcvbn = Zxcvbn();

  final result = zxcvbn.evaluate('P@ssw0rd');

  print('Password: ${result.password}');
  print('Score: ${result.score}');
  print(result.feedback.warning);
  for (final suggestion in result.feedback.suggestions) {
    print(suggestion);
  }
}

Deciding to Port

Changing a password in CareApp Mobile

Changing a password in CareApp Mobile

Before we started this process, we did look for existing solutions. There is already an existing Dart implementation, Xcvbnm. However, we found that Xcvbnm returned vastly different results compared to Zxcvbn official. Xcvbnm’s readme also makes it clear the library is incomplete:

Please note, that library did not port all functionality but is already used in production.

To press forward, we decided to port Zxcvbn to Dart from the original CoffeeScript. Our goals:

  • Give exactly the same results for the same inputs as the CoffeeScript
  • Port all tests from CoffeeScript
  • Be as close to the original CoffeeScript as possible in order to aid implementing and debugging dart, as well as making it easier to bring upstream changes
  • Hide the coffee-like implementation from users of this library, so from the outside it feels like Dart

As this is a close port of CoffeeScript, rather than a clean room implementation, the Dart code inside src is somewhat ugly and non-standard Dart in a lot of ways. However, keeping it close to CoffeeScript is seen as more important.

The Process

Once we had decided to keep the Dart port as close to the original CoffeeScript as possible, porting was slow, but fairly straight forward. Thankfully Zxcvbn has good automated tests. The work essentially involved translating a module from CoffeeScript to Dart, then translating the corresponding test file, and fixing things until all tests passed.

Pitfalls

I’m happy to go on the record and say that, when it comes to programming, I prefer languages that are statically typed, and are members of the curly-brace family. CoffeeScript is none of those things, and has some syntax that probably felt good in 2012 but my old 2020 brain just can’t handle.

Numerical Limits

One of the biggest causes of bugs during the port was numerical limits. Zxcvbn was written to run in a browser, and so all numbers are a Javascript number type - a float. However, Dart, when not running in a browser, has all kinds of different numerical types. Converting from Coffee to Dart required finding the right one. Zxcvbn generates very large numbers that overflow int. We use double to handle large numbers, as we don’t need the precision of a BigInt.

And unfortunately you can’t just use dynamic everywhere. Dart will treat a number that looks like an int (eg 42) as an integer type - and not a float. So a number that starts off small, will eventually get very large and overflow the integer, even though you set it to dynamic.

Trying to keep type safety

The other big effort during the port was trying to retrofit type safety onto the code as it was being ported. This involved trial and error - running tests, seeing casting errors, change a variable type, try again. Eventually we got there.

Like the numerical limit issue, you can’t just throw dynamic in and call it a day. This is demonstrated below:

dynamic x = 42;
dynamic y = 123.4;

dynamic z = x * y; // runtime error

This example is trivial, but when it’s happening somewhere in a thousand lines of very maths heavy code, errors can be hard to spot.

Discovering Properties

While a Javascript object can have properties tacked on throughout its lifetime, this is not possible in Dart. The closest equivalent is using a Map<String, dynamic>, which has its own set of issues.

This was an issue, because Zxcvbn has several different strength estimation algorithms, all of which add new properties to the PasswordMatch object.

To work around this issue, we ended up adding all the properties to PasswordMatch, and also overriding operator[] to allow accessing by name. This feels a bit gross, but was the quickest way to getting the port complete. Here’s an excerpt of PasswordMatch.

class PasswordMatch {
  String pattern;
  int i;
  int j;
  String token;
  String matched_word;

  dynamic operator [](String arg) {
    switch (arg) {
      case 'pattern':
        return pattern;
      case 'i':
        return i;
      case 'j':
        return j;
      case 'token':
        return token;
      case 'matched_word':
        return matched_word;
    }
  }
}

The Future

Zxcvbn for Dart works and is in use in CareApp Mobile. In our testing, we get exactly the same output when compared to the official Zxcvbn code. However, the code is not very Dart-like. Future work really is to tidy up the API and make the ergonomics better. Again, PRs are welcome!

Visual feedback for a weak password

Visual feedback for a weak password

comments powered by Disqus