| 92 | } |
| 93 | |
| 94 | func (n *nativeAttachmentStore) VerifyDownload(token, messageID string, index int) bool { |
| 95 | if len(n.secret) == 0 { |
| 96 | return false // never validate against an empty key (see DownloadURL) |
| 97 | } |
| 98 | dot := strings.IndexByte(token, '.') |
| 99 | if dot < 0 { |
| 100 | return false |
| 101 | } |
| 102 | payloadB64, sigB64 := token[:dot], token[dot+1:] |
| 103 | payload, err := base64.RawURLEncoding.DecodeString(payloadB64) |
| 104 | if err != nil { |
| 105 | return false |
| 106 | } |
| 107 | gotSig, err := base64.RawURLEncoding.DecodeString(sigB64) |
| 108 | if err != nil { |
| 109 | return false |
| 110 | } |
| 111 | // Recompute the MAC over the presented payload and constant-time compare. |
| 112 | mac := hmac.New(sha256.New, n.secret) |
| 113 | mac.Write(payload) |
| 114 | if !hmac.Equal(gotSig, mac.Sum(nil)) { |
| 115 | return false |
| 116 | } |
| 117 | // Parse message_id|index|exp and bind to the requested attachment + check expiry. |
| 118 | parts := strings.Split(string(payload), "|") |
| 119 | if len(parts) != 3 { |
| 120 | return false |
| 121 | } |
| 122 | wantIndex, err := strconv.Atoi(parts[1]) |
| 123 | if err != nil || wantIndex != index || parts[0] != messageID { |
| 124 | return false |
| 125 | } |
| 126 | expUnix, err := strconv.ParseInt(parts[2], 10, 64) |
| 127 | if err != nil || !time.Now().Before(time.Unix(expUnix, 0)) { |
| 128 | return false // reject at/after the exact expiry second |
| 129 | } |
| 130 | return true |
| 131 | } |
| 132 | |
| 133 | // ── HTTP surface ───────────────────────────────────────────────────────────── |
| 134 | |