Line data Source code
1 : import 'package:flutter/foundation.dart';
2 :
3 : /// HMAC-based request signing configuration for HTTP workers.
4 : ///
5 : /// When set on an HTTP worker, each request is signed using HMAC-SHA256 and
6 : /// the signature is added as a request header (default: `X-Signature`).
7 : /// An optional `X-Timestamp` header is also added (in milliseconds since epoch)
8 : /// so that servers can reject replayed requests that are too old.
9 : ///
10 : /// ## What gets signed
11 : ///
12 : /// The signature covers a canonical message composed of:
13 : /// ```
14 : /// METHOD\n
15 : /// URL\n
16 : /// BODY\n ← only when signBody=true and there is a body
17 : /// TIMESTAMP ← only when includeTimestamp=true
18 : /// ```
19 : ///
20 : /// ## Example — download with server validation
21 : ///
22 : /// ```dart
23 : /// worker: HttpDownloadWorker(
24 : /// url: 'https://api.example.com/protected/report.pdf',
25 : /// savePath: '/tmp/report.pdf',
26 : /// requestSigning: RequestSigning(
27 : /// secretKey: env['API_SECRET']!,
28 : /// ),
29 : /// ),
30 : /// ```
31 : ///
32 : /// ## Example — upload with non-default header name
33 : ///
34 : /// ```dart
35 : /// worker: HttpUploadWorker(
36 : /// url: 'https://api.example.com/upload',
37 : /// filePath: '/tmp/data.csv',
38 : /// requestSigning: RequestSigning(
39 : /// secretKey: env['API_SECRET']!,
40 : /// headerName: 'X-Hub-Signature-256',
41 : /// signaturePrefix: 'sha256=',
42 : /// ),
43 : /// ),
44 : /// ```
45 : ///
46 : /// ## Server-side verification (example in Node.js)
47 : ///
48 : /// ```js
49 : /// const crypto = require('crypto');
50 : /// function verify(req, secret) {
51 : /// const ts = req.headers['x-timestamp'] ?? '';
52 : /// const sig = req.headers['x-signature'] ?? '';
53 : /// const body = req.rawBody ?? '';
54 : /// const msg = `${req.method}\n${req.url}\n${body}\n${ts}`;
55 : /// const expected = crypto.createHmac('sha256', secret).update(msg).digest('hex');
56 : /// return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
57 : /// }
58 : /// ```
59 : @immutable
60 : class RequestSigning {
61 1 : const RequestSigning({
62 : required this.secretKey,
63 : this.headerName = 'X-Signature',
64 : this.signaturePrefix = '',
65 : this.includeTimestamp = true,
66 : this.signBody = true,
67 3 : }) : assert(secretKey.length >= 16,
68 : 'secretKey must be at least 16 characters for meaningful security');
69 :
70 : /// The shared secret used to compute the HMAC-SHA256 digest.
71 : ///
72 : /// Must be at least 16 characters. Keep this out of source control —
73 : /// read it from a secure secret store at runtime.
74 : final String secretKey;
75 :
76 : /// Name of the HTTP header that carries the signature.
77 : ///
78 : /// Default: `'X-Signature'`
79 : final String headerName;
80 :
81 : /// Optional string prepended to the raw hex digest in [headerName].
82 : ///
83 : /// GitHub-style webhooks use `'sha256='`. Leave empty for a bare hex string.
84 : ///
85 : /// Default: `''` (bare hex)
86 : final String signaturePrefix;
87 :
88 : /// When `true` (default), the current Unix timestamp in **milliseconds** is
89 : /// included in the signed message and sent as an `X-Timestamp` header.
90 : ///
91 : /// Servers should reject requests whose `X-Timestamp` is more than
92 : /// a configurable window (e.g. 5 minutes) in the past to prevent replay attacks.
93 : final bool includeTimestamp;
94 :
95 : /// When `true` (default), the request body bytes are included in the signed
96 : /// message.
97 : ///
98 : /// For GET/HEAD requests (no body), this has no effect.
99 : /// For large upload bodies this adds a small CPU cost.
100 : final bool signBody;
101 :
102 4 : Map<String, dynamic> toMap() => {
103 2 : 'secretKey': secretKey,
104 2 : 'headerName': headerName,
105 2 : 'signaturePrefix': signaturePrefix,
106 2 : 'includeTimestamp': includeTimestamp,
107 2 : 'signBody': signBody,
108 : };
109 : }
|