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_goWe 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:
$ 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
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
- 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
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 storeThe Receipt is an assemblage of structs:
import "github.com/gmmapowell/ignorance/receipt_term/internal/receipt"
type Store interface {
MakePurchase() *receipt.Receipt
}
package receiptWith these in place, we can write the code that generates a receipt ready to transmit it inside the context of an APDU communication:
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
}
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 apduThis 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:
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
}
func Sender(card *smartcard.Card) BlockSender {The implementing struct is ApduSender as created there:
return &ApduSender{card: card}
}
type ApduSender struct {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:
card *smartcard.Card
}
func (sender *ApduSender) Transmit(blk receipt.WireBlock) error {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:
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
}
func writeBlock(p1 byte, p2 byte, data []byte) smartcard.CommandAPDU {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 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)
}
- 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)
func xxLen(apdu []byte, length int) []byte {Finally, the isOK method wraps up the notion of checking for an 90 00 response.
if length < 256 {
return append(apdu, byte(length))
} else {
return append(apdu, 0x00, byte(length/256), byte(length%256))
}
}
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 cardLooking 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.
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
Next time, we can improve our Android app to read and understand this information.
No comments:
Post a Comment