Tuesday, March 11, 2025

A PoS Terminal App in Go

Let's start on the Go end, partly because we haven't done anything there yet and partly because it will be easier to develop the Android app if the terminal is just sitting there waiting for a tap.

I think we have enough to get started, so let's just do that. First off, we can create a project with a cmd directory for the receipt_term application; copying in the launch.json we already have and adding the PCSC project as a dependency.
$ mkdir receipt_term_go
$ cd receipt_term_go
$ go mod init github.com/gmmapowell/ignorance/receipt_term
go: creating new go.mod: module github.com/gmmapowell/ignorance/receipt_term
$ mkdir cmd
$ mkdir receipt_term
$ mkdir .vscode
$ cp ~/acs1552u/gopcsc/.vscode/launch.json .vscode/
$ go get github.com/deeper-x/gopcsc
go: downloading github.com/deeper-x/gopcsc v1.9.9
go: added github.com/deeper-x/gopcsc v1.9.9
We can then fill in the minimal amount of code to make sure that we can connect to a card (basically copying extracts from our existing show-atr and apdu commands:
package main

import (
    "log"

    "github.com/deeper-x/gopcsc/smartcard"
)

func main() {
    // Create a context
    ctx, err := smartcard.EstablishContext()
    if err != nil {
        panic(err)
    }
    defer ctx.Release()

    for {
        doCardInteraction(ctx)
    }
}

func doCardInteraction(ctx *smartcard.Context) {
    reader, err := ctx.WaitForCardPresent()
    if err != nil {
        panic(err)
    }
    card, err := reader.Connect()
    if err != nil {
        panic(err)
    }
    log.Printf("connected to smart card\n")
    tryToSendReceipt(card)
    card.Disconnect()
    reader.WaitUntilCardRemoved()
    log.Printf("disconnected from smart card\n")
}

func tryToSendReceipt(card *smartcard.Card) {
    log.Printf("placeholder to send receipt to card %v\n", card)
}

GO_TERMINAL_RECEIPT_MINIMAL:receipt_term_go/cmd/receipt_term/main.go

And the launch configuration is fairly simple:
{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "receipt_term",
            "type": "go",
            "request": "launch",
            "program": "cmd/receipt_term",
            "args": []
        }
    ]
}

GO_TERMINAL_RECEIPT_MINIMAL:receipt_term_go/.vscode/launch.json

And, not surprisingly, it all works when we tap the phone or a room key:
2025/01/15 22:42:05 connected to smart card
2025/01/15 22:42:05 placeholder to send receipt to card &{0xc0001944f0 181793975 2 [59 128 128 1 1]}
2025/01/15 22:42:05 disconnected from smart card

Selecting the Receipt Application

The first step is to select the receipt application on the "card" (or phone, as you will). Obviously, we need to consider error cases because no cards, and very few phones, will actually have this application loaded. Now, we can limit the number of error cases by having a UX on the terminal that says "How would you like your receipt?" where the options include "TapReceipt, Inc." or some such. But for now, we are just going to either proceed or silently (ish) fail.

Borrowing code from the APDU program, we can write this:
func tryToSendReceipt(card *smartcard.Card) {
    log.Printf("selecting receipt app on card\n")
    // select command
    apdu := []byte{0x00, 0xA4, 0x04, 0x00}
    // AID
    apdu = append(apdu, 0x10, 0xF1, 0x67, 0x6d, 0x6d, 0x61, 0x70, 0x6f, 0x77, 0x65, 0x6c, 0x6c, 0x2d, 0x61, 0x70, 0x70, 0x31)
    // Le - expected response length
    apdu = append(apdu, 0x00)
    cmd := smartcard.CommandAPDU(apdu)
    if !cmd.IsValid() {
        panic(fmt.Sprintf("invalid apdu from %v", apdu))
    }
    log.Printf("issuing cmd %s\n", cmd)
    res, err := card.TransmitAPDU(cmd)
    if err != nil {
        panic(err)
    }
    log.Printf("<< %s\n", res)
    if res == nil || len(res) != 2 {
        panic("result was not 2 bytes")
    }
    if res[0] != 0x90 || res[1] != 0x00 {
        log.Printf("response was not 0x90 0x00")
        return
    }
    log.Printf("still need to transmit receipt")
}

