esim_delete.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. package device
  2. import (
  3. "context"
  4. "encoding/hex"
  5. "errors"
  6. "fmt"
  7. "strings"
  8. )
  9. var (
  10. // These errors mirror the standardized SGP.22 DeleteProfileResponse values.
  11. // Keep them exported so the HTTP layer can return actionable API errors
  12. // instead of leaking raw BER-TLV response bytes to the UI.
  13. ErrESIMDeleteProfileNotFound = errors.New("esim: profile was not found on the eUICC")
  14. ErrESIMDeleteProfileNotDisabled = errors.New("esim: the active profile cannot be deleted; enable another profile first")
  15. ErrESIMDeleteDisallowedByPolicy = errors.New("esim: profile deletion is not allowed by its policy")
  16. )
  17. // EsimDeleteResult reports storage reclaimed by a successful ES10c delete.
  18. type EsimDeleteResult struct {
  19. SpaceDelta int64
  20. Warning string
  21. }
  22. func buildDeleteProfileRequest(iccid string) ([]byte, error) {
  23. bcd, err := encodeICCID(strings.TrimSpace(iccid))
  24. if err != nil {
  25. return nil, err
  26. }
  27. // SGP.22 ES10c DeleteProfileRequest: BF33 { 5A <ICCID BCD> }.
  28. return derConstruct(0xBF33, derEncode(0x5A, bcd)), nil
  29. }
  30. func deleteProfileResult(payload []byte) (byte, bool) {
  31. nodes := derParse(payload)
  32. if len(nodes) != 1 || nodes[0].tag != 0xBF33 {
  33. return 0, false
  34. }
  35. result := derFindValue(payload, 0x80)
  36. if len(result) != 1 {
  37. return 0, false
  38. }
  39. return result[0], true
  40. }
  41. func deleteProfileResponseError(result byte, payload []byte) error {
  42. raw := strings.ToUpper(hex.EncodeToString(payload))
  43. switch result {
  44. case 0:
  45. return nil
  46. case 1:
  47. return fmt.Errorf("%w (result=0x%02X, raw %s)", ErrESIMDeleteProfileNotFound, result, raw)
  48. case 2:
  49. return fmt.Errorf("%w (result=0x%02X, raw %s)", ErrESIMDeleteProfileNotDisabled, result, raw)
  50. case 3:
  51. return fmt.Errorf("%w (result=0x%02X, raw %s)", ErrESIMDeleteDisallowedByPolicy, result, raw)
  52. default:
  53. return fmt.Errorf("esim: eUICC rejected DeleteProfile, result=0x%02X (raw %s)", result, raw)
  54. }
  55. }
  56. // ESIMDeleteProfile removes one installed, disabled profile through ES10c.
  57. // Root CI certificates are not involved in local profile management; the
  58. // eUICC authorizes this operation through its ISD-R interface.
  59. func (manager *Manager) ESIMDeleteProfile(ctx context.Context, id, iccid, aidHex string) (*EsimDeleteResult, error) {
  60. request, err := buildDeleteProfileRequest(iccid)
  61. if err != nil {
  62. return nil, err
  63. }
  64. manager.esimMu.Lock()
  65. defer manager.esimMu.Unlock()
  66. if err := manager.waitForESIMRecovery(ctx, id); err != nil {
  67. return nil, err
  68. }
  69. channel, err := manager.openEuiccAID(ctx, id, targetEuiccAID(aidHex))
  70. if err != nil {
  71. return nil, err
  72. }
  73. defer channel.close(context.Background())
  74. freeBefore, beforeKnown := 0, false
  75. if info2, infoErr := channel.getEUICCInfo2(ctx); infoErr == nil {
  76. freeBefore, beforeKnown = euiccFreeNVRAM(info2)
  77. }
  78. // DeleteProfile is non-idempotent. Once submitted, finish reading the card's
  79. // result even if the browser request is cancelled.
  80. commitContext, cancelCommit := context.WithTimeout(context.WithoutCancel(ctx), csimAPDUTimeout)
  81. payload, err := channel.es10(commitContext, request)
  82. cancelCommit()
  83. if err != nil {
  84. return nil, err
  85. }
  86. result, ok := deleteProfileResult(payload)
  87. if !ok {
  88. return nil, fmt.Errorf("esim: unexpected DeleteProfile response %s", strings.ToUpper(hex.EncodeToString(payload)))
  89. }
  90. if err := deleteProfileResponseError(result, payload); err != nil {
  91. return nil, err
  92. }
  93. deleted := &EsimDeleteResult{}
  94. if info2, infoErr := channel.getEUICCInfo2(ctx); infoErr == nil {
  95. if freeAfter, afterKnown := euiccFreeNVRAM(info2); beforeKnown && afterKnown && freeAfter >= freeBefore {
  96. deleted.SpaceDelta = int64(freeAfter - freeBefore)
  97. }
  98. } else {
  99. deleted.Warning = "Profile was deleted, but reclaimed storage could not be read"
  100. }
  101. manager.removeCachedProfile(id, strings.TrimSpace(iccid))
  102. return deleted, nil
  103. }