Diff -- /var/www/vhosts/3dshawn.com/abforge.3dshawn.com/MODS/ABForge/SignedDownload.pm
Diff

/var/www/vhosts/3dshawn.com/abforge.3dshawn.com/MODS/ABForge/SignedDownload.pm

added on local at 2026-07-01 16:01:29

Added
+106
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to d0a3da217424
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1package MODS::ABForge::SignedDownload;
2#======================================================================
3# ABForge - signed-URL helper for receipt-email download links.
4#
5# Buyers who check out as a guest (no buyer_account on the storefront)
6# can't authenticate to /download.cgi via cookie. Receipt emails embed
7# a signed token that proves "this URL was minted by us, for this
8# (order_id, file_id), within the 30-day download window". The token
9# is short and url-safe.
10#
11# Token format:
12# <order_id>.<file_id>.<paid_at_epoch>.<hmac>
13# where hmac = first 32 hex chars of HMAC-SHA256(join('|', the three
14# preceding parts), secret).
15#
16# verify() must:
17# - parse the four dot-separated parts
18# - recompute the HMAC over the first three parts
19# - constant-time compare against the supplied hmac
20# - check that paid_at_epoch is within the 30-day window vs now
21# It returns (ok=>1, order_id=>N, file_id=>M, paid_at=>EPOCH) on success
22# or (ok=>0, error=>'reason') otherwise.
23#
24# This module never opens a DB connection -- it just proves the URL was
25# minted by us. download.cgi still authorizes against the order row.
26#======================================================================
27
28use strict;
29use warnings;
30use Digest::SHA qw(hmac_sha256_hex);
31use MODS::ABForge::Config;
32
33# Window must match download.cgi's $DOWNLOAD_WINDOW_DAYS. Kept here as
34# a constant so mint() refuses to create tokens that would be rejected
35# by verify() before they reach the buyer's inbox.
36use constant DOWNLOAD_WINDOW_SECONDS => 30 * 86400;
37
38sub _secret {
39 my $cfg = MODS::ABForge::Config->new;
40 return $cfg->settings('stripe_webhook_secret')
41 || $cfg->settings('stripe_secret')
42 || 'abforge-static-fallback';
43}
44
45# Build the canonical message we sign over. Order matters -- changing
46# this format invalidates every token in flight.
47sub _canonical {
48 my ($order_id, $file_id, $paid_epoch) = @_;
49 return "$order_id|$file_id|$paid_epoch";
50}
51
52# mint($order_id, $file_id, $paid_at_unix) -> string token
53# $paid_at_unix is the epoch seconds value of orders.paid_at. Pass as
54# integer; helper does not parse SQL timestamps.
55sub mint {
56 my ($class, $order_id, $file_id, $paid_at_unix) = @_;
57 $order_id = 0 + ($order_id || 0);
58 $file_id = 0 + ($file_id || 0);
59 $paid_at_unix = 0 + ($paid_at_unix || 0);
60 return '' unless $order_id && $file_id && $paid_at_unix;
61
62 my $canon = _canonical($order_id, $file_id, $paid_at_unix);
63 my $hmac = substr(hmac_sha256_hex($canon, _secret()), 0, 32);
64 return "$order_id.$file_id.$paid_at_unix.$hmac";
65}
66
67# verify($token) -> hash { ok, order_id, file_id, paid_at, error }
68sub verify {
69 my ($class, $token) = @_;
70 return { ok => 0, error => 'missing token' } unless defined $token && length $token;
71
72 # Format guard: exactly four dot-separated parts, each non-empty
73 # and a positive integer for the first three, 32 hex chars for the
74 # signature.
75 my @parts = split /\./, $token, 5;
76 return { ok => 0, error => 'malformed token' } unless @parts == 4;
77 my ($o, $f, $p, $sig) = @parts;
78 return { ok => 0, error => 'malformed token' }
79 unless $o =~ /^\d+$/ && $f =~ /^\d+$/ && $p =~ /^\d+$/ && $sig =~ /^[a-f0-9]{32}$/i;
80
81 my $canon = _canonical($o + 0, $f + 0, $p + 0);
82 my $expected = substr(hmac_sha256_hex($canon, _secret()), 0, 32);
83
84 # Constant-time compare. Same-length strings xor'd byte by byte.
85 return { ok => 0, error => 'bad signature' } if length($sig) != length($expected);
86 my $diff = 0;
87 for my $i (0 .. length($expected) - 1) {
88 $diff |= ord(substr($expected, $i, 1)) ^ ord(substr($sig, $i, 1));
89 }
90 return { ok => 0, error => 'bad signature' } if $diff;
91
92 # Window check: paid_at_epoch must be within the last DOWNLOAD_WINDOW
93 # seconds. We check here too -- belt-and-suspenders against a stale
94 # token surviving past the download.cgi window check.
95 if (time() - ($p + 0) > DOWNLOAD_WINDOW_SECONDS) {
96 return { ok => 0, error => 'token expired' };
97 }
98 return {
99 ok => 1,
100 order_id => $o + 0,
101 file_id => $f + 0,
102 paid_at => $p + 0,
103 };
104}
105
1061;
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help