@push.rocks/smartmta 9.1.1 → 9.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +10 -0
- package/dist_rust/mailer-bin_linux_amd64 +0 -0
- package/dist_rust/mailer-bin_linux_arm64 +0 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/mail/routing/functions.routing.validation.d.ts +22 -1
- package/dist_ts/mail/routing/functions.routing.validation.js +44 -1
- package/package.json +1 -1
- package/readme.md +2 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/mail/routing/functions.routing.validation.ts +53 -0
package/changelog.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026-07-26 - 9.2.0
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- add SCRAM-SHA-256 verifier derivation helper (auth)
|
|
8
|
+
- `deriveScramVerifier(password, iterations = SCRAM_DEFAULT_ITERATIONS)` is the canonical embedder-facing RFC 5802 derivation (fresh random 16-byte salt), matching exactly what `UnifiedEmailServer` verifies at AUTH time; bounds are enforced at derivation.
|
|
9
|
+
- `SCRAM_DEFAULT_ITERATIONS` is 600,000 (OWASP-aligned for PBKDF2-HMAC-SHA256) — machine-generated high-entropy secrets gain nothing from high counts and callers may pass lower values within the accepted bounds.
|
|
10
|
+
- Covered by validation, reference-derivation, and end-to-end PLAIN authentication tests.
|
|
11
|
+
- Known follow-ups (not in this release): the private verify-side `computeScramCredentials` still duplicates the derivation formula and should converge onto the shared helper; the legacy plaintext-credential path also still derives SCRAM fields with a hardcoded 4096 iterations — one more reason for embedders to finish moving to stored verifiers.
|
|
12
|
+
|
|
3
13
|
## 2026-07-26 - 9.1.1
|
|
4
14
|
|
|
5
15
|
### Changes
|
|
Binary file
|
|
Binary file
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@push.rocks/smartmta',
|
|
6
|
-
version: '9.
|
|
6
|
+
version: '9.2.0',
|
|
7
7
|
description: 'A high-performance, enterprise-grade Mail Transfer Agent (MTA) built from scratch in TypeScript with Rust acceleration.'
|
|
8
8
|
};
|
|
9
9
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSxzQkFBc0I7SUFDNUIsT0FBTyxFQUFFLE9BQU87SUFDaEIsV0FBVyxFQUFFLHlIQUF5SDtDQUN2SSxDQUFBIn0=
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ISmtpAuthAccount } from './interfaces.js';
|
|
1
|
+
import type { ISmtpAuthAccount, ISmtpScramVerifier } from './interfaces.js';
|
|
2
2
|
/**
|
|
3
3
|
* PBKDF2 iteration bounds for SCRAM verifiers. The lower bound is the RFC
|
|
4
4
|
* 5802 recommended minimum; the upper bound caps per-attempt CPU so a single
|
|
@@ -16,6 +16,27 @@ export declare function assertValidAuthAccounts(accounts: ISmtpAuthAccount[] | u
|
|
|
16
16
|
* Reject retention values that cannot be represented and scheduled reliably.
|
|
17
17
|
*/
|
|
18
18
|
export declare function assertValidRetentionDays(retentionDays: unknown, owner: string): asserts retentionDays is number;
|
|
19
|
+
/**
|
|
20
|
+
* Default PBKDF2 iteration count for freshly derived SCRAM verifiers —
|
|
21
|
+
* OWASP-aligned for PBKDF2-HMAC-SHA256, protecting human-chosen passwords
|
|
22
|
+
* against offline attack on a leaked verifier store. Costs roughly 100-150ms
|
|
23
|
+
* per derivation and per PLAIN/LOGIN authentication on the bounded async
|
|
24
|
+
* pool, still far inside the Rust bridge's 5s callback window. Callers whose
|
|
25
|
+
* passwords are machine-generated high-entropy secrets gain nothing from
|
|
26
|
+
* high counts and may pass a lower value within the accepted bounds.
|
|
27
|
+
*/
|
|
28
|
+
export declare const SCRAM_DEFAULT_ITERATIONS = 600000;
|
|
29
|
+
/**
|
|
30
|
+
* Derive an RFC 5802 SCRAM-SHA-256 verifier from a plaintext password with a
|
|
31
|
+
* fresh random 16-byte salt. This is the canonical embedder-facing derivation
|
|
32
|
+
* matching exactly what `UnifiedEmailServer` verifies at AUTH time — persist
|
|
33
|
+
* only the returned verifier, never the plaintext.
|
|
34
|
+
*
|
|
35
|
+
* SaltedPassword = PBKDF2-HMAC-SHA256(password, salt, iterations, 32);
|
|
36
|
+
* StoredKey = SHA256(HMAC-SHA256(SaltedPassword, 'Client Key'));
|
|
37
|
+
* ServerKey = HMAC-SHA256(SaltedPassword, 'Server Key').
|
|
38
|
+
*/
|
|
39
|
+
export declare function deriveScramVerifier(password: string, iterations?: number): ISmtpScramVerifier;
|
|
19
40
|
export declare function assertValidInboundPolicy(inboundPolicy: {
|
|
20
41
|
defaultAction: unknown;
|
|
21
42
|
retentionDays: unknown;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
1
2
|
/**
|
|
2
3
|
* PBKDF2 iteration bounds for SCRAM verifiers. The lower bound is the RFC
|
|
3
4
|
* 5802 recommended minimum; the upper bound caps per-attempt CPU so a single
|
|
@@ -104,6 +105,48 @@ export function assertValidRetentionDays(retentionDays, owner) {
|
|
|
104
105
|
throw new Error(`${owner} retentionDays must be a finite positive integer`);
|
|
105
106
|
}
|
|
106
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Default PBKDF2 iteration count for freshly derived SCRAM verifiers —
|
|
110
|
+
* OWASP-aligned for PBKDF2-HMAC-SHA256, protecting human-chosen passwords
|
|
111
|
+
* against offline attack on a leaked verifier store. Costs roughly 100-150ms
|
|
112
|
+
* per derivation and per PLAIN/LOGIN authentication on the bounded async
|
|
113
|
+
* pool, still far inside the Rust bridge's 5s callback window. Callers whose
|
|
114
|
+
* passwords are machine-generated high-entropy secrets gain nothing from
|
|
115
|
+
* high counts and may pass a lower value within the accepted bounds.
|
|
116
|
+
*/
|
|
117
|
+
export const SCRAM_DEFAULT_ITERATIONS = 600_000;
|
|
118
|
+
/**
|
|
119
|
+
* Derive an RFC 5802 SCRAM-SHA-256 verifier from a plaintext password with a
|
|
120
|
+
* fresh random 16-byte salt. This is the canonical embedder-facing derivation
|
|
121
|
+
* matching exactly what `UnifiedEmailServer` verifies at AUTH time — persist
|
|
122
|
+
* only the returned verifier, never the plaintext.
|
|
123
|
+
*
|
|
124
|
+
* SaltedPassword = PBKDF2-HMAC-SHA256(password, salt, iterations, 32);
|
|
125
|
+
* StoredKey = SHA256(HMAC-SHA256(SaltedPassword, 'Client Key'));
|
|
126
|
+
* ServerKey = HMAC-SHA256(SaltedPassword, 'Server Key').
|
|
127
|
+
*/
|
|
128
|
+
export function deriveScramVerifier(password, iterations = SCRAM_DEFAULT_ITERATIONS) {
|
|
129
|
+
if (typeof password !== 'string' || password.length === 0) {
|
|
130
|
+
throw new Error('Cannot derive a SCRAM verifier from an empty password');
|
|
131
|
+
}
|
|
132
|
+
if (!Number.isInteger(iterations)
|
|
133
|
+
|| iterations < SCRAM_MIN_ITERATIONS
|
|
134
|
+
|| iterations > SCRAM_MAX_ITERATIONS) {
|
|
135
|
+
throw new Error(`SCRAM verifier iterations must be an integer between ${SCRAM_MIN_ITERATIONS} and ${SCRAM_MAX_ITERATIONS}`);
|
|
136
|
+
}
|
|
137
|
+
const salt = crypto.randomBytes(16);
|
|
138
|
+
const saltedPassword = crypto.pbkdf2Sync(password, salt, iterations, 32, 'sha256');
|
|
139
|
+
const clientKey = crypto.createHmac('sha256', saltedPassword).update('Client Key').digest();
|
|
140
|
+
const storedKey = crypto.createHash('sha256').update(clientKey).digest();
|
|
141
|
+
const serverKey = crypto.createHmac('sha256', saltedPassword).update('Server Key').digest();
|
|
142
|
+
return {
|
|
143
|
+
algorithm: 'SCRAM-SHA-256',
|
|
144
|
+
salt: salt.toString('base64'),
|
|
145
|
+
iterations,
|
|
146
|
+
storedKey: storedKey.toString('base64'),
|
|
147
|
+
serverKey: serverKey.toString('base64'),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
107
150
|
export function assertValidInboundPolicy(inboundPolicy, domain) {
|
|
108
151
|
if (inboundPolicy.defaultAction !== 'store'
|
|
109
152
|
&& inboundPolicy.defaultAction !== 'reject') {
|
|
@@ -111,4 +154,4 @@ export function assertValidInboundPolicy(inboundPolicy, domain) {
|
|
|
111
154
|
}
|
|
112
155
|
assertValidRetentionDays(inboundPolicy.retentionDays, `Inbound policy for domain ${domain}`);
|
|
113
156
|
}
|
|
114
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
157
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZnVuY3Rpb25zLnJvdXRpbmcudmFsaWRhdGlvbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3RzL21haWwvcm91dGluZy9mdW5jdGlvbnMucm91dGluZy52YWxpZGF0aW9uLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxNQUFNLE1BQU0sYUFBYSxDQUFDO0FBSXRDOzs7O0dBSUc7QUFDSCxNQUFNLENBQUMsTUFBTSxvQkFBb0IsR0FBRyxJQUFJLENBQUM7QUFDekMsTUFBTSxDQUFDLE1BQU0sb0JBQW9CLEdBQUcsU0FBUyxDQUFDO0FBRTlDLE1BQU0sZ0JBQWdCLEdBQUcsQ0FBQyxLQUFjLEVBQUUsVUFBbUIsRUFBVyxFQUFFO0lBQ3hFLElBQUksT0FBTyxLQUFLLEtBQUssUUFBUSxJQUFJLEtBQUssQ0FBQyxNQUFNLEtBQUssQ0FBQztRQUFFLE9BQU8sS0FBSyxDQUFDO0lBQ2xFLElBQUksQ0FBQztRQUNILE1BQU0sT0FBTyxHQUFHLE1BQU0sQ0FBQyxJQUFJLENBQUMsS0FBSyxFQUFFLFFBQVEsQ0FBQyxDQUFDO1FBQzdDLElBQUksT0FBTyxDQUFDLE1BQU0sS0FBSyxDQUFDO1lBQUUsT0FBTyxLQUFLLENBQUM7UUFDdkMsa0VBQWtFO1FBQ2xFLElBQUksT0FBTyxDQUFDLFFBQVEsQ0FBQyxRQUFRLENBQUMsQ0FBQyxPQUFPLENBQUMsS0FBSyxFQUFFLEVBQUUsQ0FBQyxLQUFLLEtBQUssQ0FBQyxPQUFPLENBQUMsS0FBSyxFQUFFLEVBQUUsQ0FBQyxFQUFFLENBQUM7WUFDL0UsT0FBTyxLQUFLLENBQUM7UUFDZixDQUFDO1FBQ0QsT0FBTyxVQUFVLEtBQUssU0FBUyxJQUFJLE9BQU8sQ0FBQyxNQUFNLEtBQUssVUFBVSxDQUFDO0lBQ25FLENBQUM7SUFBQyxNQUFNLENBQUM7UUFDUCxPQUFPLEtBQUssQ0FBQztJQUNmLENBQUM7QUFDSCxDQUFDLENBQUM7QUFFRjs7OztHQUlHO0FBQ0gsTUFBTSxV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
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -355,6 +355,8 @@ Enforcement semantics:
|
|
|
355
355
|
|
|
356
356
|
Configuration is validated loudly at construction and `updateOptions` time: unusable accounts (empty `scope.senders`, `mode: 'patterns'` without patterns, both or neither credential kind, malformed base64 keys, `iterations` outside `[4096, 1000000]`, duplicate usernames) throw instead of degrading into silent deny-all or silent auth failure. Note that `updateOptions({ auth })` replaces the whole `auth` block — include `users`, `required`, and `methods` again if they should persist.
|
|
357
357
|
|
|
358
|
+
Deriving verifiers: `deriveScramVerifier(password, iterations?)` is the canonical embedder-facing derivation, matching exactly what the server verifies at AUTH time — persist only the returned verifier. The exported `SCRAM_DEFAULT_ITERATIONS` (600,000) is OWASP-aligned to protect human-chosen passwords against offline attack on a leaked verifier store, at roughly 100-150ms per derivation and per `PLAIN`/`LOGIN` authentication on the bounded async pool. Machine-generated high-entropy secrets gain nothing from high counts — callers may pass any lower value within `[SCRAM_MIN_ITERATIONS, SCRAM_MAX_ITERATIONS]`. `assertValidAuthAccounts` is exported too, so embedders can pre-validate database-sourced account sets with the exact rules the server enforces.
|
|
359
|
+
|
|
358
360
|
Compatibility probe: `smartMtaCapabilities.smtpAuthAccountsAndScopes`.
|
|
359
361
|
|
|
360
362
|
### 📧 Sending Emails (Automatic MX Discovery)
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
|
|
1
3
|
import type { ISmtpAuthAccount, ISmtpScramVerifier } from './interfaces.js';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -134,6 +136,57 @@ export function assertValidRetentionDays(
|
|
|
134
136
|
}
|
|
135
137
|
}
|
|
136
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Default PBKDF2 iteration count for freshly derived SCRAM verifiers —
|
|
141
|
+
* OWASP-aligned for PBKDF2-HMAC-SHA256, protecting human-chosen passwords
|
|
142
|
+
* against offline attack on a leaked verifier store. Costs roughly 100-150ms
|
|
143
|
+
* per derivation and per PLAIN/LOGIN authentication on the bounded async
|
|
144
|
+
* pool, still far inside the Rust bridge's 5s callback window. Callers whose
|
|
145
|
+
* passwords are machine-generated high-entropy secrets gain nothing from
|
|
146
|
+
* high counts and may pass a lower value within the accepted bounds.
|
|
147
|
+
*/
|
|
148
|
+
export const SCRAM_DEFAULT_ITERATIONS = 600_000;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Derive an RFC 5802 SCRAM-SHA-256 verifier from a plaintext password with a
|
|
152
|
+
* fresh random 16-byte salt. This is the canonical embedder-facing derivation
|
|
153
|
+
* matching exactly what `UnifiedEmailServer` verifies at AUTH time — persist
|
|
154
|
+
* only the returned verifier, never the plaintext.
|
|
155
|
+
*
|
|
156
|
+
* SaltedPassword = PBKDF2-HMAC-SHA256(password, salt, iterations, 32);
|
|
157
|
+
* StoredKey = SHA256(HMAC-SHA256(SaltedPassword, 'Client Key'));
|
|
158
|
+
* ServerKey = HMAC-SHA256(SaltedPassword, 'Server Key').
|
|
159
|
+
*/
|
|
160
|
+
export function deriveScramVerifier(
|
|
161
|
+
password: string,
|
|
162
|
+
iterations: number = SCRAM_DEFAULT_ITERATIONS,
|
|
163
|
+
): ISmtpScramVerifier {
|
|
164
|
+
if (typeof password !== 'string' || password.length === 0) {
|
|
165
|
+
throw new Error('Cannot derive a SCRAM verifier from an empty password');
|
|
166
|
+
}
|
|
167
|
+
if (
|
|
168
|
+
!Number.isInteger(iterations)
|
|
169
|
+
|| iterations < SCRAM_MIN_ITERATIONS
|
|
170
|
+
|| iterations > SCRAM_MAX_ITERATIONS
|
|
171
|
+
) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`SCRAM verifier iterations must be an integer between ${SCRAM_MIN_ITERATIONS} and ${SCRAM_MAX_ITERATIONS}`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
const salt = crypto.randomBytes(16);
|
|
177
|
+
const saltedPassword = crypto.pbkdf2Sync(password, salt, iterations, 32, 'sha256');
|
|
178
|
+
const clientKey = crypto.createHmac('sha256', saltedPassword).update('Client Key').digest();
|
|
179
|
+
const storedKey = crypto.createHash('sha256').update(clientKey).digest();
|
|
180
|
+
const serverKey = crypto.createHmac('sha256', saltedPassword).update('Server Key').digest();
|
|
181
|
+
return {
|
|
182
|
+
algorithm: 'SCRAM-SHA-256',
|
|
183
|
+
salt: salt.toString('base64'),
|
|
184
|
+
iterations,
|
|
185
|
+
storedKey: storedKey.toString('base64'),
|
|
186
|
+
serverKey: serverKey.toString('base64'),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
137
190
|
export function assertValidInboundPolicy(
|
|
138
191
|
inboundPolicy: {
|
|
139
192
|
defaultAction: unknown;
|