Doorbot.ts – Let me in the building, with Typescript

A common issue with makerspaces is letting people in the door. Members sign up and should have access immediately. Members leave and should have their access dropped just as fast. Given the popularity of the Raspberry Pi among makers, it makes sense to start there, but how do you handle the software end?

Doorbot.ts is a modular solution to this problem. It’s split into three parts:

  • A Reader takes input from some device. It could be a USB key reader, or a Wiegand RFID reader
  • An Authenticator checks if the given input credentials are allowed
  • An Activator does something if the input passes

A basic setup might read a keyfob from the reader, authenticate against an API held on a central server, and then (assuming everything checks out) activate a GPIO pin on the Raspberry Pi for 30 seconds. This pin could be connected to a solenoid or magnetic hold to unlock the door (usually with a MOSFET or relay, since the RPi can’t drive the voltages for either of those).

The parts that interface to a Raspberry Pi (such as a Wiegand reader or activating a GPIO pin) are in a separate repository here: https://github.com/frezik/rpi-doorbot-ts

Security

The system can be compromised in quite a few ways. RFID tags can be copied. Cheap 10 digit keys can be brute forced. If you buy cheaply in bulk, there’s a good chance those RFIDs are sequential or otherwise predictable. The Raspberry Pi can be compromised. The database can be compromised.

Which is not to say it’s hopeless. That is, it’s no more hopeless than what security you already have. An attacker can put a rock through any window. Physical locks can be picked, often without much skill at all. If you’re like many makerspaces, you’ll give a keyfob to anyone who shows up with membership dues, and you probably don’t even check their ID.

So no, it’s not the most secure system in the world, but it’s also not the weakest point in the chain, either.

This isn’t to say security should be ignored. Take basic precautions here, like using good passwords, and keeping the systems up to date.

Wiegand

Wiegand is a common protocol for RFID readers. There are cheap readers that work like a USB keyboard, “typing” the numbers of the fob. However, they’re usually not built for outdoor mounting. There are cheap Wiegand options that are.

The protocol runs over two wires (plus power/ground). There is a Reader module in rpi-doorbot-ts for handling it directly. I don’t recommend using it. The issue is that reading the data over GPIO pins means using tight timing, and doing this in Node.js is often unreliable. It often resulted in needing to scan fobs two or three times before getting a clean read.

Instead, you can use a C program who’s only job is to read Wiegand, interpret the bits, and spit out the number. Doorbot.ts then has as Reader that can read a filehandle. So you launch the C program, pipe its stdout into the Reader, and there you go. From practical testing, this has been far more reliable in scanning fobs the first time.

Installing the System

I recommend creating a directory in your home dir on your Raspberry Pi:

$ mkdir doorbot-deployment
$ cd doorbot-deployment

And then create a basic package.json:

{
  "name": "doorbot",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@frezik/doorbot-ts": "^0.13.0",
    "@frezik/rpi-doorbot-ts": "^0.6.0"
  }
}

Be sure typescript is installed globally, and then install all the dependencies:

$ sudo npm install -g typescript
$ npm install

Next, make an index.ts to run the system:

import * as Doorbot from '../index';

const INPUT_FILE = "database.json";


let reader = new Doorbot.FHReader(
    process.stdin
);
let auth = new Doorbot.JSONAuthenticator( INPUT_FILE );
let act = new Doorbot.DoNothingActivator( () => {
    console.log( "Activate!" );
});

Doorbot.init_logger();
Doorbot.log.info( "Init" );
reader.setAuthenticator( auth );
auth.setActivator( act );
reader.init();

Doorbot.log.info( "Ready to read" );
reader
    .run()
    .then( () => {} );

This code creates a reader that works from STDIN, and authenticates against a JSON file, which would have entries like this:

{
    "1234": true,
    "5678": true,
    "0123": false
}

It then hooks the reader, authenticator, and activator together and starts it running. You can type the key codes (“1234”, “5678”, etc.) in to get a response.

I’ll be following up with some more advanced usages in future posts.

The Wisdom of TAP Numbering in a JavaScript World

TAP started as a simple way to test the Perl interpreter. It worked by outputting a test count on the first line, followed by a series of “ok” and “not ok” strings on subsequent lines. The hash character could be used for comments.

1..4
ok
ok
not ok # Better check if the frobniscator is working
ok

There’s been some additions over the years, but a lot of output of individual tests still looks a lot like this. Multiple tests could be wrapped together with prove. Here’s what it looks like to run that on Graphics::GVG:

