mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-19 22:33:43 +08:00
114 lines
3.7 KiB
Go
114 lines
3.7 KiB
Go
package device
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
// These errors mirror the standardized SGP.22 DeleteProfileResponse values.
|
|
// Keep them exported so the HTTP layer can return actionable API errors
|
|
// instead of leaking raw BER-TLV response bytes to the UI.
|
|
ErrESIMDeleteProfileNotFound = errors.New("esim: profile was not found on the eUICC")
|
|
ErrESIMDeleteProfileNotDisabled = errors.New("esim: the active profile cannot be deleted; enable another profile first")
|
|
ErrESIMDeleteDisallowedByPolicy = errors.New("esim: profile deletion is not allowed by its policy")
|
|
)
|
|
|
|
// EsimDeleteResult reports storage reclaimed by a successful ES10c delete.
|
|
type EsimDeleteResult struct {
|
|
SpaceDelta int64
|
|
Warning string
|
|
}
|
|
|
|
func buildDeleteProfileRequest(iccid string) ([]byte, error) {
|
|
bcd, err := encodeICCID(strings.TrimSpace(iccid))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// SGP.22 ES10c DeleteProfileRequest: BF33 { 5A <ICCID BCD> }.
|
|
return derConstruct(0xBF33, derEncode(0x5A, bcd)), nil
|
|
}
|
|
|
|
func deleteProfileResult(payload []byte) (byte, bool) {
|
|
nodes := derParse(payload)
|
|
if len(nodes) != 1 || nodes[0].tag != 0xBF33 {
|
|
return 0, false
|
|
}
|
|
result := derFindValue(payload, 0x80)
|
|
if len(result) != 1 {
|
|
return 0, false
|
|
}
|
|
return result[0], true
|
|
}
|
|
|
|
func deleteProfileResponseError(result byte, payload []byte) error {
|
|
raw := strings.ToUpper(hex.EncodeToString(payload))
|
|
switch result {
|
|
case 0:
|
|
return nil
|
|
case 1:
|
|
return fmt.Errorf("%w (result=0x%02X, raw %s)", ErrESIMDeleteProfileNotFound, result, raw)
|
|
case 2:
|
|
return fmt.Errorf("%w (result=0x%02X, raw %s)", ErrESIMDeleteProfileNotDisabled, result, raw)
|
|
case 3:
|
|
return fmt.Errorf("%w (result=0x%02X, raw %s)", ErrESIMDeleteDisallowedByPolicy, result, raw)
|
|
default:
|
|
return fmt.Errorf("esim: eUICC rejected DeleteProfile, result=0x%02X (raw %s)", result, raw)
|
|
}
|
|
}
|
|
|
|
// ESIMDeleteProfile removes one installed, disabled profile through ES10c.
|
|
// Root CI certificates are not involved in local profile management; the
|
|
// eUICC authorizes this operation through its ISD-R interface.
|
|
func (manager *Manager) ESIMDeleteProfile(ctx context.Context, id, iccid, aidHex string) (*EsimDeleteResult, error) {
|
|
request, err := buildDeleteProfileRequest(iccid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
manager.esimMu.Lock()
|
|
defer manager.esimMu.Unlock()
|
|
if err := manager.waitForESIMRecovery(ctx, id); err != nil {
|
|
return nil, err
|
|
}
|
|
channel, err := manager.openEuiccAID(ctx, id, targetEuiccAID(aidHex))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer channel.close(context.Background())
|
|
|
|
freeBefore, beforeKnown := 0, false
|
|
if info2, infoErr := channel.getEUICCInfo2(ctx); infoErr == nil {
|
|
freeBefore, beforeKnown = euiccFreeNVRAM(info2)
|
|
}
|
|
|
|
// DeleteProfile is non-idempotent. Once submitted, finish reading the card's
|
|
// result even if the browser request is cancelled.
|
|
commitContext, cancelCommit := context.WithTimeout(context.WithoutCancel(ctx), csimAPDUTimeout)
|
|
payload, err := channel.es10(commitContext, request)
|
|
cancelCommit()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result, ok := deleteProfileResult(payload)
|
|
if !ok {
|
|
return nil, fmt.Errorf("esim: unexpected DeleteProfile response %s", strings.ToUpper(hex.EncodeToString(payload)))
|
|
}
|
|
if err := deleteProfileResponseError(result, payload); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
deleted := &EsimDeleteResult{}
|
|
if info2, infoErr := channel.getEUICCInfo2(ctx); infoErr == nil {
|
|
if freeAfter, afterKnown := euiccFreeNVRAM(info2); beforeKnown && afterKnown && freeAfter >= freeBefore {
|
|
deleted.SpaceDelta = int64(freeAfter - freeBefore)
|
|
}
|
|
} else {
|
|
deleted.Warning = "Profile was deleted, but reclaimed storage could not be read"
|
|
}
|
|
manager.removeCachedProfile(id, strings.TrimSpace(iccid))
|
|
return deleted, nil
|
|
}
|