Skip to content

Commit d9544c6

Browse files
cursoragentlovasoa
andcommitted
feat: Add base64 output option to hmac function
Co-authored-by: contact <[email protected]>
1 parent 176b49f commit d9544c6

File tree

8 files changed

+83
-60
lines changed

8 files changed

+83
-60
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
- Generate tamper-proof tokens for API authentication
77
- Secure download links and temporary access codes
88
- Supports SHA-256 (default) and SHA-512 algorithms
9-
- Returns hexadecimal string representation of HMAC
9+
- Output formats: hexadecimal (default) or base64 (e.g., `sha256-base64`)
1010
- See the [function documentation](https://sql-page.com/functions.sql?function=hmac) for detailed examples
1111

1212
## v0.37.1

examples/official-site/sqlpage/migrations/67_hmac_function.sql

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,13 @@ Think of it like a wax seal on a letter - only someone with the right seal (your
2727
The `sqlpage.hmac` function takes three inputs:
2828
1. **Your data** - The text you want to sign (like a message or request body)
2929
2. **Your secret key** - A password only you know (keep this safe!)
30-
3. **Algorithm** (optional) - Either `sha256` (default) or `sha512`
30+
3. **Algorithm** (optional) - The hash algorithm and output format:
31+
- `sha256` (default) - SHA-256 with hexadecimal output
32+
- `sha256-base64` - SHA-256 with base64 output
33+
- `sha512` - SHA-512 with hexadecimal output
34+
- `sha512-base64` - SHA-512 with base64 output
3135
32-
It returns a long string of letters and numbers (the signature). If someone changes even one letter in your data, the signature will be completely different.
36+
It returns a signature string. If someone changes even one letter in your data, the signature will be completely different.
3337
3438
### Example 1: Verify Shopify Webhooks
3539
@@ -39,27 +43,21 @@ When Shopify sends you a webhook (like when someone places an order), it include
3943
-- Shopify includes the signature in the X-Shopify-Hmac-SHA256 header
4044
-- and sends the webhook data in the request body
4145
42-
SELECT ''text'' as component,
43-
CASE
44-
WHEN sqlpage.hmac(
45-
sqlpage.request_body(),
46-
sqlpage.environment_variable(''SHOPIFY_WEBHOOK_SECRET''),
47-
''sha256''
48-
) = sqlpage.header(''X-Shopify-Hmac-SHA256'')
49-
THEN ''✅ Webhook verified! This is really from Shopify.''
50-
ELSE ''❌ Invalid signature - this might be fake!''
51-
END as contents;
52-
53-
-- If verified, process the order:
54-
INSERT INTO orders (order_data, received_at)
55-
SELECT
56-
sqlpage.request_body(),
57-
datetime(''now'')
46+
-- First, verify the signature - redirect to error page if invalid
47+
SELECT ''redirect'' as component,
48+
''/error.sql?message='' || sqlpage.url_encode(''Invalid webhook signature'') as link
5849
WHERE sqlpage.hmac(
5950
sqlpage.request_body(),
6051
sqlpage.environment_variable(''SHOPIFY_WEBHOOK_SECRET''),
61-
''sha256''
62-
) = sqlpage.header(''X-Shopify-Hmac-SHA256'');
52+
''sha256-base64''
53+
) != sqlpage.header(''X-Shopify-Hmac-SHA256'');
54+
55+
-- If we reach here, the signature is valid - process the order:
56+
INSERT INTO orders (order_data, received_at)
57+
VALUES (sqlpage.request_body(), datetime(''now''));
58+
59+
SELECT ''text'' as component,
60+
''✅ Webhook verified and processed successfully!'' as contents;
6361
```
6462
6563
### Example 2: Create Secure Download Links
@@ -99,6 +97,7 @@ SELECT sqlpage.hmac(
9997
- **Use strong keys**: Your secret should be long and random (at least 32 characters)
10098
- **The signature is case-sensitive**: Even one wrong letter means the signature won''t match
10199
- **Algorithms**: Use `sha256` for most cases (it''s the default), or `sha512` for extra security
100+
- **Output formats**: Use `hex` (default) for most cases, or `base64` when the service expects base64 (like Shopify)
102101
- **NULL handling**: If your data or key is NULL, the function returns NULL
103102
'
104103
);
@@ -128,6 +127,6 @@ VALUES (
128127
'hmac',
129128
3,
130129
'algorithm',
131-
'The hash algorithm to use. Optional, defaults to `sha256`. Supported values: `sha256`, `sha512`.',
130+
'The hash algorithm and output format. Optional, defaults to `sha256` (hex output). Supported values: `sha256`, `sha256-base64`, `sha512`, `sha512-base64`.',
132131
'TEXT'
133132
);

src/webserver/database/sqlpage_functions/functions.rs

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -757,7 +757,15 @@ async fn hmac<'a>(
757757
};
758758

759759
let algorithm = algorithm.as_deref().unwrap_or("sha256");
760-
let result = match algorithm.to_lowercase().as_str() {
760+
761+
// Parse algorithm and output format (e.g., "sha256" or "sha256-base64")
762+
let (hash_algo, output_format) = if let Some((algo, format)) = algorithm.split_once('-') {
763+
(algo, format)
764+
} else {
765+
(algorithm, "hex")
766+
};
767+
768+
let result = match hash_algo.to_lowercase().as_str() {
761769
"sha256" => {
762770
let mut mac = Hmac::<Sha256>::new_from_slice(key.as_bytes())
763771
.map_err(|e| anyhow!("Invalid HMAC key: {e}"))?;
@@ -772,18 +780,30 @@ async fn hmac<'a>(
772780
}
773781
_ => {
774782
anyhow::bail!(
775-
"Unsupported HMAC algorithm: {algorithm}. Supported algorithms: sha256, sha512"
783+
"Unsupported HMAC algorithm: {hash_algo}. Supported algorithms: sha256, sha512"
776784
)
777785
}
778786
};
779787

780-
// Convert to hexadecimal string
781-
let hex_result = result.into_iter().fold(String::new(), |mut acc, byte| {
782-
write!(&mut acc, "{byte:02x}").unwrap();
783-
acc
784-
});
788+
// Convert to requested output format
789+
let output = match output_format.to_lowercase().as_str() {
790+
"hex" => {
791+
result.into_iter().fold(String::new(), |mut acc, byte| {
792+
write!(&mut acc, "{byte:02x}").unwrap();
793+
acc
794+
})
795+
}
796+
"base64" => {
797+
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, result)
798+
}
799+
_ => {
800+
anyhow::bail!(
801+
"Unsupported output format: {output_format}. Supported formats: hex, base64"
802+
)
803+
}
804+
};
785805

786-
Ok(Some(hex_result))
806+
Ok(Some(output))
787807
}
788808

789809
async fn client_ip(request: &RequestInfo) -> Option<String> {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-- Test HMAC with base64 output format
2+
-- Redirect if hash doesn't match expected value
3+
SELECT 'redirect' as component, '/error.sql' as link
4+
WHERE sqlpage.hmac('The quick brown fox jumps over the lazy dog', 'key', 'sha256-base64') != '97yD9DBThCSxMpjmqm+xQ+9NWaFJRhdZl0edvC0aPNg=';
5+
6+
SELECT 'text' as component, 'It works ! HMAC SHA-256 base64 output is correct' as contents;
Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
SELECT 'text' as component,
2-
CASE
3-
WHEN sqlpage.hmac('test data', 'test key') = sqlpage.hmac('test data', 'test key', 'sha256')
4-
THEN 'It works ! HMAC default algorithm is SHA-256'
5-
ELSE 'Default algorithm mismatch'
6-
END as contents;
1+
-- Redirect if default algorithm doesn't match sha256
2+
SELECT 'redirect' as component, '/error.sql' as link
3+
WHERE sqlpage.hmac('test data', 'test key') != sqlpage.hmac('test data', 'test key', 'sha256');
4+
5+
SELECT 'text' as component, 'It works ! HMAC default algorithm is SHA-256' as contents;
Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
SELECT 'text' as component,
2-
CASE
3-
WHEN sqlpage.hmac('The quick brown fox jumps over the lazy dog', 'key', 'sha256') = 'f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8'
4-
THEN 'It works ! HMAC SHA-256 hash is correct'
5-
ELSE 'Hash mismatch: ' || sqlpage.hmac('The quick brown fox jumps over the lazy dog', 'key', 'sha256')
6-
END as contents;
1+
-- Redirect if hash doesn't match expected value
2+
SELECT 'redirect' as component, '/error.sql' as link
3+
WHERE sqlpage.hmac('The quick brown fox jumps over the lazy dog', 'key', 'sha256') != 'f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8';
4+
5+
SELECT 'text' as component, 'It works ! HMAC SHA-256 hash is correct' as contents;
Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
SELECT 'text' as component,
2-
CASE
3-
WHEN sqlpage.hmac('The quick brown fox jumps over the lazy dog', 'key', 'sha512') = 'b42af09057bac1e2d41708e48a902e09b5ff7f12ab428a4fe86653c73dd248fb82f948a549f7b791a5b41915ee4d1ec3935357e4e2317250d0372afa2ebeeb3a'
4-
THEN 'It works ! HMAC SHA-512 hash is correct'
5-
ELSE 'Hash mismatch: ' || sqlpage.hmac('The quick brown fox jumps over the lazy dog', 'key', 'sha512')
6-
END as contents;
1+
-- Redirect if hash doesn't match expected value
2+
SELECT 'redirect' as component, '/error.sql' as link
3+
WHERE sqlpage.hmac('The quick brown fox jumps over the lazy dog', 'key', 'sha512') != 'b42af09057bac1e2d41708e48a902e09b5ff7f12ab428a4fe86653c73dd248fb82f948a549f7b791a5b41915ee4d1ec3935357e4e2317250d0372afa2ebeeb3a';
4+
5+
SELECT 'text' as component, 'It works ! HMAC SHA-512 hash is correct' as contents;
Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
-- Test Shopify webhook HMAC validation
2-
-- Shopify sends webhook body and HMAC signature in X-Shopify-Hmac-SHA256 header
1+
-- Test Shopify webhook HMAC validation with base64 output
2+
-- Shopify sends webhook body and HMAC signature in X-Shopify-Hmac-SHA256 header (base64 format)
33

4+
-- Redirect to error if signature doesn't match (proper pattern)
5+
SELECT 'redirect' as component,
6+
'/error.sql?msg=invalid_signature' as link
7+
WHERE sqlpage.hmac(
8+
'{"id":1234567890,"email":"[email protected]","total_price":"123.45"}',
9+
'test-webhook-secret',
10+
'sha256-base64'
11+
) != 'QNyObTlKbMx2qDlPF/ZOZcBqg5OgPg+2oky3zldc0Gw=';
12+
13+
-- If we reach here, signature is valid
414
SELECT 'text' as component,
5-
CASE
6-
-- Example webhook data and signature (simulating Shopify webhook)
7-
WHEN sqlpage.hmac(
8-
'{"id":1234567890,"email":"[email protected]","total_price":"123.45"}',
9-
'test-webhook-secret',
10-
'sha256'
11-
) = '40dc8e6d394a6ccc76a8394f17f64e65c06a8393a03e0fb6a24cb7ce575cd06c'
12-
THEN 'It works ! Shopify webhook signature verified'
13-
ELSE 'Signature mismatch: ' || sqlpage.hmac('{"id":1234567890,"email":"[email protected]","total_price":"123.45"}', 'test-webhook-secret', 'sha256')
14-
END as contents;
15+
'It works ! Shopify webhook signature verified (base64 format)' as contents;

0 commit comments

Comments
 (0)