$ prove -I lib t/*
t/001_load.t ............. ok     
t/002_pod.t .............. ok     
t/010_single_line.t ...... ok   
t/020_many_lines.t ....... ok   
t/030_circle.t ........... ok   
t/040_rect.t ............. ok   
t/050_color_variable.t ... ok   
t/060_num_variable.t ..... ok   
t/070_ellipse.t .......... ok   
t/080_regular_polygon.t .. ok   
t/090_int_variable.t ..... ok   
t/100_glow_effect.t ...... ok   
t/110_comments.t ......... ok   
t/120_point.t ............ ok   
t/130_include.t .......... skipped: Implement include files
t/140_include_vars.t ..... skipped: Implement include files
t/150_block_var.t ........ ok   
t/160_poly_offset.t ...... ok   
t/170_ast_to_string.t .... ok   
t/180_two_parses.t ....... ok   
t/190_meta.t ............. ok   
t/200_renderer.t ......... ok   
t/210_two_meta.t ......... ok   
t/220_named_params.t ..... ok   
t/230_renderer_class.t ... ok   
All tests successful.
Files=25, Tests=91, 10 wallclock secs ( 0.10 usr  0.05 sys +  8.86 cusr  0.57 csys =  9.58 CPU)
Result: PASS

Since TAP is just text output, it’s easy to implement libraries for other languages. With prove‘s --exec argument, we just pass an interpreter. Here’s an example of a TypeScript project of mine:

$ prove --exec ts-node test/*
test/activator_do_nothing.ts ........ ok   
test/activator_multi.ts ............. ok   
test/authenticator_always_false.ts .. ok   
test/authenticator_always.ts ........ ok   
test/authenticator_multi_fails.ts ... ok   
test/authenticator_multi.ts ......... ok   
test/logger.ts ...................... ok   
test/reader_fh.ts ................... ok   
test/reader_mock.ts ................. ok   
test/sanity.ts ...................... ok   
All tests successful.
Files=10, Tests=12, 17 wallclock secs ( 0.05 usr  0.01 sys + 30.40 cusr  1.16 csys = 31.62 CPU)
Result: PASS

Although the TAP package for JavaScript has a nice little runner all its own, which includes an automatic coverage report:

$ node_modules/.bin/tap --ts test/**/*.ts
 PASS  test/activator_do_nothing.ts 1 OK 5s
 PASS  test/authenticator_always_false.ts 1 OK 5s
 PASS  test/activator_multi.ts 2 OK 5s
 PASS  test/authenticator_always.ts 1 OK 5s
 PASS  test/authenticator_multi_fails.ts 1 OK 5s
 PASS  test/authenticator_multi.ts 1 OK 5s
 PASS  test/logger.ts 1 OK 5s
 PASS  test/reader_fh.ts 2 OK 5s
 PASS  test/sanity.ts 1 OK 3s
 PASS  test/reader_mock.ts 1 OK 3s

                         
  🌈 SUMMARY RESULTS 🌈
                         

Suites:   10 passed, 10 of 10 completed
Asserts:  12 passed, of 12
Time:     14s
--------------------------|----------|----------|----------|----------|-------------------|
File                      |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
--------------------------|----------|----------|----------|----------|-------------------|
All files                 |    93.04 |    78.57 |    82.86 |    93.33 |                   |
 doorbot.ts               |      100 |       80 |      100 |      100 |                   |
  index.ts                |      100 |       80 |      100 |      100 |                26 |
 doorbot.ts/src           |     91.4 |    77.78 |    81.82 |    92.31 |                   |
  activator_do_nothing.ts |      100 |      100 |      100 |      100 |                   |
  activator_multi.ts      |      100 |      100 |      100 |      100 |                   |
  authenticator_always.ts |      100 |      100 |      100 |      100 |                   |
  authenticator_multi.ts  |      100 |       80 |      100 |      100 |                22 |
  read_data.ts            |      100 |      100 |      100 |      100 |                   |
  reader.ts               |    28.57 |      100 |       25 |    33.33 |       39,40,41,44 |
  reader_fh.ts            |    81.25 |        0 |       50 |    81.25 |          43,53,54 |
  reader_mock.ts          |      100 |      100 |      100 |      100 |                   |
--------------------------|----------|----------|----------|----------|-------------------|

Which, of course, can be dropped into the scripts -> test section of your package.json.

There’s a feature of TAP that’s been falling out of use in recent years, even in Perl, and that’s test counts. Modern TAP allows you to put the test counts at the end, which can be done in Test::More by calling done_testing(). In the tap module for Node.js, you can simply not set a test count at all and it does it for you. Here’s an example of that with a sanity test, which just makes sure we can run the test suite at all:

import * as tap from 'tap';
import * as doorbot from '../index';

tap.pass( "Things basically work" );

Avoiding a test count seems to be the trend in Perl modules these days. After all, automated test libraries in other languages don’t have anything similar, and they seem to get by fine. If the test fails in the middle, that can be detected by a non-zero exit code. It’s always felt like annoying bookkeeping, so why bother?

For simple tests like the above, I think that’s fine. Failing with a non-zero exit code has worked reliably for me in the past.

However, there’s one place where I think TAP had the right idea way back in 1988: event driven or otherwise asynchronous code. Systems like this have been popping up in Perl over the years, but naturally, it’s Node.js that has built an entire ecosystem around the concept. Here’s one of my tests that uses a callback system:

import * as tap from 'tap';
import * as doorbot from '../index';
import * as os from 'os';

doorbot.init_logger( os.tmpdir() + "/doorbot_test.log"  );

tap.plan( 1 );


const always = new doorbot.AlwaysAuthenticator();
const act = new doorbot.DoNothingActivator( () => {
    tap.pass( "Callback made" );
});
always.setActivator( act );

const data = new doorbot.ReadData( "foo" );
const auth_promise = always.authenticate( data );

auth_promise.then( (res) => {} );

If the callback to run tap.pass() never gets hit, this test will fail. If you had removed the call to tap.plan( 1 ) towards the start, it would pass as long as it compiles and doesn’t otherwise hit a fatal error. Which is a pretty big thing to miss, since the callback is critical to the functionality of this particular module. Even if I knew it worked now, it might not in regression testing later.

Most (all?) other automated test frameworks around JavaScript have this problem. Sometimes, you can get around it by writing in clever ways, but it is far too easy to write test code that can erroneously declare success. Besides, shouldn’t your tests be written in the most obvious, straightforward way? TAP had the answer decades ago.