GO_TERMINAL_RECEIPT_SELECT:receipt_term_go/cmd/receipt_term/main.go

This simply just builds up the same APDU string we used before and then passes it to the smartcard package for processing. It doesn't handle errors well at the moment (it would not be good to have a PoS terminal that paniced this much!) but it does distinguish between the clear "success" case and the various "error" cases. It's worth noting that if you don't have your phone turned on, the reader "beeps" but the application is not selected and an error code (our old friend 6A 82) is returned.

Writing the Receipt

There's a design decision to be made here, and I have made it; I accept there are other ways to go about this, and you are free to explore them on your own time.

Part of the basic APDU protocol is the ability to write "blocks" of data to the smart card. This is probably obvious if you are familiar with the way the technology is physically implemented, but it makes sense to me that a card would have a fixed number of fixed-size blocks. Each of these can be individually written using an APDU Update Binary command. Basically, this just says "write this data to this block" and I assume that the assumption is that either you will write exactly the right number of bytes, or that the rest of the block will be blanked. Anyway, none of that matters much to me. What does matter is that we can specify two byte-sized integers that give a reference to a "page" and then can write up to 64K of data to that page. My intention is to write each "line" of the receipt as a separate page.

Because I can, I'm going to use the two keys somewhat independently: the first one is going to identify a "type" of line, and the second is going to be an "index" or "count".

Specifically, I am going to use P1 as follows:
  • 0x11: this is a generic header line
  • 0x12: this is a preface line, such as MID or PID
  • 0x21: this is an individual item line
  • 0x22: this is a comment on an individual item line, such as a "weight" or a "discount code"
  • 0x31: this is a subtotal line
  • 0x32: this is an adjustment line (e.g. discounts)
  • 0x33: this is a tax indicator
  • 0x3f: this is the final total
  • 0x41: this indicates a payment
  • 0x51: generic footer lines
And then I am going to use P2 to count through these lines, so that, for example, for the item lines:
  • 0x01 is the first item
  • 0x02 is the second item
  • If the third line has P1 = 0x22, 0x01 indicates that this is the first comment on the previous 0x21 line
  • And so on
In general, I would expect every combination of P1 and P2 in the receipt to be unique EXCEPT for the 0x22 lines, where they may repeat because the P2 will reset to 0x01 for each new 0x01.

I hope that's clear.

Generating Receipts

For fun, and because it will make for more interesting output and a wider range of experiences, I am going to generate each receipt from scratch. This requires quite a bit of work, most of which has nothing to do with what we're here for. So I'm just going to present the interfaces which have the end result and leave it to anyone interested in the details to dig in themselves. All of this code is in internal/store and internal/receipt. Suffice it to say that I have put more effort into this than it deserves in the context, but less effort than it would take to make it realistic.

A Store is basically just an interface behind which most of the code is buried and represents the "real world" concept of a shop or store:
package store

import "github.com/gmmapowell/ignorance/receipt_term/internal/receipt"

type Store interface {
    MakePurchase() *receipt.Receipt
}
The Receipt is an assemblage of structs:
package receipt

type Receipt struct {
    Headers   []string
    Preface   []Preface
    LineItems []LineItem
    Totals    []TotalLine
    Payments  []PaymentLine
    Footers   []string
}

type Preface struct {
    Title string
    Value string
}

type LineItem struct {
    Desc     string
    Price    Money
    Comments []LineItemComment
}

type LineItemComment interface {
}

type LineItemQuant struct {
    Quant     int
    UnitPrice Money
}

type LineItemMultiBuy struct {
    Explanation string
    Discount    Money
}

type TotalLine struct {
    Text   string
    Amount Money
}

