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
20 changes: 13 additions & 7 deletions apps/fluux/src/e2ee/OpenPGPPluginBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1511,13 +1511,19 @@ export abstract class OpenPGPPluginBase implements E2EEPlugin {
: `${this.pluginName()}: signcrypt <to/> does not address ${ownBareJid}`,
)
}
const skew = Math.abs(envelope.timestamp.getTime() - this.now())
if (skew > SIGNCRYPT_CLOCK_SKEW_MS) {
throw new E2EEPluginError(
'permanent',
'envelope-stale',
`${this.pluginName()}: signcrypt <time/> is ${Math.round(skew / 1000)}s outside the ±7-day skew window`,
)
// Skip timestamp skew check for archived messages (MAM replay,
// retryPendingDecrypts). The check is an anti-replay defence that
// only makes sense for live messages — archived messages are
// authentically old and would always fail the ±7-day window.
if (!context?.fromArchive) {
const skew = Math.abs(envelope.timestamp.getTime() - this.now())
if (skew > SIGNCRYPT_CLOCK_SKEW_MS) {
throw new E2EEPluginError(
'permanent',
'envelope-stale',
`${this.pluginName()}: signcrypt <time/> is ${Math.round(skew / 1000)}s outside the ±7-day skew window`,
)
}
}

const plaintextBytes = new TextEncoder().encode(envelope.payloadXml)
Expand Down
4 changes: 3 additions & 1 deletion packages/fluux-sdk/src/core/e2ee/stanzaDecrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,13 @@ export async function decryptStanzaInPlace(
try {
const messageId = stanza.attrs.id
const isSelfOutgoing = options?.isSelfOutgoing === true
const fromArchive = source === 'archive'
const context: InboundDecryptContext | undefined =
messageId || isSelfOutgoing
messageId || isSelfOutgoing || fromArchive
? {
...(messageId && { messageId }),
...(isSelfOutgoing && { isSelfOutgoing: true as const }),
...(fromArchive && { fromArchive: true as const }),
}
: undefined
const target = { kind: 'direct' as const, peer: senderPeer }
Expand Down
8 changes: 8 additions & 0 deletions packages/fluux-sdk/src/core/e2ee/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,14 @@ export interface InboundDecryptContext {
* (OpenPGP, OMEMO) must branch on this flag.
*/
isSelfOutgoing?: boolean
/**
* `true` when this stanza was replayed from the XEP-0313 MAM archive
* or is being re-decrypted by {@link XMPPClient.retryPendingDecrypts}.
* Plugins should relax time-based anti-replay checks (e.g. signcrypt
* timestamp skew) for archived messages, since they may be arbitrarily
* old yet still authentic.
*/
fromArchive?: boolean
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/fluux-sdk/src/core/modules/MAM.e2ee.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,6 @@ describe('MAM E2EE wiring', () => {
await runQueryWithEntry(harness, PEER, archiveEntry)

const lastCall = decryptSpy.mock.calls[decryptSpy.mock.calls.length - 1]
expect(lastCall[2]).toEqual({ messageId: 'mam-msg-id' })
expect(lastCall[2]).toEqual({ messageId: 'mam-msg-id', fromArchive: true })
})
})
Loading