7z: encrypted headers are known from the encoded header, without bsdtar
Test and deploy / test (push) Successful in 2m6s
Test and deploy / deploy (push) Successful in 2m25s

85% of the 7z archives inspected have encrypted headers (-mhe). Their
encoded header lists the 7zAES coder, so the packed header stream is no
longer downloaded and bsdtar no longer runs for them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daan MeijerandClaude Opus 5.5 committed 2026-10-06 10:23:32 +02:00
1 parent 9ae15ed26f
commit 4d3cb6474c
3 files changed
+97 -2

No files matched your search

+68 -1
View File
@@ -12,8 +12,13 @@ import { crc32 } from 'node:zlib'
const SIGNATURE = Buffer.from([0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c])
const SIGNATURE_HEADER_SIZE = 32
/** Header property ids (7zFormat.txt). */
const K_ENCODED_HEADER = 0x17
const K_END = 0x00
const K_PACK_INFO = 0x06
const K_UNPACK_INFO = 0x07
const K_SIZE = 0x09
const K_CRC = 0x0a
const K_FOLDER = 0x0b
const K_ENCODED_HEADER = 0x17
export interface SevenZipStart {
/** Where the archive header starts, from the start of the archive. */
@@ -49,6 +54,68 @@ export function listingStart(start: SevenZipStart, header: Buffer): number {
return SIGNATURE_HEADER_SIZE + packPos
}
/** 7zAES, the coder of password-protected streams. */
const AES_CODER = Buffer.from([0x06, 0xf1, 0x07, 0x01])
/**
* Whether an encoded header (`header`, the bytes at headerStart) is
* encrypted (7z -mhe): its coders include 7zAES. Then nothing can be listed
* without the password, and neither the packed header nor bsdtar is needed.
* Null when the header isn't encoded or doesn't read as expected.
*/
export function encodedHeaderEncrypted(header: Buffer): boolean | null {
try {
if (header[0] !== K_ENCODED_HEADER || header[1] !== K_PACK_INFO) return null
let pos = 2
let numPackStreams: number
;[, pos] = readNumber(header, pos) // packPos
;[numPackStreams, pos] = readNumber(header, pos)
for (let id = header[pos++]; id !== K_END; id = header[pos++]) {
if (id === K_SIZE) {
for (let i = 0; i < numPackStreams; i++) [, pos] = readNumber(header, pos)
} else if (id === K_CRC) {
const allDefined = header[pos++]
let defined = numPackStreams
if (!allDefined) {
const bits = header.subarray(pos, pos + Math.ceil(numPackStreams / 8))
pos += bits.length
defined = [...bits].reduce((sum, byte) => sum + byte.toString(2).replaceAll('0', '').length, 0)
}
pos += 4 * defined
} else {
return null
}
if (pos >= header.length) return null
}
if (header[pos++] !== K_UNPACK_INFO || header[pos++] !== K_FOLDER) return null
let numFolders: number
;[numFolders, pos] = readNumber(header, pos)
if (numFolders < 1 || header[pos++] !== 0) return null // folders stored elsewhere
let numCoders: number
;[numCoders, pos] = readNumber(header, pos)
if (!(numCoders >= 1) || pos >= header.length) return null
for (let c = 0; c < numCoders; c++) {
const flags = header[pos++]
const idSize = flags & 0x0f
if (header.subarray(pos, pos + idSize).equals(AES_CODER)) return true
pos += idSize
if (flags & 0x10) {
;[, pos] = readNumber(header, pos)
;[, pos] = readNumber(header, pos)
}
if (flags & 0x20) {
let size: number
;[size, pos] = readNumber(header, pos)
pos += size
}
if (pos > header.length) return null
}
return false
} catch {
return null
}
}
/**
* 7z's variable-length number: the leading 1-bits of the first byte count
* the little-endian bytes that follow; its remaining bits are the high part.