BadCustomers

How private checks work

When your store checks a customer privately, through the WooCommerce plugin or a drop-in client for Node or PHP, the customer’s details never reach us. We can’t see who you checked, and we can’t tell whether they matched. This isn’t a promise in a privacy policy. The way the lookup works makes it impossible, and you can check that yourself.

In plain words

  1. On your server, the plugin or client turns the customer into lookup keys: their email, phone, street, name and IP, plus near-miss versions (the email with one letter dropped, the phone with one digit dropped, street spellings, nicknames, a sound-alike of the last name).
  2. Each key is scrambled with a random number that only your server knows, and thrown away after the check. What we receive is a list of numbers that look random. They’d look just as random for any other customer.
  3. We apply our secret key to those numbers and send them back. We can’t undo your scrambling, so we still don’t know what they were.
  4. Your server removes its scrambling. Only a key that really belongs to a reported customer unlocks an entry in the key table your server downloads from us. Matching and scoring happen on your server.
  5. Your server then tells us the verdict and score, nothing else, so the order counts in the numbers on the Check page.

Check it yourself

Fingerprint (SHA-256 of the key): 1f98e69999e2b9a54e1be7374db2e4fb27d66a819c7c79c6bf8f6b861b8dfcff

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA044J0YgMN7MalDJBopGl
VzkrL4MJy5qmAsMCY93y/wVyIFK2loDZL0hE5XtWJFRIQNpSEs1BUejVfWsSnvYG
ZgaEBntgv6ISSDvEvwnJL2GnIDyF1O82tSCVoglbSTVblfw6pO8pT0TVUymi1gh9
EAMz+Pl0Ku3/ATnWJWAZ9sqxXlMi3ChGIekbJ3QpgrnraEk9PQTr2QDeIhgRfUu3
ymhpbur0wgtm3Lru1WzP1AuTDy0eTTZV9b6n8aNK8iOojQLqn9zMJUbhDR0QfwbX
EzF1Y4X2l17MecRn8rOZ65GCpewUk0VnG3y76GnbWo2g4xYyICxuiI/heU09rWfs
lQIDAQAB
-----END PUBLIC KEY-----

This server’s plugin download, badcustomers-guard.zip: SHA-256 06591db7d192a43e296bb8b3594100b4f16999b2e82e34b3dfd7e373cc1efdeb

What we do see

Limits, honestly

Not on WooCommerce? Drop-in clients

One file does the whole private check from your own server with your API key: the lookup keys, the blinding, the key table, the scoring and the verdict. No dependencies, our key is pinned, and the answer has the same shape as POST /api/v1/check. The customer’s details never leave your server.

// Node
import { PrivateCheck } from './badcustomers-private.mjs';
const bc = new PrivateCheck({ apiKey: process.env.BC_API_KEY, cacheFile: '/var/cache/badcustomers-table.json' });
const result = await bc.check(customer, shipping, { orderRef: '1042' });
if (result.recommendation === 'hold') holdOrder();

// PHP
require 'badcustomers-private.php';
$bc     = new BadCustomers\PrivateCheck( getenv( 'BC_API_KEY' ), array( 'cache_file' => '/var/cache/badcustomers-table.json' ) );
$result = $bc->check( $customer, $shipping, array( 'order_ref' => '1042' ) );

customer and shipping take the same fields as /api/v1/check. The options and errors are explained at the top of each file. Both are built from the plugin’s and the registry’s own code and tested against them, so they reach the same verdicts key for key. They don’t update themselves: when the private-check rules change, the client says so and you download it again.

The method, for developers

A blind-RSA oblivious PRF (RSA-2048, e = 65537), private-check rules version 1. For each key x (“kind|value”, for example e|bob@gmail.com):

H(x)  = SHA-512("bcb1|h1|0|"+x) ‖ … ‖ SHA-512("bcb1|h1|4|"+x)  as a number, mod N
store:  r uniform in [2, N), send B = H(x) · r^e mod N           POST /api/v1/blind/eval
us:     S = B^d mod N  =  H(x)^d · r
store:  s = S · r^-1 mod N, check s^e = H(x)
        o = SHA-256("bcb1|out|" + x + "|" + s as 256 bytes)
        tag = SHA-256("bcb1|tag|" + o)[0..8]
        look tag up in GET /api/v1/blind/table (20-byte entries sorted by tag)
        entry[8..20] XOR SHA-256("bcb1|pad|" + o)[0..12] =
          report id u32 · flags u8 · category u8 · severity u8 · days since 2020-01-01 u16 · vendor u24
store:  POST /api/v1/blind/verdict { check_id, risk, score }

The keys and the scoring are in the plugin’s includes/blind.php. The registry’s own copy, used to build the table, is written in JavaScript and checked against the PHP one key for key. The drop-in clients package both.