Wallet Pass Barcodes

Áskell can sign wallet-pass barcode values so scanning systems can verify that the value was generated by Áskell and has not been modified.

Format

The signed barcode value is sent in this format:

<value>:<signature>

Rules

  • value is the original barcode value.

  • signature is the HMAC-SHA256 of value.

  • Both value and the shared secret are treated as UTF-8 bytes.

  • signature is sent as a lowercase hex string.

  • The colon : is reserved as the delimiter, so barcode_message_template must not contain :.

Shared Secret

Each wallet pass template gets an automatically generated shared secret:

  • the secret is generated automatically when the template is created

  • the secret cannot be changed afterward

  • the secret is visible on the template details page in the dashboard and can be copied from there

Working Example

You can use this example to verify your implementation:

secret = 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
value = sub_SUB123
signature = fa7e0e69738cb28e457aad7e38a2aad2c66c7976b96f22d72f5d387ee6824105
barcode = sub_SUB123:fa7e0e69738cb28e457aad7e38a2aad2c66c7976b96f22d72f5d387ee6824105

Python

import hashlib
import hmac


def sign_wallet_barcode(value: str, secret: str) -> str:
    signature = hmac.new(
        secret.encode("utf-8"),
        value.encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()
    return f"{value}:{signature}"


def verify_wallet_barcode(barcode: str, secret: str) -> bool:
    value, signature = barcode.rsplit(":", 1)
    expected = hmac.new(
        secret.encode("utf-8"),
        value.encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(signature, expected)


secret = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
barcode = "sub_SUB123:fa7e0e69738cb28e457aad7e38a2aad2c66c7976b96f22d72f5d387ee6824105"

assert sign_wallet_barcode("sub_SUB123", secret) == barcode
assert verify_wallet_barcode(barcode, secret) is True

JavaScript / Node.js

const crypto = require('crypto');

function signWalletBarcode(value, secret) {
  const signature = crypto
    .createHmac('sha256', Buffer.from(secret, 'utf8'))
    .update(Buffer.from(value, 'utf8'))
    .digest('hex');

  return `${value}:${signature}`;
}

function verifyWalletBarcode(barcode, secret) {
  const index = barcode.lastIndexOf(':');
  const value = barcode.slice(0, index);
  const signature = barcode.slice(index + 1);
  const expected = crypto
    .createHmac('sha256', Buffer.from(secret, 'utf8'))
    .update(Buffer.from(value, 'utf8'))
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature, 'utf8'),
    Buffer.from(expected, 'utf8'),
  );
}

const secret =
  '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
const barcode =
  'sub_SUB123:fa7e0e69738cb28e457aad7e38a2aad2c66c7976b96f22d72f5d387ee6824105';

console.assert(signWalletBarcode('sub_SUB123', secret) === barcode);
console.assert(verifyWalletBarcode(barcode, secret) === true);

PHP

<?php

function sign_wallet_barcode(string $value, string $secret): string {
    $signature = hash_hmac('sha256', $value, $secret);
    return $value . ':' . $signature;
}

function verify_wallet_barcode(string $barcode, string $secret): bool {
    [$value, $signature] = explode(':', $barcode, 2);
    $expected = hash_hmac('sha256', $value, $secret);
    return hash_equals($expected, $signature);
}

$secret = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
$barcode = 'sub_SUB123:fa7e0e69738cb28e457aad7e38a2aad2c66c7976b96f22d72f5d387ee6824105';

assert(sign_wallet_barcode('sub_SUB123', $secret) === $barcode);
assert(verify_wallet_barcode($barcode, $secret) === true);

Go

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "strings"
)

func signWalletBarcode(value, secret string) string {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(value))
    signature := hex.EncodeToString(mac.Sum(nil))
    return value + ":" + signature
}

func verifyWalletBarcode(barcode, secret string) bool {
    index := strings.LastIndex(barcode, ":")
    value := barcode[:index]
    signature := barcode[index+1:]
    expected := signWalletBarcode(value, secret)
    expectedSignature := expected[strings.LastIndex(expected, ":")+1:]
    return hmac.Equal([]byte(signature), []byte(expectedSignature))
}

func main() {
    secret := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
    barcode := "sub_SUB123:fa7e0e69738cb28e457aad7e38a2aad2c66c7976b96f22d72f5d387ee6824105"

    fmt.Println(signWalletBarcode("sub_SUB123", secret) == barcode)
    fmt.Println(verifyWalletBarcode(barcode, secret))
}

C#

using System;
using System.Security.Cryptography;
using System.Text;

static string SignWalletBarcode(string value, string secret)
{
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    var signatureBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(value));
    var signature = Convert.ToHexString(signatureBytes).ToLowerInvariant();
    return $"{value}:{signature}";
}

static bool VerifyWalletBarcode(string barcode, string secret)
{
    var index = barcode.LastIndexOf(':');
    var value = barcode[..index];
    var signature = barcode[(index + 1)..];
    var expected = SignWalletBarcode(value, secret);
    var expectedSignature = expected[(expected.LastIndexOf(':') + 1)..];
    return CryptographicOperations.FixedTimeEquals(
        Encoding.UTF8.GetBytes(signature),
        Encoding.UTF8.GetBytes(expectedSignature)
    );
}

var secret = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
var barcode = "sub_SUB123:fa7e0e69738cb28e457aad7e38a2aad2c66c7976b96f22d72f5d387ee6824105";

Console.WriteLine(SignWalletBarcode("sub_SUB123", secret) == barcode);
Console.WriteLine(VerifyWalletBarcode(barcode, secret));

Notes

  • If you want to display human-readable text below the barcode, use barcode_alt_text_template. The signature is appended to the machine-readable barcode value, not necessarily the visible text.

  • During verification, always use a constant-time comparison helper where the language provides one, such as compare_digest, timingSafeEqual, or hash_equals.