Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/crypto/encrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { DEFAULT_EMAIL_ATTRIBUTES, type EmailAttributes } from '../util/attribut
import { resolveSigningKeys } from './signing.js';
import Chunker, { withTransform } from './chunker.js';
import { createZipReadable } from '../util/zip.js';
import { nowSeconds } from '../util/policy.js';
import { loadWasm } from '../util/wasm.js';
import type { RetryOptions } from '../util/retry.js';

Expand Down Expand Up @@ -59,7 +60,7 @@ export async function encryptPipeline(options: EncryptPipelineOptions): Promise<
]);

// Build encryption policy
const ts = Math.round(Date.now() / 1000);
const ts = nowSeconds();
const policy = buildEncryptionPolicy(recipients, ts, emailAttrs);

// If the sign method requests including the sender, add a sender entry
Expand Down Expand Up @@ -186,7 +187,7 @@ export async function sealRaw(options: SealRawOptions): Promise<Uint8Array> {
]);

// Build encryption policy
const ts = Math.round(Date.now() / 1000);
const ts = nowSeconds();
const policy = buildEncryptionPolicy(recipients, ts, emailAttrs);

// Include sender in policy if requested
Expand Down
11 changes: 11 additions & 0 deletions src/util/policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@ export function sortPolicies(con: { t: string; v?: string }[]): { t: string; v?:
return [...con].sort((a, b) => a.t.localeCompare(b.t));
}

/** Current Unix time in whole seconds.
*
* Uses `floor`, never `round`: this stamps the policy timestamp a sealed
* container is keyed on, and the PKG rejects a USK request whose timestamp is
* in the future ("chronology error"). `Math.round` can land up to ~1s ahead of
* the real time, so an immediate decrypt (encrypt→decrypt within the same
* second) could fail; `floor` is always ≤ now. */
export function nowSeconds(): number {
return Math.floor(Date.now() / 1000);
}

/** Calculate seconds until 4 AM (PKG key validity period) */
export function secondsTill4AM(): number {
const now = Date.now();
Expand Down
29 changes: 29 additions & 0 deletions tests/now-seconds.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// The seal policy timestamp must never be in the future: the PKG rejects a USK
// request whose timestamp is > now ("chronology error"), so an immediate
// encrypt→decrypt could fail if the timestamp were rounded up. nowSeconds()
// uses floor for exactly this reason.
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { nowSeconds } from '../src/util/policy.js';

describe('nowSeconds', () => {
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());

it('floors — never rounds a sub-second past 0.5 up to the next second', () => {
// 1000.700s: Math.round would give 1001 (one second in the future).
vi.setSystemTime(new Date(1000_700));
expect(nowSeconds()).toBe(1000);
});

it('is exact on a whole second', () => {
vi.setSystemTime(new Date(1000_000));
expect(nowSeconds()).toBe(1000);
});

it('never exceeds Date.now()/1000 (would trip the PKG chronology check)', () => {
for (const ms of [1234_001, 1234_499, 1234_500, 1234_999]) {
vi.setSystemTime(new Date(ms));
expect(nowSeconds()).toBeLessThanOrEqual(Date.now() / 1000);
}
});
});
Loading