mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-13 03:13:43 +08:00
239 lines
7.0 KiB
Go
239 lines
7.0 KiB
Go
//go:build linux && (amd64 || arm64)
|
|
|
|
package pcsc
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type nativeBackend struct{}
|
|
|
|
func newNativeBackend() Backend { return &nativeBackend{} }
|
|
|
|
func (backend *nativeBackend) dial(ctx context.Context) (*pcscdClient, error) {
|
|
paths := []string{strings.TrimSpace(os.Getenv("PCSCLITE_CSOCK_NAME")), "/run/pcscd/pcscd.comm", "/var/run/pcscd/pcscd.comm"}
|
|
var failures []error
|
|
seen := make(map[string]bool)
|
|
for _, path := range paths {
|
|
if path == "" || seen[path] {
|
|
continue
|
|
}
|
|
seen[path] = true
|
|
conn, err := (&net.Dialer{Timeout: 5 * time.Second}).DialContext(ctx, "unix", path)
|
|
if err != nil {
|
|
failures = append(failures, err)
|
|
continue
|
|
}
|
|
client, err := establishPCSCD(ctx, conn)
|
|
if err == nil {
|
|
return client, nil
|
|
}
|
|
_ = conn.Close()
|
|
failures = append(failures, err)
|
|
}
|
|
return nil, fmt.Errorf("%w: pcscd socket is not reachable: %w", ErrUnavailable, errors.Join(failures...))
|
|
}
|
|
|
|
func (backend *nativeBackend) Readers(ctx context.Context) ([]Reader, error) {
|
|
client, err := backend.dial(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer client.closeContext(context.Background())
|
|
states, err := client.readers(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
readers := make([]Reader, 0, len(states))
|
|
for _, state := range states {
|
|
reader := Reader{
|
|
Name: state.name,
|
|
CardPresent: state.state&pcscCardPresent != 0,
|
|
ATR: strings.ToUpper(hex.EncodeToString(state.atr)),
|
|
}
|
|
if path, ok := backend.readerUSBPath(ctx, client, state.name); ok {
|
|
reader.USBPath = path
|
|
reader.VendorID = readSysfsText(path, "idVendor")
|
|
reader.ProductID = readSysfsText(path, "idProduct")
|
|
reader.Manufacturer = readSysfsText(path, "manufacturer")
|
|
reader.Product = readSysfsText(path, "product")
|
|
} else {
|
|
reader.USBPath = "pcsc:" + state.name
|
|
}
|
|
if reader.Product == "" {
|
|
reader.Product = strings.TrimSpace(strings.TrimSuffix(state.name, " 00 00"))
|
|
}
|
|
readers = append(readers, reader)
|
|
}
|
|
return readers, nil
|
|
}
|
|
|
|
func (backend *nativeBackend) readerUSBPath(ctx context.Context, client *pcscdClient, name string) (string, bool) {
|
|
card, _, err := client.connect(ctx, name, pcscShareDirect, 0)
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
disposition := uint32(pcscLeaveCard)
|
|
defer client.simpleCardCommand(context.Background(), pcscCmdDisconnect, card, &disposition)
|
|
attribute, err := client.getAttrib(ctx, card, pcscAttrChannelID)
|
|
if err != nil || len(attribute) < 4 {
|
|
return "", false
|
|
}
|
|
channel := binary.LittleEndian.Uint32(attribute[:4])
|
|
if channel>>16 != 0x0020 {
|
|
return "", false
|
|
}
|
|
bus, device := int((channel>>8)&0xff), int(channel&0xff)
|
|
entries, err := os.ReadDir("/sys/bus/usb/devices")
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() && entry.Type()&os.ModeSymlink == 0 {
|
|
continue
|
|
}
|
|
path := filepath.Join("/sys/bus/usb/devices", entry.Name())
|
|
entryBus, busErr := readSysfsInt(path, "busnum")
|
|
entryDevice, deviceErr := readSysfsInt(path, "devnum")
|
|
if busErr == nil && deviceErr == nil && entryBus == bus && entryDevice == device {
|
|
return entry.Name(), true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func (backend *nativeBackend) Open(ctx context.Context, selector Selector) (Card, error) {
|
|
readers, err := backend.Readers(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
reader, ok := matchReader(readers, selector)
|
|
if !ok {
|
|
return nil, ErrReaderNotFound
|
|
}
|
|
if !reader.CardPresent {
|
|
return nil, ErrNoCard
|
|
}
|
|
client, err := backend.dial(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
handle, protocol, err := client.connect(ctx, reader.Name, pcscShareShared, pcscProtocolAny)
|
|
if err != nil {
|
|
_ = client.closeContext(context.Background())
|
|
return nil, err
|
|
}
|
|
if err := client.simpleCardCommand(ctx, pcscCmdBeginTransaction, handle, nil); err != nil {
|
|
disposition := uint32(pcscLeaveCard)
|
|
_ = client.simpleCardCommand(context.Background(), pcscCmdDisconnect, handle, &disposition)
|
|
_ = client.closeContext(context.Background())
|
|
return nil, fmt.Errorf("pcsc: begin card transaction: %w", err)
|
|
}
|
|
return &nativeCard{client: client, handle: handle, protocol: protocol}, nil
|
|
}
|
|
|
|
type nativeCard struct {
|
|
client *pcscdClient
|
|
handle int32
|
|
protocol uint32
|
|
closed bool
|
|
}
|
|
|
|
func (card *nativeCard) Transmit(ctx context.Context, command []byte) ([]byte, uint16, error) {
|
|
if card == nil || card.client == nil || card.closed {
|
|
return nil, 0, errors.New("pcsc: card session is closed")
|
|
}
|
|
return card.transmit(ctx, append([]byte(nil), command...), 0)
|
|
}
|
|
|
|
func (card *nativeCard) TransmitRaw(ctx context.Context, command []byte) ([]byte, uint16, error) {
|
|
if card == nil || card.client == nil || card.closed {
|
|
return nil, 0, errors.New("pcsc: card session is closed")
|
|
}
|
|
response, err := card.client.transmit(ctx, card.handle, card.protocol, append([]byte(nil), command...))
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if len(response) < 2 {
|
|
return nil, 0, errors.New("pcsc: APDU response omitted its status word")
|
|
}
|
|
last := len(response) - 2
|
|
return append([]byte(nil), response[:last]...), uint16(response[last])<<8 | uint16(response[last+1]), nil
|
|
}
|
|
|
|
func (card *nativeCard) transmit(ctx context.Context, command []byte, depth int) ([]byte, uint16, error) {
|
|
if depth > 8 {
|
|
return nil, 0, errors.New("pcsc: too many APDU continuations")
|
|
}
|
|
data, status, err := card.TransmitRaw(ctx, command)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
sw1, sw2 := byte(status>>8), byte(status)
|
|
if sw1 == 0x6c && len(command) >= 5 {
|
|
retry := append([]byte(nil), command...)
|
|
retry[len(retry)-1] = sw2
|
|
return card.transmit(ctx, retry, depth+1)
|
|
}
|
|
if sw1 == 0x61 || sw1 == 0x9f {
|
|
more, sw, err := card.transmit(ctx, []byte{0x00, 0xc0, 0x00, 0x00, sw2}, depth+1)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return append(data, more...), sw, nil
|
|
}
|
|
return data, status, ctx.Err()
|
|
}
|
|
|
|
func (card *nativeCard) Close() error { return card.close(pcscLeaveCard) }
|
|
|
|
func (card *nativeCard) CloseWithReset() error { return card.close(pcscResetCard) }
|
|
|
|
func (card *nativeCard) close(disposition uint32) error {
|
|
if card == nil || card.closed {
|
|
return nil
|
|
}
|
|
card.closed = true
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
var result []error
|
|
if card.client != nil {
|
|
if err := card.client.simpleCardCommand(ctx, pcscCmdEndTransaction, card.handle, &disposition); err != nil {
|
|
result = append(result, err)
|
|
}
|
|
if err := card.client.simpleCardCommand(ctx, pcscCmdDisconnect, card.handle, &disposition); err != nil {
|
|
result = append(result, err)
|
|
}
|
|
if err := card.client.closeContext(ctx); err != nil {
|
|
result = append(result, err)
|
|
}
|
|
}
|
|
return errors.Join(result...)
|
|
}
|
|
|
|
func readSysfsText(usbPath, name string) string {
|
|
value, err := os.ReadFile(filepath.Join("/sys/bus/usb/devices", usbPath, name))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(string(value))
|
|
}
|
|
|
|
func readSysfsInt(path, name string) (int, error) {
|
|
value, err := os.ReadFile(filepath.Join(path, name))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return strconv.Atoi(strings.TrimSpace(string(value)))
|
|
}
|