mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-13 03:13:43 +08:00
- 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.
125 lines
4.2 KiB
Go
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 }
|