type PaymentLine struct {
    Method string
    Amount Money
}
With these in place, we can write the code that generates a receipt ready to transmit it inside the context of an APDU communication:
func tryToSendReceipt(card *smartcard.Card) {
    log.Printf("selecting receipt app on card\n")
    // select command
    apdu := []byte{0x00, 0xA4, 0x04, 0x00}
    // AID
    apdu = append(apdu, 0x10, 0xF1, 0x67, 0x6d, 0x6d, 0x61, 0x70, 0x6f, 0x77, 0x65, 0x6c, 0x6c, 0x2d, 0x61, 0x70, 0x70, 0x31)
    // Le - expected response length
    apdu = append(apdu, 0x00)
    cmd := smartcard.CommandAPDU(apdu)
    if !cmd.IsValid() {
        panic(fmt.Sprintf("invalid apdu from %v", apdu))
    }
    log.Printf("issuing cmd %s\n", cmd)
    res, err := card.TransmitAPDU(cmd)
    if err != nil {
        panic(err)
    }
    log.Printf("<< %s\n", res)
    if res == nil || len(res) != 2 {
        panic("result was not 2 bytes")
    }
    if res[0] != 0x90 || res[1] != 0x00 {
        log.Printf("response was not 0x90 0x00")
        return
    }
    store := store.AnyStore()
    if store == nil {
        log.Printf("no store found")
        return
    }
    receipt := store.MakePurchase()
    if receipt == nil {
        log.Printf("no receipt generated")
        return
    }
    transmitReceipt(card, receipt)
}

func transmitReceipt(_ *smartcard.Card, _ *receipt.Receipt) {
    fmt.Printf("transmit receipt here")
}

GO_TERMINAL_RECEIPT_TYPES:receipt_term_go/cmd/receipt_term/main.go

Where the highlighted lines choose a store, create a receipt and then transmit it.

Transmitting Receipts

From the perspective of the "main" code, transmitting a receipt consists of first translating it into a set of WireBlocks, that is, the individual messages for each line of the receipt that we are going to transmit as separate "blocks" or "pages" of information, and then sending them across using APDU.

This isn't that hard to code at the top level:
func transmitReceipt(card *smartcard.Card, send *receipt.Receipt) {
    blocks := send.AsWire()
    sender := apdu.Sender(card)
    for i, blk := range blocks {
        log.Printf("sending blk %d", i)
        err := sender.Transmit(blk)
        if err != nil {
            panic("failed to send")
        }
    }
    err := sender.Close()
    if err != nil {
        panic("failed to close")
    }
    log.Printf("finished sending receipt\n")
}

GO_TERMINAL_TRANSMIT_RECEIPT:receipt_term_go/cmd/receipt_term/main.go

Each of the blocks is a WireBlock:
package receipt

import "encoding/binary"

type WireBlock struct {
    P1, P2 byte
    Data   []byte
}

GO_TERMINAL_TRANSMIT_RECEIPT:receipt_term_go/internal/receipt/wirefmt.go

I'm not going to show the code that encodes the receipt into these blocks (it's in the receipt package if you want to look) but assume that it works in the way I described above. Each WireBlock contains the appropriate P1 and P2 values for the page and, with the caveat for the item comments I outlined above, they are all unique.

But let's have a look at the APDU BlockSender:
package apdu

import (
    "fmt"
    "log"

    "github.com/deeper-x/gopcsc/smartcard"
    "github.com/gmmapowell/ignorance/receipt_term/internal/receipt"
)

type BlockSender interface {
    Transmit(receipt.WireBlock) error
    Close() error
}

This is the interface which is used by the main code; and there is a corresponding method to create a Sender which is also used by the main code:
func Sender(card *smartcard.Card) BlockSender {
    return &ApduSender{card: card}
}
The implementing struct is ApduSender as created there:
type ApduSender struct {
    card *smartcard.Card
}
And there are two "top level" methods which have a lot in common. I'm tempted to refactor, but the differences are just too annoying:
func (sender *ApduSender) Transmit(blk receipt.WireBlock) error {
    cmd := writeBlock(blk.P1, blk.P2, blk.Data)
    reply, err := sender.card.TransmitAPDU(cmd)
    if err != nil {
        return err
    }
    if !isOK(reply) {
        return fmt.Errorf("invalid response from phone")
    }
    return nil
}

