Files
MengMengCode 22487dbb1f Add PC/SC backend support for USB SIM readers
- Implemented a new `unsupportedBackend` in `backend_stub.go` to handle unsupported platforms.
- Created a `Service` struct in `service.go` to manage interactions with smart cards, including session management and identity reading.
- Added methods for reading identity, checking readiness, and authenticating with USIM applications.
- Introduced a `scriptedCard` for testing purposes in `service_test.go` to simulate card responses.
- Defined necessary types and error handling in `types.go` for better clarity and usability.
- Developed a `PCSCAdapter` in `vowifi/pcsc_adapter.go` to integrate PC/SC service with Wi-Fi calling functionalities.
- Added a new SVG icon for USB SIM readers in `public/sim-reader.svg`.
- Implemented tests to ensure correct functionality and error handling in various scenarios.
2026-08-12 02:33:32 +08:00

125 lines
4.2 KiB
Go

package vowifi
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"vocat/internal/pcsc"
)
type PCSCBindingResolver func(context.Context, string) (pcsc.Selector, string, error)
// PCSCAdapter uses a directly attached USB smart-card reader as the UICC for
// Wi-Fi Calling. It deliberately exposes no cellular-radio behaviour.
type PCSCAdapter struct {
service *pcsc.Service
resolve PCSCBindingResolver
mu sync.RWMutex
bindings map[string]string
}
var (
_ SIMIdentityReader = (*PCSCAdapter)(nil)
_ SMSCenterReader = (*PCSCAdapter)(nil)
_ AKAProvider = (*PCSCAdapter)(nil)
_ RadioController = (*PCSCAdapter)(nil)
)
func NewPCSCAdapter(service *pcsc.Service, resolver PCSCBindingResolver) (*PCSCAdapter, error) {
if service == nil || resolver == nil {
return nil, errors.New("vocat: PC/SC service and reader resolver are required")
}
return &PCSCAdapter{service: service, resolve: resolver, bindings: make(map[string]string)}, nil
}
func (adapter *PCSCAdapter) ReadIdentity(ctx context.Context, deviceID string) (SIMIdentity, error) {
selector, pin, err := adapter.resolve(ctx, strings.TrimSpace(deviceID))
if err != nil {
return SIMIdentity{}, err
}
identity, err := adapter.service.ReadIdentity(ctx, selector, pin)
if err != nil {
return SIMIdentity{}, fmt.Errorf("read USB SIM identity: %w", err)
}
if len(identity.IMSI) < 5 {
return SIMIdentity{}, errors.New("vocat: USB SIM reader returned an invalid IMSI")
}
adapter.mu.Lock()
adapter.bindings[identity.ICCID] = strings.TrimSpace(deviceID)
adapter.mu.Unlock()
mncLength := identity.MNCLength
if mncLength != 2 && mncLength != 3 {
if mcc, mnc, ok := assignedHomePLMN(identity.IMSI); ok {
return SIMIdentity{ICCID: identity.ICCID, IMSI: identity.IMSI, HomeMCC: mcc, HomeMNC: mnc, SMSC: identity.SMSC}, nil
}
return SIMIdentity{}, ErrEC20MNCUnavailable
}
if len(identity.IMSI) < 3+mncLength {
return SIMIdentity{}, errors.New("vocat: USB SIM IMSI is shorter than its EF_AD home PLMN")
}
return SIMIdentity{
ICCID: identity.ICCID, IMSI: identity.IMSI,
HomeMCC: identity.IMSI[:3], HomeMNC: identity.IMSI[3 : 3+mncLength],
SMSC: identity.SMSC,
}, nil
}
func (adapter *PCSCAdapter) ReadSMSCenter(ctx context.Context, deviceID string) (string, error) {
selector, pin, err := adapter.resolve(ctx, strings.TrimSpace(deviceID))
if err != nil {
return "", err
}
identity, err := adapter.service.ReadIdentity(ctx, selector, pin)
if err != nil {
return "", err
}
if strings.TrimSpace(identity.SMSC) == "" {
return "", errors.New("vocat: USB SIM does not expose a service-centre address")
}
return identity.SMSC, nil
}
func (adapter *PCSCAdapter) CheckReady(ctx context.Context, identity SIMIdentity) (AKAEvidence, error) {
selector, pin, err := adapter.resolve(ctx, adapter.deviceID(identity))
if err != nil {
return AKAEvidence{}, err
}
aid, err := adapter.service.CheckReady(ctx, selector, identity.ICCID, pin)
if err != nil {
return AKAEvidence{}, fmt.Errorf("check USB SIM AKA application: %w", err)
}
return AKAEvidence{Ready: true, Application: aid}, nil
}
func (adapter *PCSCAdapter) deviceID(identity SIMIdentity) string {
adapter.mu.RLock()
deviceID := adapter.bindings[identity.ICCID]
adapter.mu.RUnlock()
return deviceID
}
func (adapter *PCSCAdapter) Authenticate(ctx context.Context, identity SIMIdentity, challenge AKAChallenge) (AKAResult, error) {
selector, pin, err := adapter.resolve(ctx, adapter.deviceID(identity))
if err != nil {
return AKAResult{}, err
}
result, err := adapter.service.Authenticate(ctx, selector, identity.ICCID, pin, pcsc.AKAChallenge(challenge))
if err != nil {
if errors.Is(err, pcsc.ErrAKARejected) {
return AKAResult{}, errors.Join(ErrEC20AKAMACFailure, err)
}
return AKAResult{}, fmt.Errorf("authenticate with USB SIM: %w", err)
}
return AKAResult(result), nil
}
func (*PCSCAdapter) Snapshot(context.Context, string) (RadioSnapshot, error) {
return RadioSnapshot{OperatingMode: 4, PureAirplanePolicy: true}, nil
}
func (*PCSCAdapter) StopCellularData(context.Context, string) error { return nil }
func (*PCSCAdapter) EnterVoWiFiRFOff(context.Context, string) error { return nil }
func (*PCSCAdapter) Restore(context.Context, string, RadioSnapshot) error { return nil }