tryDecryptSubscription attempts to decrypt an RVSUB1-prefixed payload. Returns the plaintext when successful, or a specific error so callers can surface a meaningful message to the user instead of a generic "unsupported format".
(body string)
| 50 | // the plaintext when successful, or a specific error so callers can surface a |
| 51 | // meaningful message to the user instead of a generic "unsupported format". |
| 52 | func tryDecryptSubscription(body string) (string, error) { |
| 53 | body = strings.TrimSpace(body) |
| 54 | body = strings.TrimPrefix(body, utf8BOM) |
| 55 | body = strings.TrimSpace(body) |
| 56 | if !strings.HasPrefix(body, subscriptionMagic) { |
| 57 | return body, nil |
| 58 | } |
| 59 | if subscriptionEncryptKey == "" { |
| 60 | return "", ErrSubscriptionKeyMissing |
| 61 | } |
| 62 | keyHex := strings.TrimSpace(subscriptionEncryptKey) |
| 63 | keyBytes, err := hex.DecodeString(keyHex) |
| 64 | if err != nil { |
| 65 | return "", fmt.Errorf("decryption key is not valid hex: %w", err) |
| 66 | } |
| 67 | if len(keyBytes) != 32 { |
| 68 | return "", fmt.Errorf("decryption key must be 32 bytes (got %d)", len(keyBytes)) |
| 69 | } |
| 70 | |
| 71 | encoded := strings.Map(func(r rune) rune { |
| 72 | if r == ' ' || r == '\t' || r == '\n' || r == '\r' { |
| 73 | return -1 |
| 74 | } |
| 75 | return r |
| 76 | }, body[len(subscriptionMagic):]) |
| 77 | |
| 78 | data, err := decodeBase64Flexible(encoded) |
| 79 | if err != nil { |
| 80 | return "", fmt.Errorf("base64 decode failed: %w", err) |
| 81 | } |
| 82 | |
| 83 | block, err := aes.NewCipher(keyBytes) |
| 84 | if err != nil { |
| 85 | return "", fmt.Errorf("aes cipher init failed: %w", err) |
| 86 | } |
| 87 | gcm, err := cipher.NewGCM(block) |
| 88 | if err != nil { |
| 89 | return "", fmt.Errorf("gcm init failed: %w", err) |
| 90 | } |
| 91 | |
| 92 | if len(data) < gcm.NonceSize()+gcm.Overhead() { |
| 93 | return "", fmt.Errorf("ciphertext too short (%d bytes, need at least %d)", len(data), gcm.NonceSize()+gcm.Overhead()) |
| 94 | } |
| 95 | |
| 96 | nonce, ciphertext := data[:gcm.NonceSize()], data[gcm.NonceSize():] |
| 97 | plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) |
| 98 | if err != nil { |
| 99 | return "", fmt.Errorf("gcm open failed (wrong key or corrupted payload): %w", err) |
| 100 | } |
| 101 | return string(plaintext), nil |
| 102 | } |
| 103 | |
| 104 | // decodeBase64Flexible tries standard, URL-safe, and raw (no-padding) variants. |
| 105 | func decodeBase64Flexible(s string) ([]byte, error) { |