func (sender *ApduSender) Close() error {
    log.Printf("closing communication after sending receipt")
    // writing to 0,0 says "we're done".  The data is irrelevant but we need to write at least one byte
    cmd := writeBlock(0, 0, []byte{0x00})
    reply, err := sender.card.TransmitAPDU(cmd)
    if err != nil {
        return err
    }
    if !isOK(reply) {
        return fmt.Errorf("invalid response from phone")
    }
    // We could disconnect, but the main loop does that anyway
    // a.card.Disconnect()
    return nil
}

Both of these delegate the writing of the block to writeBlock and then call the smartcard library to transmit the resulting block to the phone. writeBlock thus does the heavy lifting of generating an APDU UPDATE BINARY command from the fields we have in a WireBlock:
func writeBlock(p1 byte, p2 byte, data []byte) smartcard.CommandAPDU {
    if len(data) < 1 || len(data) > 65535 {
        panic(fmt.Sprintf("cannot write block of size %d", len(data)))
    }
    apdu := []byte{0xFF, 0xD6, p1, p2}
    apdu = xxLen(apdu, len(data))
    apdu = append(apdu, data...)
    return smartcard.CommandAPDU(apdu)
}
0xFF 0xD6 is the basic command for UPDATE BINARY. The p1 and p2 are the block parameters as described above. The length of the data is then written in the XX format, which has the rules:
  • If the value is 01-FF, write one byte
  • If the value is 1000-FFFF, write a zero (which is not significant but is not a valid one byte value and thus differentiates the two cases), followed by the MSB (01-FF), followed by the LSB (00-FF)
This is encoded in xxLen:
func xxLen(apdu []byte, length int) []byte {
    if length < 256 {
        return append(apdu, byte(length))
    } else {
        return append(apdu, 0x00, byte(length/256), byte(length%256))
    }
}
Finally, the isOK method wraps up the notion of checking for an 90 00 response.
func isOK(data []byte) bool {
    if data == nil || len(data) != 2 {
        return false
    }
    return data[0] == 0x90 && data[1] == 0x00
}

GO_TERMINAL_TRANSMIT_RECEIPT:receipt_term_go/internal/apdu/sender.go

And now we can tap and see the following come out of the Go command window:
2025/01/16 13:04:09 connected to smart card
2025/01/16 13:04:09 selecting receipt app on card
2025/01/16 13:04:09 issuing cmd 00 A4 04 00 10 F1676D6D61706F77656C6C2D61707031 00
2025/01/16 13:04:09 << 9000
2025/01/16 13:04:09 sending blk 0
2025/01/16 13:04:09 sending blk 1
2025/01/16 13:04:09 sending blk 2
2025/01/16 13:04:09 sending blk 3
2025/01/16 13:04:09 sending blk 4
2025/01/16 13:04:09 sending blk 5
2025/01/16 13:04:09 sending blk 6
2025/01/16 13:04:09 sending blk 7
2025/01/16 13:04:09 sending blk 8
2025/01/16 13:04:09 sending blk 9
2025/01/16 13:04:09 sending blk 10
2025/01/16 13:04:09 sending blk 11
2025/01/16 13:04:09 sending blk 12
2025/01/16 13:04:09 sending blk 13
2025/01/16 13:04:09 sending blk 14
2025/01/16 13:04:09 sending blk 15
2025/01/16 13:04:10 sending blk 16
2025/01/16 13:04:10 sending blk 17
2025/01/16 13:04:10 sending blk 18
2025/01/16 13:04:10 closing communication after sending receipt
2025/01/16 13:04:10 finished sending receipt
Looking back over this as I check in, I can see some duplication here between what I've done in Sender and what I've done in main. I'm going to eliminate that (off-camera), especially since there is altogether too much stuff in main already.

Next time, we can improve our Android app to read and understand this information.

Conclusion

Now that we know what we're doing, and what we're trying to achieve, it's very easy to build out a server in Go. We've managed to generate a receipt and encode that in a series of APDU UPDATE BINARY commands. Now we need to read those on the Android side.

No comments:

Post a Comment