From d26937f9eb2d288b47c5c80293c9cc57aadecd38 Mon Sep 17 00:00:00 2001 From: MengMengCode <227010654+MengMengCode@users.noreply.github.com> Date: Fri, 21 Aug 2026 12:25:16 +0800 Subject: [PATCH] Fix something --- internal/vowifi/carrier_compat.go | 22 ++++--- internal/vowifi/carrier_compat_test.go | 10 ++++ internal/vowifi/carrier_profiles.json | 22 ++++++- internal/vowifi/ec20_adapter.go | 83 +++++++++++++++++++++++++- internal/vowifi/ec20_adapter_test.go | 53 ++++++++++++++++ 5 files changed, 179 insertions(+), 11 deletions(-) diff --git a/internal/vowifi/carrier_compat.go b/internal/vowifi/carrier_compat.go index 39fc4b9..90b1663 100644 --- a/internal/vowifi/carrier_compat.go +++ b/internal/vowifi/carrier_compat.go @@ -76,13 +76,13 @@ type carrierProfileDocument struct { } type carrierProfileRule struct { - ID string `json:"id"` - Match carrierProfileMatch `json:"match,omitzero"` - MatchAny []carrierProfileMatch `json:"match_any,omitempty"` - Route carrierProfileRoute `json:"route,omitzero"` - EPDG carrierProfileEPDG `json:"epdg,omitzero"` - IKE carrierProfileIKE `json:"ike,omitzero"` - IMS carrierProfileIMS `json:"ims,omitzero"` + ID string `json:"id"` + Match carrierProfileMatch `json:"match,omitzero"` + MatchAny []carrierProfileMatch `json:"match_any,omitempty"` + Route carrierProfileRoute `json:"route,omitzero"` + EPDG carrierProfileEPDG `json:"epdg,omitzero"` + IKE carrierProfileIKE `json:"ike,omitzero"` + IMS carrierProfileIMS `json:"ims,omitzero"` } type carrierProfileMatch struct { @@ -479,8 +479,12 @@ func matchCarrierProfile(match carrierProfileMatch, identity SIMIdentity) (int, }{ {name: "imsi", weight: 80, values: match.IMSIPrefixes, actual: identity.IMSI}, {name: "iccid", weight: 70, values: match.ICCIDPrefixes, actual: identity.ICCID}, - {name: "gid1", weight: 50, values: match.GID1Prefixes, actual: identity.GID1, foldCase: true}, - {name: "gid2", weight: 40, values: match.GID2Prefixes, actual: identity.GID2, foldCase: true}, + // GID values identify an MVNO/service profile within a host network and + // therefore outrank the host issuer's broad ICCID prefix. Otherwise a + // home-PLMN+ICCID AT&T rule hides RedPocket/Cricket/etc. even when the SIM + // exposes the carrier bundle's exact GID selector. + {name: "gid1", weight: 90, values: match.GID1Prefixes, actual: identity.GID1, foldCase: true}, + {name: "gid2", weight: 85, values: match.GID2Prefixes, actual: identity.GID2, foldCase: true}, } { if len(selector.values) == 0 { continue diff --git a/internal/vowifi/carrier_compat_test.go b/internal/vowifi/carrier_compat_test.go index 4b17604..05eaf26 100644 --- a/internal/vowifi/carrier_compat_test.go +++ b/internal/vowifi/carrier_compat_test.go @@ -55,6 +55,16 @@ func TestResolveCarrierProfileATT(t *testing.T) { } } +func TestResolveCarrierProfileRedPocketOutranksBroadATTICCID(t *testing.T) { + profile := ResolveCarrierProfile(SIMIdentity{ + ICCID: "8901410000000000001", IMSI: "310170000000001", + HomeMCC: "310", HomeMNC: "170", SPN: "Red Pocket", GID1: "42FFFF", + }) + if profile.ID != "ipcc-redpocket-310170" || profile.MatchSource != "hplmn+gid1" { + t.Fatalf("RedPocket profile = %#v", profile) + } +} + func TestResolveCarrierProfileStandardHasNoRegisterOverrides(t *testing.T) { profile := ResolveCarrierProfile(SIMIdentity{HomeMCC: "999", HomeMNC: "99"}) if profile.ID != CarrierProfileStandard { diff --git a/internal/vowifi/carrier_profiles.json b/internal/vowifi/carrier_profiles.json index 6b5ff63..f77b24d 100644 --- a/internal/vowifi/carrier_profiles.json +++ b/internal/vowifi/carrier_profiles.json @@ -570,6 +570,14 @@ { "id": "ipcc-redpocket-310170", "match_any": [ + { + "home_plmns": [ + "310170" + ], + "gid1_prefixes": [ + "42" + ] + }, { "home_plmns": [ "310410" @@ -585,6 +593,18 @@ "gid1_prefixes": [ "42" ] + }, + { + "home_plmns": [ + "310170", + "310410", + "310280" + ], + "spns": [ + "Red Pocket", + "RedPocket", + "Red Pocket Mobile" + ] } ], "epdg": { @@ -11783,4 +11803,4 @@ } ], "version": 1 -} \ No newline at end of file +} diff --git a/internal/vowifi/ec20_adapter.go b/internal/vowifi/ec20_adapter.go index 5bee6f0..7492a30 100644 --- a/internal/vowifi/ec20_adapter.go +++ b/internal/vowifi/ec20_adapter.go @@ -29,6 +29,7 @@ var ( const ( usimAIDPrefix = "A0000000871002" isimAIDPrefix = "A0000000871004" + efDIRFileID = 0x2f00 efADDecimal = 28589 // 0x6FAD efEHPLMNDecimal = 28441 // 0x6F19 (3GPP TS 31.102 EF_EHPLMN) channelCleanupTimeout = 3 * time.Second @@ -1025,6 +1026,19 @@ func (adapter *EC20Adapter) discoverAKAApplication( } } } + // CUAD is optional and is rejected by a number of EC20 firmware branches. + // In that case do not immediately fall back to the seven-byte registered + // application-provider prefix: cards may expose multiple USIM instances and + // require the complete PIX from EF_DIR to select the provisioned one. Read + // EF_DIR over the standards-based basic channel, which remains available on + // the same firmware that rejects CCHO/CGLA. + if discovered, discoverErr := adapter.discoverBasicApplicationAID( + ctx, + deviceID, + usimAIDPrefix, + ); discoverErr == nil { + return discovered, "USIM", nil + } // AT+CUAD is optional on older EC20 firmware. CCHO still provides a // standards-based, evidence-bearing probe of the assigned USIM AID. @@ -1053,6 +1067,64 @@ func (adapter *EC20Adapter) discoverPreferredAKAApplication( return aidPrefix, application, nil } +func (adapter *EC20Adapter) discoverBasicApplicationAID( + ctx context.Context, + deviceID string, + aidPrefix string, +) (string, error) { + selectFile := func(fileID uint16) error { + apdu := []byte{ + 0x00, 0xa4, 0x00, 0x04, 0x02, + byte(fileID >> 8), byte(fileID), 0x00, + } + raw, err := adapter.transmitBasicAPDU(ctx, deviceID, apdu, false) + if err != nil { + return err + } + _, status, err := splitAPDUStatus(raw) + if err != nil { + return err + } + if status != 0x9000 { + return fmt.Errorf("vocat: EC20 basic-channel SELECT returned %04X", status) + } + return nil + } + if err := selectFile(0x3f00); err != nil { + return "", fmt.Errorf("select EC20 MF for application discovery: %w", err) + } + if err := selectFile(efDIRFileID); err != nil { + return "", fmt.Errorf("select EC20 EF_DIR for application discovery: %w", err) + } + for record := 1; record <= 32; record++ { + raw, err := adapter.transmitBasicAPDU( + ctx, + deviceID, + []byte{0x00, 0xb2, byte(record), 0x04, 0x00}, + false, + ) + if err != nil { + return "", fmt.Errorf("read EC20 EF_DIR record %d: %w", record, err) + } + body, status, err := splitAPDUStatus(raw) + if err != nil { + return "", err + } + if status == 0x6a83 || status == 0x9402 { + break + } + if status != 0x9000 { + continue + } + for _, candidate := range collectApplicationAIDs(body) { + if strings.HasPrefix(candidate, aidPrefix) { + return candidate, nil + } + } + } + return "", ErrEC20ApplicationAbsent +} + func (adapter *EC20Adapter) openLogicalChannel( ctx context.Context, deviceID string, @@ -1174,8 +1246,17 @@ func (adapter *EC20Adapter) transmitBasicAPDU( if err != nil { return nil, err } - collected = append(collected, body...) sw1 := byte(status >> 8) + if sw1 == 0x6c { + // The UICC knows the exact response length. Retry the original APDU + // with the advised Le without retaining the procedure response. + if len(current) < 5 { + return nil, errors.New("vocat: EC20 APDU cannot apply corrected response length") + } + current[len(current)-1] = byte(status) + continue + } + collected = append(collected, body...) if sw1 != 0x61 && sw1 != 0x9f { collected = append(collected, byte(status>>8), byte(status)) return collected, nil diff --git a/internal/vowifi/ec20_adapter_test.go b/internal/vowifi/ec20_adapter_test.go index 72f14d1..bce3c67 100644 --- a/internal/vowifi/ec20_adapter_test.go +++ b/internal/vowifi/ec20_adapter_test.go @@ -263,6 +263,59 @@ func TestEC20AdapterCSIMFallbackSupportsSuccessAndSynchronizationFailure( } } +func TestEC20AdapterDiscoversFullUSIMAIDFromEFDIRWhenCUADFails(t *testing.T) { + t.Parallel() + const fullAID = "A0000000871002FFFFFFFF8903020000" + record := "61184F10" + fullAID + "50045553494D" + encodedResponse := strings.ToUpper(hex.EncodeToString(successfulUSIMResponse())) + var challenge AKAChallenge + for index := range challenge.RAND { + challenge.RAND[index] = byte(index) + challenge.AUTN[index] = byte(0xf0 + index) + } + authAPDU := buildUSIMAuthenticateAPDU(challenge) + authCommand := fmt.Sprintf( + `AT+CSIM=%d,"%s"`, + len(authAPDU)*2, + strings.ToUpper(hex.EncodeToString(authAPDU)), + ) + selectApplication := `AT+CSIM=42,"00A4040410` + fullAID + `"` + transcript := &ec20Transcript{ + t: t, + steps: append( + identityTranscriptStepsWithoutEFAD("310280000000001"), + []ec20TranscriptStep{ + {command: "AT+CCID", lines: []string{"+CCID: 8944101234567890123"}}, + {command: "AT+CUAD", err: errors.New("+CME ERROR: 13"), final: "+CME ERROR: 13"}, + {command: `AT+CSIM=16,"00A40004023F0000"`, lines: []string{`+CSIM: 4,"9000"`}}, + {command: `AT+CSIM=16,"00A40004022F0000"`, lines: []string{`+CSIM: 4,"9000"`}}, + {command: `AT+CSIM=10,"00B2010400"`, lines: []string{`+CSIM: 4,"6C1A"`}}, + {command: `AT+CSIM=10,"00B201041A"`, lines: []string{fmt.Sprintf(`+CSIM: %d,"%s9000"`, len(record)+4, record)}}, + {command: `AT+CCHO="` + fullAID + `"`, err: errors.New("unsupported"), final: "ERROR"}, + {command: selectApplication, lines: []string{`+CSIM: 4,"9000"`}}, + {command: "AT+CCID", lines: []string{"+CCID: 8944101234567890123"}}, + {command: selectApplication, lines: []string{`+CSIM: 4,"9000"`}}, + {command: authCommand, sensitive: true, lines: []string{fmt.Sprintf(`+CSIM: %d,"%s"`, len(encodedResponse), encodedResponse)}}, + }..., + ), + } + adapter, err := NewEC20Adapter(transcript, EC20AdapterOptions{}) + if err != nil { + t.Fatal(err) + } + identity, err := adapter.ReadIdentity(context.Background(), "ec20-1") + if err != nil { + t.Fatalf("ReadIdentity: %v", err) + } + if _, err := adapter.CheckReady(context.Background(), identity); err != nil { + t.Fatalf("CheckReady: %v", err) + } + if _, err := adapter.Authenticate(context.Background(), identity, challenge); err != nil { + t.Fatalf("Authenticate: %v", err) + } + transcript.assertDone() +} + func TestEC20AdapterLogicalChannelAuthenticateFollowsGetResponse( t *testing.T, ) {