| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588 |
- package vowifi
- import (
- "bytes"
- "context"
- "encoding/hex"
- "errors"
- "fmt"
- "strings"
- "sync"
- "testing"
- "vocat/internal/modem"
- )
- type ec20TranscriptStep struct {
- command string
- sensitive bool
- lines []string
- final string
- err error
- }
- type ec20Transcript struct {
- t *testing.T
- mu sync.Mutex
- steps []ec20TranscriptStep
- next int
- }
- func TestICCIDIdentifierStripsBCDPadding(t *testing.T) {
- for _, test := range []struct {
- wire string
- want string
- }{
- {wire: "8944110069353447454F", want: "8944110069353447454"},
- {wire: "894921007608519523FF", want: "894921007608519523"},
- } {
- response := modem.Response{Lines: []string{"+QCCID: " + test.wire}}
- if got := iccidIdentifier(response, []string{"+CCID:", "+QCCID:"}, 18, 22); got != test.want {
- t.Fatalf("iccidIdentifier(%q) = %q, want %q", test.wire, got, test.want)
- }
- }
- }
- func (transcript *ec20Transcript) ExecuteAT(
- _ context.Context,
- _ string,
- command string,
- ) (modem.Response, error) {
- return transcript.execute(command, false)
- }
- func (transcript *ec20Transcript) ExecuteSensitiveAT(
- _ context.Context,
- _ string,
- command string,
- ) (modem.Response, error) {
- return transcript.execute(command, true)
- }
- func (transcript *ec20Transcript) execute(
- command string,
- sensitive bool,
- ) (modem.Response, error) {
- transcript.t.Helper()
- transcript.mu.Lock()
- defer transcript.mu.Unlock()
- if transcript.next >= len(transcript.steps) {
- transcript.t.Fatalf("unexpected EC20 command %q", command)
- }
- step := transcript.steps[transcript.next]
- transcript.next++
- if command != step.command {
- transcript.t.Fatalf(
- "EC20 command %d = %q, want %q",
- transcript.next,
- command,
- step.command,
- )
- }
- if sensitive != step.sensitive {
- transcript.t.Fatalf(
- "EC20 command %q sensitive=%v, want %v",
- command,
- sensitive,
- step.sensitive,
- )
- }
- final := step.final
- if final == "" && step.err == nil {
- final = "OK"
- }
- return modem.Response{
- Command: command,
- Lines: append([]string(nil), step.lines...),
- Final: final,
- }, step.err
- }
- func (transcript *ec20Transcript) assertDone() {
- transcript.t.Helper()
- transcript.mu.Lock()
- defer transcript.mu.Unlock()
- if transcript.next != len(transcript.steps) {
- transcript.t.Fatalf(
- "consumed %d/%d EC20 transcript steps",
- transcript.next,
- len(transcript.steps),
- )
- }
- }
- func TestEC20AdapterCSIMFallbackSupportsSuccessAndSynchronizationFailure(
- t *testing.T,
- ) {
- t.Parallel()
- tests := []struct {
- name string
- apdu []byte
- wantErr error
- assert func(*testing.T, AKAResult)
- }{
- {
- name: "success",
- apdu: successfulUSIMResponse(),
- assert: func(t *testing.T, result AKAResult) {
- t.Helper()
- if result.SynchronizationFailure {
- t.Fatal("successful result was marked as synchronization failure")
- }
- if !bytes.Equal(result.RES, []byte{1, 2, 3, 4, 5, 6, 7, 8}) {
- t.Fatalf("RES = %x", result.RES)
- }
- if len(result.CK) != 16 || len(result.IK) != 16 {
- t.Fatalf("CK/IK lengths = %d/%d", len(result.CK), len(result.IK))
- }
- },
- },
- {
- name: "synchronization_failure",
- apdu: synchronizationFailureUSIMResponse(),
- assert: func(t *testing.T, result AKAResult) {
- t.Helper()
- if !result.SynchronizationFailure {
- t.Fatal("AUTS result was not marked as synchronization failure")
- }
- if len(result.AUTS) != 14 {
- t.Fatalf("AUTS length = %d", len(result.AUTS))
- }
- if len(result.RES) != 0 || len(result.CK) != 0 || len(result.IK) != 0 {
- t.Fatal("synchronization failure exposed a success vector")
- }
- },
- },
- {
- name: "mac_failure_9862",
- apdu: []byte{0x98, 0x62},
- wantErr: ErrEC20AKAMACFailure,
- },
- }
- for _, test := range tests {
- test := test
- t.Run(test.name, func(t *testing.T) {
- t.Parallel()
- 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)),
- )
- encodedResponse := strings.ToUpper(hex.EncodeToString(test.apdu))
- transcript := &ec20Transcript{
- t: t,
- steps: append(
- identityTranscriptSteps("234150123456789"),
- ec20TranscriptStep{
- command: "AT+CCID",
- lines: []string{"+CCID: 8944101234567890123"},
- },
- ec20TranscriptStep{
- command: "AT+CUAD",
- lines: []string{
- `+CUAD: 22,"61094F07A0000000871002"`,
- },
- },
- ec20TranscriptStep{
- command: `AT+CCHO="A0000000871002"`,
- err: errors.New("unsupported"),
- final: "ERROR",
- },
- // The SELECT response requests GET RESPONSE. This is the
- // behavior observed on EC20 basic-channel firmware.
- ec20TranscriptStep{
- command: `AT+CSIM=24,"00A4040407A0000000871002"`,
- lines: []string{`+CSIM: 4,"613A"`},
- },
- ec20TranscriptStep{
- command: `AT+CSIM=10,"00C000003A"`,
- lines: []string{`+CSIM: 4,"9000"`},
- },
- ec20TranscriptStep{
- command: "AT+CCID",
- lines: []string{"+CCID: 8944101234567890123"},
- },
- ec20TranscriptStep{
- command: `AT+CSIM=24,"00A4040407A0000000871002"`,
- lines: []string{`+CSIM: 4,"9000"`},
- },
- ec20TranscriptStep{
- 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)
- }
- evidence, err := adapter.CheckReady(context.Background(), identity)
- if err != nil {
- t.Fatalf("CheckReady: %v", err)
- }
- if !evidence.Ready || evidence.Application != "USIM" {
- t.Fatalf("AKA evidence = %#v", evidence)
- }
- result, err := adapter.Authenticate(
- context.Background(),
- identity,
- challenge,
- )
- if test.wantErr != nil {
- if !errors.Is(err, test.wantErr) {
- t.Fatalf("Authenticate error = %v, want %v", err, test.wantErr)
- }
- transcript.assertDone()
- return
- }
- if err != nil {
- t.Fatalf("Authenticate: %v", err)
- }
- test.assert(t, result)
- transcript.assertDone()
- })
- }
- }
- func TestEC20AdapterLogicalChannelAuthenticateFollowsGetResponse(
- t *testing.T,
- ) {
- t.Parallel()
- 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+CGLA=1,%d,"%s"`,
- len(authAPDU)*2,
- strings.ToUpper(hex.EncodeToString(authAPDU)),
- )
- chainedResponse := logicalChainedUSIMResponse()
- if len(chainedResponse)-2 != 0x35 {
- t.Fatalf(
- "test response body length = %d, want 0x35",
- len(chainedResponse)-2,
- )
- }
- encodedResponse := strings.ToUpper(hex.EncodeToString(chainedResponse))
- transcript := &ec20Transcript{
- t: t,
- steps: append(
- identityTranscriptSteps("234150123456789"),
- ec20TranscriptStep{
- command: "AT+CCID",
- lines: []string{"+CCID: 8944101234567890123"},
- },
- ec20TranscriptStep{
- command: "AT+CUAD",
- lines: []string{
- `+CUAD: 22,"61094F07A0000000871002"`,
- },
- },
- ec20TranscriptStep{
- command: `AT+CCHO="A0000000871002"`,
- lines: []string{"+CCHO: 1"},
- },
- ec20TranscriptStep{command: "AT+CCHC=1"},
- ec20TranscriptStep{
- command: "AT+CCID",
- lines: []string{"+CCID: 8944101234567890123"},
- },
- ec20TranscriptStep{
- command: `AT+CCHO="A0000000871002"`,
- lines: []string{"+CCHO: 1"},
- },
- ec20TranscriptStep{
- command: authCommand,
- sensitive: true,
- lines: []string{`+CGLA: 4,"6135"`},
- },
- ec20TranscriptStep{
- command: `AT+CGLA=1,10,"00C0000035"`,
- sensitive: true,
- lines: []string{fmt.Sprintf(
- `+CGLA: %d,"%s"`,
- len(encodedResponse),
- encodedResponse,
- )},
- },
- ec20TranscriptStep{command: "AT+CCHC=1"},
- ),
- }
- 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)
- }
- result, err := adapter.Authenticate(
- context.Background(),
- identity,
- challenge,
- )
- if err != nil {
- t.Fatalf("Authenticate: %v", err)
- }
- if !bytes.Equal(result.RES, []byte{1, 2, 3, 4, 5, 6, 7, 8}) ||
- len(result.CK) != 16 ||
- len(result.IK) != 16 {
- t.Fatalf(
- "AKA result RES=%x CK=%d IK=%d",
- result.RES,
- len(result.CK),
- len(result.IK),
- )
- }
- transcript.assertDone()
- }
- func TestEC20AdapterReadsExplicitHomePLMNAndKnownAssignmentFallback(
- t *testing.T,
- ) {
- t.Parallel()
- tests := []struct {
- name string
- steps []ec20TranscriptStep
- }{
- {
- name: "EF_AD",
- steps: identityTranscriptSteps("234150123456789"),
- },
- {
- name: "assigned HPLMN when EF_AD omits MNC length",
- steps: append(
- identityTranscriptStepsWithoutEFAD("234150123456789"),
- ec20TranscriptStep{
- command: "AT+CRSM=176,28589,0,0,4",
- err: errors.New("not available"),
- final: "ERROR",
- },
- ec20TranscriptStep{
- command: "AT+CRSM=176,28589,0,0,0",
- err: errors.New("not available"),
- final: "ERROR",
- },
- ),
- },
- }
- for _, test := range tests {
- test := test
- t.Run(test.name, func(t *testing.T) {
- t.Parallel()
- transcript := &ec20Transcript{t: t, steps: test.steps}
- 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 identity.HomeMCC != "234" || identity.HomeMNC != "15" {
- t.Fatalf(
- "home PLMN = %s/%s, want 234/15",
- identity.HomeMCC,
- identity.HomeMNC,
- )
- }
- transcript.assertDone()
- })
- }
- }
- func TestEC20AdapterRadioTransactionRestoresCFUNAndPDPContexts(
- t *testing.T,
- ) {
- t.Parallel()
- transcript := &ec20Transcript{
- t: t,
- steps: []ec20TranscriptStep{
- {command: "AT+CFUN?", lines: []string{"+CFUN: 1"}},
- {
- command: "AT+CGACT?",
- lines: []string{"+CGACT: 1,1", "+CGACT: 2,0"},
- },
- {command: "AT+CFUN?", lines: []string{"+CFUN: 1"}},
- {command: "AT+CFUN=4"},
- {command: "AT+CFUN?", lines: []string{"+CFUN: 4"}},
- {
- command: "AT+CGACT?",
- lines: []string{"+CGACT: 1,0", "+CGACT: 2,0"},
- },
- {
- command: "AT+CGACT?",
- lines: []string{"+CGACT: 1,0", "+CGACT: 2,0"},
- },
- {command: "AT+CFUN?", lines: []string{"+CFUN: 4"}},
- {command: "AT+CFUN=1"},
- {command: "AT+CFUN?", lines: []string{"+CFUN: 1"}},
- {
- command: "AT+CGACT?",
- lines: []string{"+CGACT: 1,0", "+CGACT: 2,0"},
- },
- {command: "AT+CGACT=1,1"},
- {
- command: "AT+CGACT?",
- lines: []string{"+CGACT: 1,1", "+CGACT: 2,0"},
- },
- },
- }
- adapter, err := NewEC20Adapter(transcript, EC20AdapterOptions{
- PureAirplanePolicy: func(string) bool { return true },
- RestoreCellularData: true,
- })
- if err != nil {
- t.Fatal(err)
- }
- snapshot, err := adapter.Snapshot(context.Background(), "ec20-1")
- if err != nil {
- t.Fatalf("Snapshot: %v", err)
- }
- if !snapshot.CellularDataEnabled ||
- snapshot.OperatingMode != 1 ||
- !snapshot.PureAirplanePolicy {
- t.Fatalf("snapshot = %#v", snapshot)
- }
- if err := adapter.EnterVoWiFiRFOff(
- context.Background(),
- "ec20-1",
- ); err != nil {
- t.Fatalf("EnterVoWiFiRFOff: %v", err)
- }
- if err := adapter.StopCellularData(
- context.Background(),
- "ec20-1",
- ); err != nil {
- t.Fatalf("StopCellularData: %v", err)
- }
- if err := adapter.Restore(
- context.Background(),
- "ec20-1",
- snapshot,
- ); err != nil {
- t.Fatalf("Restore: %v", err)
- }
- transcript.assertDone()
- }
- func TestEC20AdapterNeverStartsCellularDataByDefault(t *testing.T) {
- t.Parallel()
- transcript := &ec20Transcript{
- t: t,
- steps: []ec20TranscriptStep{
- {command: "AT+CFUN?", lines: []string{"+CFUN: 1"}},
- {command: "AT+CGACT?", lines: []string{"+CGACT: 1,1"}},
- {command: "AT+CFUN?", lines: []string{"+CFUN: 1"}},
- {command: "AT+CFUN?", lines: []string{"+CFUN: 1"}},
- {command: "AT+CGACT?", lines: []string{"+CGACT: 1,1"}},
- {command: "AT+CGACT=0,1"},
- {command: "AT+CGACT?", lines: []string{"+CGACT: 1,0"}},
- },
- }
- adapter, err := NewEC20Adapter(transcript, EC20AdapterOptions{})
- if err != nil {
- t.Fatal(err)
- }
- snapshot, err := adapter.Snapshot(context.Background(), "ec20-1")
- if err != nil {
- t.Fatalf("Snapshot: %v", err)
- }
- if err := adapter.Restore(context.Background(), "ec20-1", snapshot); err != nil {
- t.Fatalf("Restore: %v", err)
- }
- transcript.assertDone()
- }
- func identityTranscriptSteps(imsi string) []ec20TranscriptStep {
- return append(
- identityTranscriptStepsWithoutEFAD(imsi),
- ec20TranscriptStep{
- command: "AT+CRSM=176,28589,0,0,4",
- lines: []string{`+CRSM: 144,0,"00000002"`},
- },
- )
- }
- func identityTranscriptStepsWithoutEFAD(imsi string) []ec20TranscriptStep {
- return []ec20TranscriptStep{
- {command: "AT+CPIN?", lines: []string{"+CPIN: READY"}},
- {command: "AT+CIMI", lines: []string{imsi}},
- {
- command: "AT+CCID",
- lines: []string{"+CCID: 8944101234567890123"},
- },
- {command: "AT+CGSN", lines: []string{"867530912345678"}},
- }
- }
- func successfulUSIMResponse() []byte {
- res := []byte{1, 2, 3, 4, 5, 6, 7, 8}
- ck := bytes.Repeat([]byte{0x11}, 16)
- ik := bytes.Repeat([]byte{0x22}, 16)
- kc := bytes.Repeat([]byte{0x33}, 8)
- value := []byte{byte(len(res))}
- value = append(value, res...)
- value = append(value, byte(len(ck)))
- value = append(value, ck...)
- value = append(value, byte(len(ik)))
- value = append(value, ik...)
- value = append(value, byte(len(kc)))
- value = append(value, kc...)
- raw := []byte{0xdb}
- raw = append(raw, value...)
- return append(raw, 0x90, 0x00)
- }
- func logicalChainedUSIMResponse() []byte {
- res := []byte{1, 2, 3, 4, 5, 6, 7, 8}
- ck := bytes.Repeat([]byte{0x11}, 16)
- ik := bytes.Repeat([]byte{0x22}, 16)
- kc := bytes.Repeat([]byte{0x33}, 8)
- value := []byte{byte(len(res))}
- value = append(value, res...)
- value = append(value, byte(len(ck)))
- value = append(value, ck...)
- value = append(value, byte(len(ik)))
- value = append(value, ik...)
- value = append(value, byte(len(kc)))
- value = append(value, kc...)
- raw := []byte{0xdb}
- raw = append(raw, value...)
- return append(raw, 0x90, 0x00)
- }
- func synchronizationFailureUSIMResponse() []byte {
- auts := make([]byte, 14)
- for index := range auts {
- auts[index] = byte(0xa0 + index)
- }
- raw := []byte{0xdc, byte(len(auts))}
- raw = append(raw, auts...)
- return append(raw, 0x90, 0x00)
- }
|