ec20_adapter.go 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373
  1. package vowifi
  2. import (
  3. "context"
  4. "encoding/csv"
  5. "encoding/hex"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "sort"
  10. "strconv"
  11. "strings"
  12. "sync"
  13. "time"
  14. "vocat/internal/modem"
  15. )
  16. var (
  17. ErrEC20SIMNotReady = errors.New("vocat: EC20 SIM is not ready")
  18. ErrEC20MNCUnavailable = errors.New("vocat: EC20 SIM does not expose an explicit MNC length")
  19. ErrEC20ApplicationAbsent = errors.New("vocat: EC20 has no usable USIM or ISIM application")
  20. ErrEC20IdentityChanged = errors.New("vocat: EC20 SIM identity changed during authentication")
  21. ErrEC20AKACommand = errors.New("vocat: EC20 USIM AUTHENTICATE command failed")
  22. ErrEC20AKAResponse = errors.New("vocat: EC20 returned an invalid USIM AUTHENTICATE response")
  23. ErrEC20AKAMACFailure = errors.New("vocat: EC20 USIM rejected the network authentication token")
  24. )
  25. const (
  26. usimAIDPrefix = "A0000000871002"
  27. isimAIDPrefix = "A0000000871004"
  28. efADDecimal = 28589 // 0x6FAD
  29. channelCleanupTimeout = 3 * time.Second
  30. )
  31. // EC20ATExecutor is deliberately the same narrow shape as
  32. // device.Manager.ExecuteAT. Production code passes the device manager directly;
  33. // tests can use an evidence transcript without opening any serial device.
  34. type EC20ATExecutor interface {
  35. ExecuteAT(context.Context, string, string) (modem.Response, error)
  36. }
  37. // EC20SensitiveATExecutor is implemented by device.Manager so an APDU carrying
  38. // RAND and AUTN is not retained as a device error. The fallback exists only for
  39. // small deterministic test executors.
  40. type EC20SensitiveATExecutor interface {
  41. ExecuteSensitiveAT(context.Context, string, string) (modem.Response, error)
  42. }
  43. type EC20AdapterOptions struct {
  44. // PureAirplanePolicy reports the independent user policy. The adapter only
  45. // changes the transactional CFUN projection used by VoWiFi and never
  46. // changes this policy.
  47. PureAirplanePolicy func(deviceID string) bool
  48. // HomePLMN supplies an explicit operator configuration when EF_AD omits
  49. // the MNC length. The returned MCC/MNC must exactly prefix the live IMSI.
  50. HomePLMN func(deviceID, iccid, imsi string) (mcc, mnc string, ok bool)
  51. // RestoreCellularData permits reactivating PDP contexts that were active
  52. // before the VoWiFi transaction. It is deliberately false by default:
  53. // VoWiFi must never start billable cellular data unless an operator has
  54. // explicitly opted in to that separate behavior.
  55. RestoreCellularData bool
  56. }
  57. // EC20Adapter implements SIMIdentityReader, AKAProvider, and RadioController
  58. // using standardized AT commands on the AT port selected by device.Manager.
  59. // It never discovers or opens serial ports itself, so ttyUSB0 diagnostic cannot
  60. // be selected accidentally.
  61. type EC20Adapter struct {
  62. executor EC20ATExecutor
  63. options EC20AdapterOptions
  64. apduMu sync.Mutex
  65. mu sync.Mutex
  66. bindings map[string]ec20SIMBinding
  67. checkpoints map[string]ec20RadioCheckpoint
  68. }
  69. type ec20SIMBinding struct {
  70. deviceID string
  71. iccid string
  72. imsi string
  73. aid string
  74. application string
  75. basicChannel bool
  76. }
  77. type ec20RadioCheckpoint struct {
  78. activeCIDs []int
  79. }
  80. var (
  81. _ SIMIdentityReader = (*EC20Adapter)(nil)
  82. _ AKAProvider = (*EC20Adapter)(nil)
  83. _ RadioController = (*EC20Adapter)(nil)
  84. )
  85. func NewEC20Adapter(
  86. executor EC20ATExecutor,
  87. options EC20AdapterOptions,
  88. ) (*EC20Adapter, error) {
  89. if executor == nil {
  90. return nil, errors.New("vocat: EC20 AT executor is required")
  91. }
  92. return &EC20Adapter{
  93. executor: executor,
  94. options: options,
  95. bindings: make(map[string]ec20SIMBinding),
  96. checkpoints: make(map[string]ec20RadioCheckpoint),
  97. }, nil
  98. }
  99. func (adapter *EC20Adapter) ReadIdentity(
  100. ctx context.Context,
  101. deviceID string,
  102. ) (SIMIdentity, error) {
  103. deviceID = strings.TrimSpace(deviceID)
  104. if deviceID == "" {
  105. return SIMIdentity{}, errors.New("vocat: EC20 device ID is required")
  106. }
  107. pin, err := adapter.execute(ctx, deviceID, "AT+CPIN?")
  108. if err != nil {
  109. return SIMIdentity{}, fmt.Errorf("read EC20 SIM state: %w", err)
  110. }
  111. if !responseContainsValue(pin, "+CPIN:", "READY") {
  112. return SIMIdentity{}, ErrEC20SIMNotReady
  113. }
  114. imsiResponse, err := adapter.execute(ctx, deviceID, "AT+CIMI")
  115. if err != nil {
  116. return SIMIdentity{}, fmt.Errorf("read EC20 IMSI: %w", err)
  117. }
  118. imsi := digitIdentifier(imsiResponse, []string{"+CIMI:"}, 10, 18)
  119. if imsi == "" {
  120. return SIMIdentity{}, errors.New("vocat: EC20 returned no valid IMSI")
  121. }
  122. iccid, err := adapter.readICCID(ctx, deviceID)
  123. if err != nil {
  124. return SIMIdentity{}, err
  125. }
  126. imeiResponse, err := adapter.execute(ctx, deviceID, "AT+CGSN")
  127. if err != nil {
  128. return SIMIdentity{}, fmt.Errorf("read EC20 IMEI: %w", err)
  129. }
  130. imei := digitIdentifier(imeiResponse, []string{"+CGSN:", "+GSN:"}, 14, 17)
  131. if imei == "" {
  132. return SIMIdentity{}, errors.New("vocat: EC20 returned no valid IMEI")
  133. }
  134. homeMCC, homeMNC, err := adapter.readHomePLMN(
  135. ctx,
  136. deviceID,
  137. iccid,
  138. imsi,
  139. )
  140. if err != nil {
  141. return SIMIdentity{}, err
  142. }
  143. identity := SIMIdentity{
  144. ICCID: iccid,
  145. IMSI: imsi,
  146. IMEI: imei,
  147. HomeMCC: homeMCC,
  148. HomeMNC: homeMNC,
  149. }
  150. adapter.mu.Lock()
  151. adapter.bindings[iccid] = ec20SIMBinding{
  152. deviceID: deviceID,
  153. iccid: iccid,
  154. imsi: imsi,
  155. }
  156. adapter.mu.Unlock()
  157. return identity, nil
  158. }
  159. // ReadSMSCenter returns the service-centre address configured by the SIM.
  160. // AT+CSCA? is read-only and remains available while cellular RF is disabled
  161. // for a Wi-Fi Calling session.
  162. func (adapter *EC20Adapter) ReadSMSCenter(ctx context.Context, deviceID string) (string, error) {
  163. response, err := adapter.execute(ctx, strings.TrimSpace(deviceID), "AT+CSCA?")
  164. if err != nil {
  165. return "", fmt.Errorf("read EC20 SMS service centre: %w", err)
  166. }
  167. fields := parseCSV(valueAfterATPrefix(response, "+CSCA:"))
  168. if len(fields) == 0 {
  169. return "", errors.New("vocat: EC20 returned no SMS service-centre address")
  170. }
  171. value := strings.Trim(strings.TrimSpace(fields[0]), `"`)
  172. digits := strings.TrimPrefix(value, "+")
  173. if !validDigits(digits, 3, 20) {
  174. return "", errors.New("vocat: EC20 returned an invalid SMS service-centre address")
  175. }
  176. return value, nil
  177. }
  178. func (adapter *EC20Adapter) readHomePLMN(
  179. ctx context.Context,
  180. deviceID string,
  181. iccid string,
  182. imsi string,
  183. ) (string, string, error) {
  184. mncLength, efErr := adapter.readExplicitMNCLength(ctx, deviceID)
  185. if efErr == nil {
  186. if len(imsi) < 3+mncLength {
  187. return "", "", errors.New(
  188. "vocat: IMSI is shorter than the EF_AD home PLMN",
  189. )
  190. }
  191. return imsi[:3], imsi[3 : 3+mncLength], nil
  192. }
  193. if adapter.options.HomePLMN != nil {
  194. mcc, mnc, ok := adapter.options.HomePLMN(deviceID, iccid, imsi)
  195. mcc = strings.TrimSpace(mcc)
  196. mnc = strings.TrimSpace(mnc)
  197. if ok && validConfiguredHomePLMN(imsi, mcc, mnc) {
  198. return mcc, mnc, nil
  199. }
  200. }
  201. // Exact assigned HPLMN prefixes are data, not an MNC-length heuristic. The
  202. // target Vodafone UK SIM is 234/15. Unknown assignments remain fail-closed.
  203. for prefix, mncLength := range map[string]int{"23415": 2} {
  204. if strings.HasPrefix(imsi, prefix) {
  205. return imsi[:3], imsi[3 : 3+mncLength], nil
  206. }
  207. }
  208. return "", "", efErr
  209. }
  210. func validConfiguredHomePLMN(imsi, mcc, mnc string) bool {
  211. if !validDigits(mcc, 3, 3) || !validDigits(mnc, 2, 3) {
  212. return false
  213. }
  214. return strings.HasPrefix(imsi, mcc+mnc)
  215. }
  216. func (adapter *EC20Adapter) readExplicitMNCLength(
  217. ctx context.Context,
  218. deviceID string,
  219. ) (int, error) {
  220. commands := []string{
  221. fmt.Sprintf("AT+CRSM=176,%d,0,0,4", efADDecimal),
  222. fmt.Sprintf("AT+CRSM=176,%d,0,0,0", efADDecimal),
  223. }
  224. var lastErr error
  225. for _, command := range commands {
  226. response, err := adapter.execute(ctx, deviceID, command)
  227. if err != nil {
  228. lastErr = err
  229. continue
  230. }
  231. data, err := parseCRSMData(response)
  232. if err != nil {
  233. lastErr = err
  234. continue
  235. }
  236. if len(data) < 4 {
  237. lastErr = ErrEC20MNCUnavailable
  238. continue
  239. }
  240. length := int(data[3] & 0x0f)
  241. if length == 2 || length == 3 {
  242. return length, nil
  243. }
  244. lastErr = ErrEC20MNCUnavailable
  245. }
  246. if lastErr == nil {
  247. lastErr = ErrEC20MNCUnavailable
  248. }
  249. return 0, fmt.Errorf("%w: %v", ErrEC20MNCUnavailable, lastErr)
  250. }
  251. func (adapter *EC20Adapter) readICCID(
  252. ctx context.Context,
  253. deviceID string,
  254. ) (string, error) {
  255. var lastErr error
  256. for attempt := 0; attempt < 3; attempt++ {
  257. for _, command := range []string{"AT+CCID", "AT+QCCID"} {
  258. response, err := adapter.execute(ctx, deviceID, command)
  259. if err != nil {
  260. lastErr = err
  261. continue
  262. }
  263. value := iccidIdentifier(
  264. response,
  265. []string{"+CCID:", "+QCCID:"},
  266. 18,
  267. 22,
  268. )
  269. if value != "" {
  270. return value, nil
  271. }
  272. lastErr = errors.New("response contained no valid ICCID")
  273. }
  274. if attempt < 2 {
  275. select {
  276. case <-ctx.Done():
  277. return "", ctx.Err()
  278. case <-time.After(100 * time.Millisecond):
  279. }
  280. }
  281. }
  282. return "", fmt.Errorf("read EC20 ICCID: %w", lastErr)
  283. }
  284. func (adapter *EC20Adapter) CheckReady(
  285. ctx context.Context,
  286. identity SIMIdentity,
  287. ) (AKAEvidence, error) {
  288. binding, err := adapter.bindingFor(identity)
  289. if err != nil {
  290. return AKAEvidence{}, err
  291. }
  292. if err := adapter.verifyLiveICCID(ctx, binding); err != nil {
  293. return AKAEvidence{}, err
  294. }
  295. // CCHO/CGLA/GET RESPONSE/CCHC is one UICC transaction. Serialize it
  296. // across the adapter so periodic device refreshes or another AKA exchange
  297. // cannot insert an APDU between a 61xx response and GET RESPONSE.
  298. adapter.apduMu.Lock()
  299. defer adapter.apduMu.Unlock()
  300. aid, application, err := adapter.discoverAKAApplication(ctx, binding.deviceID)
  301. if err != nil {
  302. return AKAEvidence{}, err
  303. }
  304. channel, err := adapter.openLogicalChannel(ctx, binding.deviceID, aid)
  305. basicChannel := false
  306. if err == nil {
  307. if err := adapter.closeLogicalChannelWithCleanup(
  308. binding.deviceID,
  309. channel,
  310. ); err != nil {
  311. return AKAEvidence{}, err
  312. }
  313. } else {
  314. // Several EC20 firmware branches reject CCHO/CGLA even though their
  315. // basic-channel CSIM implementation is standards compliant.
  316. if err := adapter.selectBasicApplication(
  317. ctx,
  318. binding.deviceID,
  319. aid,
  320. ); err != nil {
  321. return AKAEvidence{}, errors.Join(
  322. ErrEC20ApplicationAbsent,
  323. err,
  324. )
  325. }
  326. basicChannel = true
  327. }
  328. binding.aid = aid
  329. binding.application = application
  330. binding.basicChannel = basicChannel
  331. adapter.mu.Lock()
  332. adapter.bindings[binding.iccid] = binding
  333. adapter.mu.Unlock()
  334. return AKAEvidence{Ready: true, Application: application}, nil
  335. }
  336. func (adapter *EC20Adapter) Authenticate(
  337. ctx context.Context,
  338. identity SIMIdentity,
  339. challenge AKAChallenge,
  340. ) (AKAResult, error) {
  341. binding, err := adapter.bindingFor(identity)
  342. if err != nil {
  343. return AKAResult{}, err
  344. }
  345. if binding.aid == "" {
  346. if _, err := adapter.CheckReady(ctx, identity); err != nil {
  347. return AKAResult{}, err
  348. }
  349. binding, err = adapter.bindingFor(identity)
  350. if err != nil {
  351. return AKAResult{}, err
  352. }
  353. }
  354. if err := adapter.verifyLiveICCID(ctx, binding); err != nil {
  355. return AKAResult{}, err
  356. }
  357. adapter.apduMu.Lock()
  358. defer adapter.apduMu.Unlock()
  359. apdu := buildUSIMAuthenticateAPDU(challenge)
  360. var raw []byte
  361. if binding.basicChannel {
  362. if err := adapter.selectBasicApplication(
  363. ctx,
  364. binding.deviceID,
  365. binding.aid,
  366. ); err != nil {
  367. return AKAResult{}, err
  368. }
  369. raw, err = adapter.transmitBasicAPDU(
  370. ctx,
  371. binding.deviceID,
  372. apdu,
  373. true,
  374. )
  375. if err != nil {
  376. return AKAResult{}, ErrEC20AKACommand
  377. }
  378. } else {
  379. channel, err := adapter.openLogicalChannel(
  380. ctx,
  381. binding.deviceID,
  382. binding.aid,
  383. )
  384. if err != nil {
  385. return AKAResult{}, err
  386. }
  387. var commandErr error
  388. raw, commandErr = adapter.transmitLogicalAPDU(
  389. ctx,
  390. binding.deviceID,
  391. channel,
  392. apdu,
  393. true,
  394. )
  395. closeErr := adapter.closeLogicalChannelWithCleanup(
  396. binding.deviceID,
  397. channel,
  398. )
  399. if commandErr != nil {
  400. if closeErr != nil {
  401. return AKAResult{}, errors.Join(commandErr, closeErr)
  402. }
  403. return AKAResult{}, commandErr
  404. }
  405. if closeErr != nil {
  406. return AKAResult{}, closeErr
  407. }
  408. }
  409. return parseUSIMAuthenticateResponse(raw)
  410. }
  411. func buildUSIMAuthenticateAPDU(challenge AKAChallenge) []byte {
  412. // TS 31.102 AUTHENTICATE, 3G security context (P2=0x81):
  413. // Lc=34, then LV(RAND) and LV(AUTN), followed by Le.
  414. apdu := make([]byte, 0, 40)
  415. apdu = append(apdu, 0x00, 0x88, 0x00, 0x81, 0x22, 0x10)
  416. apdu = append(apdu, challenge.RAND[:]...)
  417. apdu = append(apdu, 0x10)
  418. apdu = append(apdu, challenge.AUTN[:]...)
  419. apdu = append(apdu, 0x00)
  420. return apdu
  421. }
  422. func parseUSIMAuthenticateResponse(raw []byte) (AKAResult, error) {
  423. if len(raw) < 2 {
  424. return AKAResult{}, fmt.Errorf(
  425. "%w: response length %d has no status word",
  426. ErrEC20AKAResponse,
  427. len(raw),
  428. )
  429. }
  430. status := uint16(raw[len(raw)-2])<<8 | uint16(raw[len(raw)-1])
  431. body := raw[:len(raw)-2]
  432. if status != 0x9000 {
  433. switch status {
  434. case 0x9862:
  435. return AKAResult{}, ErrEC20AKAMACFailure
  436. default:
  437. return AKAResult{}, fmt.Errorf(
  438. "%w: status word %04X",
  439. ErrEC20AKAResponse,
  440. status,
  441. )
  442. }
  443. }
  444. if len(body) < 2 {
  445. return AKAResult{}, fmt.Errorf(
  446. "%w: response body length %d is too short",
  447. ErrEC20AKAResponse,
  448. len(body),
  449. )
  450. }
  451. tag := body[0]
  452. value := body[1:]
  453. switch tag {
  454. case 0xdb:
  455. res, rest, ok := takeLV(value)
  456. if !ok {
  457. return AKAResult{}, fmt.Errorf(
  458. "%w: malformed RES field in %d-byte success value",
  459. ErrEC20AKAResponse,
  460. len(value),
  461. )
  462. }
  463. if len(res) < 4 || len(res) > 16 {
  464. return AKAResult{}, fmt.Errorf(
  465. "%w: invalid RES length %d",
  466. ErrEC20AKAResponse,
  467. len(res),
  468. )
  469. }
  470. ck, rest, ok := takeLV(rest)
  471. if !ok || len(ck) != 16 {
  472. return AKAResult{}, fmt.Errorf(
  473. "%w: invalid CK length %d",
  474. ErrEC20AKAResponse,
  475. len(ck),
  476. )
  477. }
  478. ik, rest, ok := takeLV(rest)
  479. if !ok || len(ik) != 16 {
  480. return AKAResult{}, fmt.Errorf(
  481. "%w: invalid IK length %d",
  482. ErrEC20AKAResponse,
  483. len(ik),
  484. )
  485. }
  486. // Kc is present on many USIMs. It is not needed by EAP-AKA, but when
  487. // present its LV still has to be structurally valid.
  488. if len(rest) > 0 {
  489. kc, tail, ok := takeLV(rest)
  490. if !ok || len(kc) != 8 || len(tail) != 0 {
  491. return AKAResult{}, fmt.Errorf(
  492. "%w: invalid optional Kc length %d with %d trailing bytes",
  493. ErrEC20AKAResponse,
  494. len(kc),
  495. len(tail),
  496. )
  497. }
  498. }
  499. return AKAResult{
  500. RES: append([]byte(nil), res...),
  501. CK: append([]byte(nil), ck...),
  502. IK: append([]byte(nil), ik...),
  503. }, nil
  504. case 0xdc:
  505. auts, tail, ok := takeLV(value)
  506. if !ok || len(auts) != 14 || len(tail) != 0 {
  507. return AKAResult{}, fmt.Errorf(
  508. "%w: invalid AUTS length %d with %d trailing bytes",
  509. ErrEC20AKAResponse,
  510. len(auts),
  511. len(tail),
  512. )
  513. }
  514. return AKAResult{
  515. AUTS: append([]byte(nil), auts...),
  516. SynchronizationFailure: true,
  517. }, nil
  518. default:
  519. return AKAResult{}, fmt.Errorf(
  520. "%w: unsupported response tag %02X with %d value bytes",
  521. ErrEC20AKAResponse,
  522. tag,
  523. len(value),
  524. )
  525. }
  526. }
  527. func takeLV(value []byte) (field, rest []byte, ok bool) {
  528. if len(value) == 0 {
  529. return nil, value, false
  530. }
  531. length := int(value[0])
  532. if length > len(value)-1 {
  533. return nil, value, false
  534. }
  535. return value[1 : 1+length], value[1+length:], true
  536. }
  537. func (adapter *EC20Adapter) Snapshot(
  538. ctx context.Context,
  539. deviceID string,
  540. ) (RadioSnapshot, error) {
  541. mode, err := adapter.readOperatingMode(ctx, deviceID)
  542. if err != nil {
  543. return RadioSnapshot{}, err
  544. }
  545. active, err := adapter.readActiveCIDs(ctx, deviceID)
  546. if err != nil {
  547. return RadioSnapshot{}, err
  548. }
  549. adapter.mu.Lock()
  550. adapter.checkpoints[deviceID] = ec20RadioCheckpoint{
  551. activeCIDs: append([]int(nil), active...),
  552. }
  553. adapter.mu.Unlock()
  554. purePolicy := false
  555. if adapter.options.PureAirplanePolicy != nil {
  556. purePolicy = adapter.options.PureAirplanePolicy(deviceID)
  557. }
  558. return RadioSnapshot{
  559. CellularDataEnabled: len(active) > 0,
  560. OperatingMode: mode,
  561. PureAirplanePolicy: purePolicy,
  562. }, nil
  563. }
  564. func (adapter *EC20Adapter) StopCellularData(
  565. ctx context.Context,
  566. deviceID string,
  567. ) error {
  568. active, err := adapter.readActiveCIDs(ctx, deviceID)
  569. if err != nil {
  570. return err
  571. }
  572. for _, cid := range active {
  573. if _, err := adapter.execute(
  574. ctx,
  575. deviceID,
  576. fmt.Sprintf("AT+CGACT=0,%d", cid),
  577. ); err != nil {
  578. return fmt.Errorf("deactivate EC20 PDP context %d: %w", cid, err)
  579. }
  580. }
  581. remaining, err := adapter.readActiveCIDs(ctx, deviceID)
  582. if err != nil {
  583. return err
  584. }
  585. if len(remaining) != 0 {
  586. return errors.New("vocat: EC20 cellular data remained active")
  587. }
  588. return nil
  589. }
  590. func (adapter *EC20Adapter) EnterVoWiFiRFOff(
  591. ctx context.Context,
  592. deviceID string,
  593. ) error {
  594. mode, err := adapter.readOperatingMode(ctx, deviceID)
  595. if err != nil {
  596. return err
  597. }
  598. if mode != 4 {
  599. if _, err := adapter.execute(ctx, deviceID, "AT+CFUN=4"); err != nil {
  600. return fmt.Errorf("enter EC20 RF-off mode: %w", err)
  601. }
  602. }
  603. mode, err = adapter.readOperatingMode(ctx, deviceID)
  604. if err != nil {
  605. return err
  606. }
  607. if mode != 4 {
  608. return fmt.Errorf("vocat: EC20 reported CFUN=%d after RF-off request", mode)
  609. }
  610. return nil
  611. }
  612. func (adapter *EC20Adapter) Restore(
  613. ctx context.Context,
  614. deviceID string,
  615. snapshot RadioSnapshot,
  616. ) error {
  617. if snapshot.OperatingMode < 0 {
  618. return errors.New("vocat: invalid EC20 radio snapshot")
  619. }
  620. currentMode, err := adapter.readOperatingMode(ctx, deviceID)
  621. if err != nil {
  622. return err
  623. }
  624. if currentMode != snapshot.OperatingMode {
  625. if _, err := adapter.execute(
  626. ctx,
  627. deviceID,
  628. fmt.Sprintf("AT+CFUN=%d", snapshot.OperatingMode),
  629. ); err != nil {
  630. return fmt.Errorf("restore EC20 operating mode: %w", err)
  631. }
  632. }
  633. currentMode, err = adapter.readOperatingMode(ctx, deviceID)
  634. if err != nil {
  635. return err
  636. }
  637. if currentMode != snapshot.OperatingMode {
  638. return fmt.Errorf(
  639. "vocat: EC20 restore reported CFUN=%d, expected %d",
  640. currentMode,
  641. snapshot.OperatingMode,
  642. )
  643. }
  644. adapter.mu.Lock()
  645. checkpoint, found := adapter.checkpoints[deviceID]
  646. adapter.mu.Unlock()
  647. if !found {
  648. if snapshot.CellularDataEnabled {
  649. return errors.New("vocat: EC20 PDP restore evidence is unavailable")
  650. }
  651. checkpoint.activeCIDs = nil
  652. }
  653. if (snapshot.OperatingMode == 0 || snapshot.OperatingMode == 4) &&
  654. len(checkpoint.activeCIDs) > 0 {
  655. return errors.New("vocat: EC20 snapshot has active data in RF-off mode")
  656. }
  657. desiredCIDs := checkpoint.activeCIDs
  658. if !adapter.options.RestoreCellularData {
  659. desiredCIDs = nil
  660. }
  661. if err := adapter.reconcileActiveCIDs(
  662. ctx,
  663. deviceID,
  664. desiredCIDs,
  665. ); err != nil {
  666. return err
  667. }
  668. adapter.mu.Lock()
  669. delete(adapter.checkpoints, deviceID)
  670. adapter.mu.Unlock()
  671. return nil
  672. }
  673. func (adapter *EC20Adapter) reconcileActiveCIDs(
  674. ctx context.Context,
  675. deviceID string,
  676. desired []int,
  677. ) error {
  678. current, err := adapter.readActiveCIDs(ctx, deviceID)
  679. if err != nil {
  680. return err
  681. }
  682. desiredSet := integerSet(desired)
  683. currentSet := integerSet(current)
  684. for _, cid := range current {
  685. if _, wanted := desiredSet[cid]; wanted {
  686. continue
  687. }
  688. if _, err := adapter.execute(
  689. ctx,
  690. deviceID,
  691. fmt.Sprintf("AT+CGACT=0,%d", cid),
  692. ); err != nil {
  693. return fmt.Errorf("restore EC20 PDP context %d: %w", cid, err)
  694. }
  695. }
  696. for _, cid := range desired {
  697. if _, active := currentSet[cid]; active {
  698. continue
  699. }
  700. if _, err := adapter.execute(
  701. ctx,
  702. deviceID,
  703. fmt.Sprintf("AT+CGACT=1,%d", cid),
  704. ); err != nil {
  705. return fmt.Errorf("restore EC20 PDP context %d: %w", cid, err)
  706. }
  707. }
  708. verified, err := adapter.readActiveCIDs(ctx, deviceID)
  709. if err != nil {
  710. return err
  711. }
  712. if !sameIntegers(verified, desired) {
  713. return fmt.Errorf(
  714. "vocat: EC20 PDP restore mismatch: active contexts %v",
  715. verified,
  716. )
  717. }
  718. return nil
  719. }
  720. func (adapter *EC20Adapter) readOperatingMode(
  721. ctx context.Context,
  722. deviceID string,
  723. ) (int, error) {
  724. response, err := adapter.execute(ctx, deviceID, "AT+CFUN?")
  725. if err != nil {
  726. return 0, fmt.Errorf("read EC20 operating mode: %w", err)
  727. }
  728. value := valueAfterATPrefix(response, "+CFUN:")
  729. fields := parseCSV(value)
  730. if len(fields) == 0 {
  731. return 0, errors.New("vocat: EC20 returned no CFUN mode")
  732. }
  733. mode, err := strconv.Atoi(fields[0])
  734. if err != nil || mode < 0 {
  735. return 0, errors.New("vocat: EC20 returned an invalid CFUN mode")
  736. }
  737. return mode, nil
  738. }
  739. func (adapter *EC20Adapter) readActiveCIDs(
  740. ctx context.Context,
  741. deviceID string,
  742. ) ([]int, error) {
  743. response, err := adapter.execute(ctx, deviceID, "AT+CGACT?")
  744. if err != nil {
  745. return nil, fmt.Errorf("read EC20 PDP contexts: %w", err)
  746. }
  747. var active []int
  748. for _, line := range response.Lines {
  749. line = strings.TrimSpace(line)
  750. if !strings.HasPrefix(strings.ToUpper(line), "+CGACT:") {
  751. continue
  752. }
  753. fields := parseCSV(strings.TrimSpace(line[len("+CGACT:"):]))
  754. if len(fields) < 2 {
  755. return nil, errors.New("vocat: EC20 returned an invalid CGACT record")
  756. }
  757. cid, cidErr := strconv.Atoi(fields[0])
  758. state, stateErr := strconv.Atoi(fields[1])
  759. if cidErr != nil || stateErr != nil || cid <= 0 || (state != 0 && state != 1) {
  760. return nil, errors.New("vocat: EC20 returned an invalid CGACT record")
  761. }
  762. if state == 1 {
  763. active = append(active, cid)
  764. }
  765. }
  766. sort.Ints(active)
  767. return uniqueIntegers(active), nil
  768. }
  769. func (adapter *EC20Adapter) bindingFor(
  770. identity SIMIdentity,
  771. ) (ec20SIMBinding, error) {
  772. iccid := strings.TrimSpace(identity.ICCID)
  773. adapter.mu.Lock()
  774. binding, ok := adapter.bindings[iccid]
  775. adapter.mu.Unlock()
  776. if !ok || iccid == "" {
  777. return ec20SIMBinding{}, errors.New("vocat: EC20 SIM identity is not bound to a device")
  778. }
  779. if strings.TrimSpace(identity.IMSI) != binding.imsi {
  780. return ec20SIMBinding{}, ErrEC20IdentityChanged
  781. }
  782. return binding, nil
  783. }
  784. func (adapter *EC20Adapter) verifyLiveICCID(
  785. ctx context.Context,
  786. binding ec20SIMBinding,
  787. ) error {
  788. iccid, err := adapter.readICCID(ctx, binding.deviceID)
  789. if err != nil {
  790. return err
  791. }
  792. if iccid != binding.iccid {
  793. return ErrEC20IdentityChanged
  794. }
  795. return nil
  796. }
  797. func (adapter *EC20Adapter) discoverAKAApplication(
  798. ctx context.Context,
  799. deviceID string,
  800. ) (aid string, application string, err error) {
  801. response, cuadErr := adapter.execute(ctx, deviceID, "AT+CUAD")
  802. if cuadErr == nil {
  803. data, parseErr := parseCUADData(response)
  804. if parseErr == nil {
  805. aids := collectApplicationAIDs(data)
  806. for _, candidate := range aids {
  807. if strings.HasPrefix(candidate, usimAIDPrefix) {
  808. return candidate, "USIM", nil
  809. }
  810. }
  811. for _, candidate := range aids {
  812. if strings.HasPrefix(candidate, isimAIDPrefix) {
  813. return candidate, "ISIM", nil
  814. }
  815. }
  816. if len(aids) > 0 {
  817. return "", "", ErrEC20ApplicationAbsent
  818. }
  819. }
  820. }
  821. // AT+CUAD is optional on older EC20 firmware. CCHO still provides a
  822. // standards-based, evidence-bearing probe of the assigned USIM AID.
  823. return usimAIDPrefix, "USIM", nil
  824. }
  825. func (adapter *EC20Adapter) openLogicalChannel(
  826. ctx context.Context,
  827. deviceID string,
  828. aid string,
  829. ) (int, error) {
  830. response, err := adapter.execute(
  831. ctx,
  832. deviceID,
  833. fmt.Sprintf(`AT+CCHO="%s"`, aid),
  834. )
  835. if err != nil {
  836. return 0, fmt.Errorf("%w: open application", ErrEC20ApplicationAbsent)
  837. }
  838. value := valueAfterATPrefix(response, "+CCHO:")
  839. if value == "" {
  840. for _, line := range response.Lines {
  841. line = strings.TrimSpace(line)
  842. if _, parseErr := strconv.Atoi(line); parseErr == nil {
  843. value = line
  844. break
  845. }
  846. }
  847. }
  848. channel, err := strconv.Atoi(strings.TrimSpace(value))
  849. if err != nil || channel < 1 || channel > 19 {
  850. return 0, errors.New("vocat: EC20 returned an invalid logical channel")
  851. }
  852. return channel, nil
  853. }
  854. func (adapter *EC20Adapter) closeLogicalChannel(
  855. ctx context.Context,
  856. deviceID string,
  857. channel int,
  858. ) error {
  859. if _, err := adapter.execute(
  860. ctx,
  861. deviceID,
  862. fmt.Sprintf("AT+CCHC=%d", channel),
  863. ); err != nil {
  864. return fmt.Errorf("close EC20 logical channel: %w", err)
  865. }
  866. return nil
  867. }
  868. func (adapter *EC20Adapter) closeLogicalChannelWithCleanup(
  869. deviceID string,
  870. channel int,
  871. ) error {
  872. ctx, cancel := context.WithTimeout(
  873. context.Background(),
  874. channelCleanupTimeout,
  875. )
  876. defer cancel()
  877. return adapter.closeLogicalChannel(ctx, deviceID, channel)
  878. }
  879. func (adapter *EC20Adapter) selectBasicApplication(
  880. ctx context.Context,
  881. deviceID string,
  882. aid string,
  883. ) error {
  884. aidBytes, err := hex.DecodeString(aid)
  885. if err != nil || len(aidBytes) == 0 || len(aidBytes) > 255 {
  886. return errors.New("vocat: invalid USIM application identifier")
  887. }
  888. // SELECT by DF name, request the first/only matching application and FCP.
  889. apdu := []byte{0x00, 0xa4, 0x04, 0x04, byte(len(aidBytes))}
  890. apdu = append(apdu, aidBytes...)
  891. raw, err := adapter.transmitBasicAPDU(ctx, deviceID, apdu, false)
  892. if err != nil {
  893. return fmt.Errorf("select EC20 basic-channel application: %w", err)
  894. }
  895. _, status, err := splitAPDUStatus(raw)
  896. if err != nil {
  897. return err
  898. }
  899. if status != 0x9000 {
  900. return fmt.Errorf(
  901. "vocat: EC20 basic-channel SELECT returned %04X",
  902. status,
  903. )
  904. }
  905. return nil
  906. }
  907. func (adapter *EC20Adapter) transmitBasicAPDU(
  908. ctx context.Context,
  909. deviceID string,
  910. apdu []byte,
  911. sensitive bool,
  912. ) ([]byte, error) {
  913. if len(apdu) == 0 || len(apdu) > 261 {
  914. return nil, errors.New("vocat: invalid EC20 APDU length")
  915. }
  916. var collected []byte
  917. current := append([]byte(nil), apdu...)
  918. for exchange := 0; exchange < 4; exchange++ {
  919. command := fmt.Sprintf(
  920. `AT+CSIM=%d,"%s"`,
  921. len(current)*2,
  922. strings.ToUpper(hex.EncodeToString(current)),
  923. )
  924. var response modem.Response
  925. var err error
  926. if sensitive {
  927. response, err = adapter.executeSensitive(ctx, deviceID, command)
  928. } else {
  929. response, err = adapter.execute(ctx, deviceID, command)
  930. }
  931. if err != nil {
  932. return nil, errors.New("vocat: EC20 CSIM exchange failed")
  933. }
  934. raw, err := parseCSIMData(response)
  935. if err != nil {
  936. return nil, err
  937. }
  938. body, status, err := splitAPDUStatus(raw)
  939. if err != nil {
  940. return nil, err
  941. }
  942. collected = append(collected, body...)
  943. sw1 := byte(status >> 8)
  944. if sw1 != 0x61 && sw1 != 0x9f {
  945. collected = append(collected, byte(status>>8), byte(status))
  946. return collected, nil
  947. }
  948. // GET RESPONSE on the same basic channel. Le=0 means 256 bytes.
  949. current = []byte{0x00, 0xc0, 0x00, 0x00, byte(status)}
  950. }
  951. return nil, errors.New("vocat: EC20 APDU response chaining exceeded limit")
  952. }
  953. func (adapter *EC20Adapter) transmitLogicalAPDU(
  954. ctx context.Context,
  955. deviceID string,
  956. channel int,
  957. apdu []byte,
  958. sensitive bool,
  959. ) ([]byte, error) {
  960. if channel < 1 || channel > 19 || len(apdu) == 0 || len(apdu) > 261 {
  961. return nil, ErrEC20AKACommand
  962. }
  963. var collected []byte
  964. current := append([]byte(nil), apdu...)
  965. for exchange := 0; exchange < 4; exchange++ {
  966. command := fmt.Sprintf(
  967. `AT+CGLA=%d,%d,"%s"`,
  968. channel,
  969. len(current)*2,
  970. strings.ToUpper(hex.EncodeToString(current)),
  971. )
  972. var response modem.Response
  973. var err error
  974. if sensitive {
  975. response, err = adapter.executeSensitive(ctx, deviceID, command)
  976. } else {
  977. response, err = adapter.execute(ctx, deviceID, command)
  978. }
  979. if err != nil {
  980. return nil, errors.Join(ErrEC20AKACommand, err)
  981. }
  982. raw, err := parseCGLAData(response)
  983. if err != nil {
  984. return nil, fmt.Errorf("%w: %v", ErrEC20AKAResponse, err)
  985. }
  986. body, status, err := splitAPDUStatus(raw)
  987. if err != nil {
  988. return nil, fmt.Errorf("%w: %v", ErrEC20AKAResponse, err)
  989. }
  990. collected = append(collected, body...)
  991. sw1 := byte(status >> 8)
  992. if sw1 != 0x61 && sw1 != 0x9f {
  993. collected = append(collected, byte(status>>8), byte(status))
  994. return collected, nil
  995. }
  996. // CGLA carries the logical-channel identifier separately, so GET
  997. // RESPONSE retains the same interindustry CLA used by AUTHENTICATE.
  998. // Le=0 means 256 bytes when SW2 is zero.
  999. current = []byte{0x00, 0xc0, 0x00, 0x00, byte(status)}
  1000. }
  1001. return nil, fmt.Errorf(
  1002. "%w: logical-channel response chaining exceeded limit",
  1003. ErrEC20AKAResponse,
  1004. )
  1005. }
  1006. func splitAPDUStatus(raw []byte) ([]byte, uint16, error) {
  1007. if len(raw) < 2 {
  1008. return nil, 0, errors.New("vocat: EC20 APDU has no status word")
  1009. }
  1010. status := uint16(raw[len(raw)-2])<<8 | uint16(raw[len(raw)-1])
  1011. return raw[:len(raw)-2], status, nil
  1012. }
  1013. func (adapter *EC20Adapter) execute(
  1014. ctx context.Context,
  1015. deviceID string,
  1016. command string,
  1017. ) (modem.Response, error) {
  1018. return adapter.executor.ExecuteAT(ctx, deviceID, command)
  1019. }
  1020. func (adapter *EC20Adapter) executeSensitive(
  1021. ctx context.Context,
  1022. deviceID string,
  1023. command string,
  1024. ) (modem.Response, error) {
  1025. if executor, ok := adapter.executor.(EC20SensitiveATExecutor); ok {
  1026. return executor.ExecuteSensitiveAT(ctx, deviceID, command)
  1027. }
  1028. return adapter.executor.ExecuteAT(ctx, deviceID, command)
  1029. }
  1030. func parseCRSMData(response modem.Response) ([]byte, error) {
  1031. value := valueAfterATPrefix(response, "+CRSM:")
  1032. fields := parseCSV(value)
  1033. if len(fields) < 2 {
  1034. return nil, errors.New("invalid CRSM response")
  1035. }
  1036. sw1, err1 := strconv.Atoi(fields[0])
  1037. sw2, err2 := strconv.Atoi(fields[1])
  1038. if err1 != nil || err2 != nil {
  1039. return nil, errors.New("invalid CRSM status")
  1040. }
  1041. if sw1 != 0x90 && sw1 != 0x91 && sw1 != 0x9f {
  1042. return nil, fmt.Errorf("CRSM status %02X%02X", sw1, sw2)
  1043. }
  1044. if len(fields) < 3 {
  1045. return nil, errors.New("CRSM response has no data")
  1046. }
  1047. data, err := hex.DecodeString(strings.Trim(fields[2], `"`))
  1048. if err != nil {
  1049. return nil, errors.New("CRSM response data is not hexadecimal")
  1050. }
  1051. return data, nil
  1052. }
  1053. func parseCUADData(response modem.Response) ([]byte, error) {
  1054. fields := parseCSV(valueAfterATPrefix(response, "+CUAD:"))
  1055. if len(fields) == 0 {
  1056. return nil, errors.New("CUAD response has no data")
  1057. }
  1058. value := fields[len(fields)-1]
  1059. data, err := hex.DecodeString(strings.Trim(value, `"`))
  1060. if err != nil || len(data) == 0 {
  1061. return nil, errors.New("CUAD response data is invalid")
  1062. }
  1063. if len(data) >= 2 && data[len(data)-2] == 0x90 && data[len(data)-1] == 0x00 {
  1064. data = data[:len(data)-2]
  1065. }
  1066. return data, nil
  1067. }
  1068. func collectApplicationAIDs(data []byte) []string {
  1069. var result []string
  1070. var walk func([]byte)
  1071. walk = func(value []byte) {
  1072. for len(value) > 0 {
  1073. tag, constructed, body, consumed, err := decodeBERTLV(value)
  1074. if err != nil || consumed == 0 {
  1075. return
  1076. }
  1077. if len(tag) == 1 && tag[0] == 0x4f && len(body) > 0 {
  1078. result = append(result, strings.ToUpper(hex.EncodeToString(body)))
  1079. }
  1080. if constructed {
  1081. walk(body)
  1082. }
  1083. value = value[consumed:]
  1084. }
  1085. }
  1086. walk(data)
  1087. return result
  1088. }
  1089. func decodeBERTLV(data []byte) (
  1090. tag []byte,
  1091. constructed bool,
  1092. value []byte,
  1093. consumed int,
  1094. err error,
  1095. ) {
  1096. if len(data) < 2 {
  1097. return nil, false, nil, 0, errors.New("short BER-TLV")
  1098. }
  1099. tagLength := 1
  1100. if data[0]&0x1f == 0x1f {
  1101. for {
  1102. if tagLength >= len(data) {
  1103. return nil, false, nil, 0, errors.New("short BER tag")
  1104. }
  1105. last := data[tagLength]&0x80 == 0
  1106. tagLength++
  1107. if last {
  1108. break
  1109. }
  1110. if tagLength > 4 {
  1111. return nil, false, nil, 0, errors.New("oversized BER tag")
  1112. }
  1113. }
  1114. }
  1115. body, bodyConsumed, err := decodeBERTLVValue(data[tagLength:])
  1116. if err != nil {
  1117. return nil, false, nil, 0, err
  1118. }
  1119. return data[:tagLength],
  1120. data[0]&0x20 != 0,
  1121. body,
  1122. tagLength + bodyConsumed,
  1123. nil
  1124. }
  1125. func decodeBERTLVValue(data []byte) ([]byte, int, error) {
  1126. if len(data) == 0 {
  1127. return nil, 0, errors.New("missing BER length")
  1128. }
  1129. length := int(data[0])
  1130. lengthOctets := 1
  1131. if data[0]&0x80 != 0 {
  1132. count := int(data[0] & 0x7f)
  1133. if count == 0 || count > 2 || len(data) < 1+count {
  1134. return nil, 0, errors.New("invalid BER length")
  1135. }
  1136. length = 0
  1137. for _, octet := range data[1 : 1+count] {
  1138. length = length<<8 | int(octet)
  1139. }
  1140. lengthOctets += count
  1141. }
  1142. if length > len(data)-lengthOctets {
  1143. return nil, 0, errors.New("short BER value")
  1144. }
  1145. return data[lengthOctets : lengthOctets+length],
  1146. lengthOctets + length,
  1147. nil
  1148. }
  1149. func parseCGLAData(response modem.Response) ([]byte, error) {
  1150. return parseATAPDUData(response, "+CGLA:")
  1151. }
  1152. func parseCSIMData(response modem.Response) ([]byte, error) {
  1153. return parseATAPDUData(response, "+CSIM:")
  1154. }
  1155. func parseATAPDUData(response modem.Response, prefix string) ([]byte, error) {
  1156. fields := parseCSV(valueAfterATPrefix(response, prefix))
  1157. if len(fields) < 2 {
  1158. return nil, errors.New("EC20 response has no APDU")
  1159. }
  1160. declared, err := strconv.Atoi(fields[0])
  1161. if err != nil || declared < 0 {
  1162. return nil, errors.New("EC20 response has invalid APDU length")
  1163. }
  1164. encoded := strings.Trim(fields[1], `" `)
  1165. data, err := hex.DecodeString(encoded)
  1166. if err != nil {
  1167. return nil, errors.New("EC20 APDU response is not hexadecimal")
  1168. }
  1169. if declared != len(encoded) && declared != len(data) {
  1170. return nil, errors.New("EC20 APDU response length mismatch")
  1171. }
  1172. return data, nil
  1173. }
  1174. func responseContainsValue(
  1175. response modem.Response,
  1176. prefix string,
  1177. expected string,
  1178. ) bool {
  1179. return strings.EqualFold(
  1180. strings.Trim(strings.TrimSpace(valueAfterATPrefix(response, prefix)), `"`),
  1181. expected,
  1182. )
  1183. }
  1184. func valueAfterATPrefix(response modem.Response, prefix string) string {
  1185. for _, line := range response.Lines {
  1186. line = strings.TrimSpace(line)
  1187. if strings.HasPrefix(strings.ToUpper(line), strings.ToUpper(prefix)) {
  1188. return strings.TrimSpace(line[len(prefix):])
  1189. }
  1190. }
  1191. return ""
  1192. }
  1193. func digitIdentifier(
  1194. response modem.Response,
  1195. prefixes []string,
  1196. minimum int,
  1197. maximum int,
  1198. ) string {
  1199. for _, prefix := range prefixes {
  1200. value := strings.Trim(valueAfterATPrefix(response, prefix), `" `)
  1201. if validDigits(value, minimum, maximum) {
  1202. return value
  1203. }
  1204. }
  1205. for _, line := range response.Lines {
  1206. value := strings.TrimSpace(line)
  1207. if validDigits(value, minimum, maximum) {
  1208. return value
  1209. }
  1210. }
  1211. return ""
  1212. }
  1213. func iccidIdentifier(
  1214. response modem.Response,
  1215. prefixes []string,
  1216. minimum int,
  1217. maximum int,
  1218. ) string {
  1219. normalize := func(value string) string {
  1220. value = strings.Trim(value, `" `)
  1221. value = strings.TrimRight(value, "Ff")
  1222. if validDigits(value, minimum, maximum) {
  1223. return value
  1224. }
  1225. return ""
  1226. }
  1227. for _, prefix := range prefixes {
  1228. if value := normalize(valueAfterATPrefix(response, prefix)); value != "" {
  1229. return value
  1230. }
  1231. }
  1232. for _, line := range response.Lines {
  1233. if value := normalize(strings.TrimSpace(line)); value != "" {
  1234. return value
  1235. }
  1236. }
  1237. return ""
  1238. }
  1239. func validDigits(value string, minimum int, maximum int) bool {
  1240. if len(value) < minimum || len(value) > maximum {
  1241. return false
  1242. }
  1243. for _, character := range value {
  1244. if character < '0' || character > '9' {
  1245. return false
  1246. }
  1247. }
  1248. return true
  1249. }
  1250. func parseCSV(value string) []string {
  1251. reader := csv.NewReader(strings.NewReader(value))
  1252. reader.TrimLeadingSpace = true
  1253. reader.LazyQuotes = true
  1254. record, err := reader.Read()
  1255. if err != nil && err != io.EOF {
  1256. return nil
  1257. }
  1258. for index := range record {
  1259. record[index] = strings.TrimSpace(record[index])
  1260. }
  1261. return record
  1262. }
  1263. func integerSet(values []int) map[int]struct{} {
  1264. result := make(map[int]struct{}, len(values))
  1265. for _, value := range values {
  1266. result[value] = struct{}{}
  1267. }
  1268. return result
  1269. }
  1270. func uniqueIntegers(values []int) []int {
  1271. if len(values) < 2 {
  1272. return values
  1273. }
  1274. result := values[:1]
  1275. for _, value := range values[1:] {
  1276. if value != result[len(result)-1] {
  1277. result = append(result, value)
  1278. }
  1279. }
  1280. return result
  1281. }
  1282. func sameIntegers(left, right []int) bool {
  1283. left = append([]int(nil), left...)
  1284. right = append([]int(nil), right...)
  1285. sort.Ints(left)
  1286. sort.Ints(right)
  1287. left = uniqueIntegers(left)
  1288. right = uniqueIntegers(right)
  1289. if len(left) != len(right) {
  1290. return false
  1291. }
  1292. for index := range left {
  1293. if left[index] != right[index] {
  1294. return false
  1295. }
  1296. }
  1297. return true
  1298. }