mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-13 03:13:43 +08:00
335 lines
11 KiB
Go
335 lines
11 KiB
Go
package pcsc
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"time"
|
|
)
|
|
|
|
// pcsc-lite exposes a small, versioned protocol over its local Unix socket.
|
|
// Speaking that protocol directly keeps VoCat's Linux binaries fully static;
|
|
// loading libpcsclite through dlopen would pull a glibc interpreter into an
|
|
// otherwise CGO-free build and make it unusable on musl-based routers.
|
|
const (
|
|
pcscProtocolMajor = 4
|
|
pcscProtocolCurrentMinor = 6
|
|
pcscProtocolOldestMinor = 4
|
|
|
|
pcscCmdEstablishContext = 0x01
|
|
pcscCmdReleaseContext = 0x02
|
|
pcscCmdConnect = 0x04
|
|
pcscCmdDisconnect = 0x06
|
|
pcscCmdBeginTransaction = 0x07
|
|
pcscCmdEndTransaction = 0x08
|
|
pcscCmdTransmit = 0x09
|
|
pcscCmdGetAttrib = 0x0f
|
|
pcscCmdVersion = 0x11
|
|
pcscCmdGetReadersState = 0x12
|
|
|
|
pcscScopeSystem = 0x0002
|
|
pcscProtocolT0 = 0x0001
|
|
pcscProtocolT1 = 0x0002
|
|
pcscProtocolAny = pcscProtocolT0 | pcscProtocolT1
|
|
pcscShareShared = 0x0002
|
|
pcscShareDirect = 0x0003
|
|
pcscLeaveCard = 0x0000
|
|
pcscResetCard = 0x0001
|
|
pcscCardPresent = 0x0004
|
|
pcscAttrChannelID = 0x00020110
|
|
pcscMaxReaderName = 128
|
|
pcscMaxATR = 33
|
|
pcscMaxReaders = 16
|
|
pcscReaderStateSize = 184
|
|
pcscGetSetBodySize = 280
|
|
pcscMaxAttribute = 264
|
|
pcscMaxAPDUResponse = 65548
|
|
pcscDefaultIOTimeout = 30 * time.Second
|
|
pcscSuccess = uint32(0)
|
|
pcscNoSmartcard = uint32(0x8010000c)
|
|
pcscNoService = uint32(0x8010001d)
|
|
pcscServiceStopped = uint32(0x8010001e)
|
|
pcscNoReaders = uint32(0x8010002e)
|
|
)
|
|
|
|
type pcscdClient struct {
|
|
conn net.Conn
|
|
contextID uint32
|
|
serverMinor int32
|
|
}
|
|
|
|
type pcscdReaderState struct {
|
|
name string
|
|
state uint32
|
|
atr []byte
|
|
protocol uint32
|
|
}
|
|
|
|
func establishPCSCD(ctx context.Context, conn net.Conn) (*pcscdClient, error) {
|
|
client := &pcscdClient{conn: conn}
|
|
version := make([]byte, 12)
|
|
binary.LittleEndian.PutUint32(version[0:4], pcscProtocolMajor)
|
|
binary.LittleEndian.PutUint32(version[4:8], pcscProtocolCurrentMinor)
|
|
for {
|
|
if err := client.exchange(ctx, pcscCmdVersion, version); err != nil {
|
|
return nil, err
|
|
}
|
|
major := int32(binary.LittleEndian.Uint32(version[0:4]))
|
|
client.serverMinor = int32(binary.LittleEndian.Uint32(version[4:8]))
|
|
rv := binary.LittleEndian.Uint32(version[8:12])
|
|
if rv == pcscSuccess {
|
|
break
|
|
}
|
|
if rv != pcscServiceStopped || major != pcscProtocolMajor || client.serverMinor < pcscProtocolOldestMinor || client.serverMinor >= pcscProtocolCurrentMinor {
|
|
return nil, pcscError("negotiate protocol", rv)
|
|
}
|
|
// pcsc-lite answers a newer client's first probe with its own
|
|
// compatible minor version. Retry on the same connection with that
|
|
// value, matching libpcsclite's official fallback behavior.
|
|
binary.LittleEndian.PutUint32(version[0:4], pcscProtocolMajor)
|
|
binary.LittleEndian.PutUint32(version[4:8], uint32(client.serverMinor))
|
|
binary.LittleEndian.PutUint32(version[8:12], pcscSuccess)
|
|
}
|
|
if client.serverMinor < pcscProtocolOldestMinor {
|
|
return nil, fmt.Errorf("pcsc: unsupported pcscd protocol %d.%d", pcscProtocolMajor, client.serverMinor)
|
|
}
|
|
body := make([]byte, 12)
|
|
binary.LittleEndian.PutUint32(body[0:4], pcscScopeSystem)
|
|
if err := client.exchange(ctx, pcscCmdEstablishContext, body); err != nil {
|
|
return nil, err
|
|
}
|
|
if rv := binary.LittleEndian.Uint32(body[8:12]); rv != pcscSuccess {
|
|
return nil, pcscError("establish context", rv)
|
|
}
|
|
client.contextID = binary.LittleEndian.Uint32(body[4:8])
|
|
return client, nil
|
|
}
|
|
|
|
func (client *pcscdClient) exchange(ctx context.Context, command uint32, body []byte) error {
|
|
if client == nil || client.conn == nil {
|
|
return errors.New("pcsc: pcscd connection is closed")
|
|
}
|
|
if err := client.setDeadline(ctx); err != nil {
|
|
return err
|
|
}
|
|
header := make([]byte, 8)
|
|
binary.LittleEndian.PutUint32(header[0:4], uint32(len(body)))
|
|
binary.LittleEndian.PutUint32(header[4:8], command)
|
|
if err := writeAll(client.conn, header); err != nil {
|
|
return fmt.Errorf("pcsc: send command %02x: %w", command, err)
|
|
}
|
|
if len(body) > 0 {
|
|
if err := writeAll(client.conn, body); err != nil {
|
|
return fmt.Errorf("pcsc: send command body %02x: %w", command, err)
|
|
}
|
|
if _, err := io.ReadFull(client.conn, body); err != nil {
|
|
return fmt.Errorf("pcsc: receive command %02x: %w", command, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (client *pcscdClient) send(ctx context.Context, command uint32, body, extra []byte) error {
|
|
if client == nil || client.conn == nil {
|
|
return errors.New("pcsc: pcscd connection is closed")
|
|
}
|
|
if err := client.setDeadline(ctx); err != nil {
|
|
return err
|
|
}
|
|
header := make([]byte, 8)
|
|
binary.LittleEndian.PutUint32(header[0:4], uint32(len(body)))
|
|
binary.LittleEndian.PutUint32(header[4:8], command)
|
|
if err := writeAll(client.conn, header); err != nil {
|
|
return err
|
|
}
|
|
if err := writeAll(client.conn, body); err != nil {
|
|
return err
|
|
}
|
|
return writeAll(client.conn, extra)
|
|
}
|
|
|
|
func (client *pcscdClient) setDeadline(ctx context.Context) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
deadline := time.Now().Add(pcscDefaultIOTimeout)
|
|
if value, ok := ctx.Deadline(); ok && value.Before(deadline) {
|
|
deadline = value
|
|
}
|
|
return client.conn.SetDeadline(deadline)
|
|
}
|
|
|
|
func (client *pcscdClient) readers(ctx context.Context) ([]pcscdReaderState, error) {
|
|
if err := client.setDeadline(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
header := make([]byte, 8)
|
|
binary.LittleEndian.PutUint32(header[4:8], pcscCmdGetReadersState)
|
|
if err := writeAll(client.conn, header); err != nil {
|
|
return nil, fmt.Errorf("pcsc: request reader states: %w", err)
|
|
}
|
|
raw := make([]byte, pcscMaxReaders*pcscReaderStateSize)
|
|
if _, err := io.ReadFull(client.conn, raw); err != nil {
|
|
return nil, fmt.Errorf("pcsc: read reader states: %w", err)
|
|
}
|
|
result := make([]pcscdReaderState, 0, pcscMaxReaders)
|
|
for offset := 0; offset < len(raw); offset += pcscReaderStateSize {
|
|
state := raw[offset : offset+pcscReaderStateSize]
|
|
name := cString(state[:pcscMaxReaderName])
|
|
if name == "" {
|
|
continue
|
|
}
|
|
atrLen := int(binary.LittleEndian.Uint32(state[176:180]))
|
|
if atrLen < 0 || atrLen > pcscMaxATR {
|
|
atrLen = 0
|
|
}
|
|
result = append(result, pcscdReaderState{
|
|
name: name,
|
|
state: binary.LittleEndian.Uint32(state[132:136]),
|
|
atr: append([]byte(nil), state[140:140+atrLen]...),
|
|
protocol: binary.LittleEndian.Uint32(state[180:184]),
|
|
})
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (client *pcscdClient) connect(ctx context.Context, reader string, share, protocols uint32) (int32, uint32, error) {
|
|
if len(reader) >= pcscMaxReaderName {
|
|
return 0, 0, errors.New("pcsc: reader name is too long")
|
|
}
|
|
body := make([]byte, 152)
|
|
binary.LittleEndian.PutUint32(body[0:4], client.contextID)
|
|
copy(body[4:132], reader)
|
|
binary.LittleEndian.PutUint32(body[132:136], share)
|
|
binary.LittleEndian.PutUint32(body[136:140], protocols)
|
|
if err := client.exchange(ctx, pcscCmdConnect, body); err != nil {
|
|
return 0, 0, err
|
|
}
|
|
if rv := binary.LittleEndian.Uint32(body[148:152]); rv != pcscSuccess {
|
|
return 0, 0, pcscError("connect reader", rv)
|
|
}
|
|
return int32(binary.LittleEndian.Uint32(body[140:144])), binary.LittleEndian.Uint32(body[144:148]), nil
|
|
}
|
|
|
|
func (client *pcscdClient) simpleCardCommand(ctx context.Context, command uint32, card int32, disposition *uint32) error {
|
|
size := 8
|
|
if disposition != nil {
|
|
size = 12
|
|
}
|
|
body := make([]byte, size)
|
|
binary.LittleEndian.PutUint32(body[0:4], uint32(card))
|
|
if disposition != nil {
|
|
binary.LittleEndian.PutUint32(body[4:8], *disposition)
|
|
}
|
|
if err := client.exchange(ctx, command, body); err != nil {
|
|
return err
|
|
}
|
|
if rv := binary.LittleEndian.Uint32(body[size-4:]); rv != pcscSuccess {
|
|
return pcscError("card command", rv)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (client *pcscdClient) transmit(ctx context.Context, card int32, protocol uint32, command []byte) ([]byte, error) {
|
|
body := make([]byte, 32)
|
|
binary.LittleEndian.PutUint32(body[0:4], uint32(card))
|
|
binary.LittleEndian.PutUint32(body[4:8], protocol)
|
|
binary.LittleEndian.PutUint32(body[8:12], 8)
|
|
binary.LittleEndian.PutUint32(body[12:16], uint32(len(command)))
|
|
binary.LittleEndian.PutUint32(body[16:20], pcscProtocolAny)
|
|
binary.LittleEndian.PutUint32(body[20:24], 8)
|
|
binary.LittleEndian.PutUint32(body[24:28], pcscMaxAPDUResponse)
|
|
if err := client.send(ctx, pcscCmdTransmit, body, command); err != nil {
|
|
return nil, fmt.Errorf("pcsc: transmit APDU: %w", err)
|
|
}
|
|
if _, err := io.ReadFull(client.conn, body); err != nil {
|
|
return nil, fmt.Errorf("pcsc: receive APDU result: %w", err)
|
|
}
|
|
if rv := binary.LittleEndian.Uint32(body[28:32]); rv != pcscSuccess {
|
|
return nil, pcscError("transmit APDU", rv)
|
|
}
|
|
length := binary.LittleEndian.Uint32(body[24:28])
|
|
if length > pcscMaxAPDUResponse {
|
|
return nil, errors.New("pcsc: pcscd returned an oversized APDU")
|
|
}
|
|
response := make([]byte, length)
|
|
if _, err := io.ReadFull(client.conn, response); err != nil {
|
|
return nil, fmt.Errorf("pcsc: receive APDU: %w", err)
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
func (client *pcscdClient) getAttrib(ctx context.Context, card int32, attribute uint32) ([]byte, error) {
|
|
body := make([]byte, pcscGetSetBodySize)
|
|
binary.LittleEndian.PutUint32(body[0:4], uint32(card))
|
|
binary.LittleEndian.PutUint32(body[4:8], attribute)
|
|
binary.LittleEndian.PutUint32(body[272:276], pcscMaxAttribute)
|
|
if err := client.exchange(ctx, pcscCmdGetAttrib, body); err != nil {
|
|
return nil, err
|
|
}
|
|
if rv := binary.LittleEndian.Uint32(body[276:280]); rv != pcscSuccess {
|
|
return nil, pcscError("get reader attribute", rv)
|
|
}
|
|
length := binary.LittleEndian.Uint32(body[272:276])
|
|
if length > pcscMaxAttribute {
|
|
return nil, errors.New("pcsc: pcscd returned an oversized attribute")
|
|
}
|
|
return append([]byte(nil), body[8:8+length]...), nil
|
|
}
|
|
|
|
func (client *pcscdClient) closeContext(ctx context.Context) error {
|
|
if client == nil || client.conn == nil {
|
|
return nil
|
|
}
|
|
body := make([]byte, 8)
|
|
binary.LittleEndian.PutUint32(body[0:4], client.contextID)
|
|
err := client.exchange(ctx, pcscCmdReleaseContext, body)
|
|
if err == nil {
|
|
if rv := binary.LittleEndian.Uint32(body[4:8]); rv != pcscSuccess {
|
|
err = pcscError("release context", rv)
|
|
}
|
|
}
|
|
closeErr := client.conn.Close()
|
|
client.conn = nil
|
|
return errors.Join(err, closeErr)
|
|
}
|
|
|
|
func pcscError(operation string, code uint32) error {
|
|
switch code {
|
|
case pcscNoSmartcard:
|
|
return ErrNoCard
|
|
case pcscNoService, pcscServiceStopped:
|
|
return fmt.Errorf("%w: %s failed with PC/SC status %08X", ErrUnavailable, operation, code)
|
|
case pcscNoReaders:
|
|
return ErrReaderNotFound
|
|
default:
|
|
return fmt.Errorf("pcsc: %s failed with status %08X", operation, code)
|
|
}
|
|
}
|
|
|
|
func cString(value []byte) string {
|
|
for index, current := range value {
|
|
if current == 0 {
|
|
return string(value[:index])
|
|
}
|
|
}
|
|
return string(value)
|
|
}
|
|
|
|
func writeAll(writer io.Writer, value []byte) error {
|
|
for len(value) > 0 {
|
|
written, err := writer.Write(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if written == 0 {
|
|
return io.ErrUnexpectedEOF
|
|
}
|
|
value = value[written:]
|
|
}
|
|
return nil
|
|
}
|