mirror of
https://github.com/gchq/CyberChef.git
synced 2024-09-16 16:41:32 +02:00
268 lines
8.5 KiB
JavaScript
268 lines
8.5 KiB
JavaScript
/**
|
|
* File type functions
|
|
*
|
|
* @author n1474335 [n1474335@gmail.com]
|
|
* @copyright Crown Copyright 2018
|
|
* @license Apache-2.0
|
|
*
|
|
*/
|
|
import {FILE_SIGNATURES} from "./FileSignatures.mjs";
|
|
import {sendStatusMessage} from "../Utils.mjs";
|
|
|
|
|
|
/**
|
|
* Checks whether a signature matches a buffer.
|
|
*
|
|
* @param {Object|Object[]} sig - A dictionary of offsets with values assigned to them.
|
|
* These values can be numbers for static checks, arrays of potential valid matches,
|
|
* or bespoke functions to check the validity of the buffer value at that offset.
|
|
* @param {Uint8Array} buf
|
|
* @param {number} [offset=0] Where in the buffer to start searching from
|
|
* @returns {boolean}
|
|
*/
|
|
function signatureMatches(sig, buf, offset=0) {
|
|
// Using a length check seems to be more performant than `sig instanceof Array`
|
|
if (sig.length) {
|
|
// sig is an Array - return true if any of them match
|
|
// The following `reduce` method is nice, but performance matters here, so we
|
|
// opt for a faster, if less elegant, for loop.
|
|
// return sig.reduce((acc, s) => acc || bytesMatch(s, buf, offset), false);
|
|
for (let i = 0; i < sig.length; i++) {
|
|
if (bytesMatch(sig[i], buf, offset)) return true;
|
|
}
|
|
return false;
|
|
} else {
|
|
return bytesMatch(sig, buf, offset);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Checks whether a set of bytes match the given buffer.
|
|
*
|
|
* @param {Object} sig - A dictionary of offsets with values assigned to them.
|
|
* These values can be numbers for static checks, arrays of potential valid matches,
|
|
* or bespoke functions to check the validity of the buffer value at that offset.
|
|
* @param {Uint8Array} buf
|
|
* @param {number} [offset=0] Where in the buffer to start searching from
|
|
* @returns {boolean}
|
|
*/
|
|
function bytesMatch(sig, buf, offset=0) {
|
|
for (const sigoffset in sig) {
|
|
const pos = parseInt(sigoffset, 10) + offset;
|
|
switch (typeof sig[sigoffset]) {
|
|
case "number": // Static check
|
|
if (buf[pos] !== sig[sigoffset])
|
|
return false;
|
|
break;
|
|
case "object": // Array of options
|
|
if (sig[sigoffset].indexOf(buf[pos]) < 0)
|
|
return false;
|
|
break;
|
|
case "function": // More complex calculation
|
|
if (!sig[sigoffset](buf[pos]))
|
|
return false;
|
|
break;
|
|
default:
|
|
throw new Error(`Unrecognised signature type at offset ${sigoffset}`);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Given a buffer, detects magic byte sequences at specific positions and returns the
|
|
* extension and mime type.
|
|
*
|
|
* @param {Uint8Array|ArrayBuffer} buf
|
|
* @param {string[]} [categories=All] - Which categories of file to look for
|
|
* @returns {Object[]} types
|
|
* @returns {string} type.name - Name of file type
|
|
* @returns {string} type.ext - File extension
|
|
* @returns {string} type.mime - Mime type
|
|
* @returns {string} [type.desc] - Description
|
|
*/
|
|
export function detectFileType(buf, categories=Object.keys(FILE_SIGNATURES)) {
|
|
if (buf instanceof ArrayBuffer) {
|
|
buf = new Uint8Array(buf);
|
|
}
|
|
|
|
if (!(buf && buf.length > 1)) {
|
|
return [];
|
|
}
|
|
|
|
const matchingFiles = [];
|
|
const signatures = {};
|
|
|
|
for (const cat in FILE_SIGNATURES) {
|
|
if (categories.includes(cat)) {
|
|
signatures[cat] = FILE_SIGNATURES[cat];
|
|
}
|
|
}
|
|
|
|
for (const cat in signatures) {
|
|
const category = signatures[cat];
|
|
|
|
category.forEach(filetype => {
|
|
if (signatureMatches(filetype.signature, buf)) {
|
|
matchingFiles.push(filetype);
|
|
}
|
|
});
|
|
}
|
|
return matchingFiles;
|
|
}
|
|
|
|
|
|
/**
|
|
* Given a buffer, searches for magic byte sequences at all possible positions and returns
|
|
* the extensions and mime types.
|
|
*
|
|
* @param {Uint8Array} buf
|
|
* @param {string[]} [categories=All] - Which categories of file to look for
|
|
* @returns {Object[]} foundFiles
|
|
* @returns {number} foundFiles.offset - The position in the buffer at which this file was found
|
|
* @returns {Object} foundFiles.fileDetails
|
|
* @returns {string} foundFiles.fileDetails.name - Name of file type
|
|
* @returns {string} foundFiles.fileDetails.ext - File extension
|
|
* @returns {string} foundFiles.fileDetails.mime - Mime type
|
|
* @returns {string} [foundFiles.fileDetails.desc] - Description
|
|
*/
|
|
export function scanForFileTypes(buf, categories=Object.keys(FILE_SIGNATURES)) {
|
|
if (!(buf && buf.length > 1)) {
|
|
return [];
|
|
}
|
|
|
|
const foundFiles = [];
|
|
const signatures = {};
|
|
|
|
for (const cat in FILE_SIGNATURES) {
|
|
if (categories.includes(cat)) {
|
|
signatures[cat] = FILE_SIGNATURES[cat];
|
|
}
|
|
}
|
|
|
|
for (const cat in signatures) {
|
|
const category = signatures[cat];
|
|
|
|
for (let i = 0; i < category.length; i++) {
|
|
const filetype = category[i];
|
|
const sigs = filetype.signature.length ? filetype.signature : [filetype.signature];
|
|
|
|
sigs.forEach(sig => {
|
|
let pos = 0;
|
|
while ((pos = locatePotentialSig(buf, sig, pos)) >= 0) {
|
|
if (bytesMatch(sig, buf, pos)) {
|
|
sendStatusMessage(`Found potential signature for ${filetype.name} at pos ${pos}`);
|
|
foundFiles.push({
|
|
offset: pos,
|
|
fileDetails: filetype
|
|
});
|
|
}
|
|
pos++;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Return found files in order of increasing offset
|
|
return foundFiles.sort((a, b) => {
|
|
return a.offset - b.offset;
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* Fastcheck function to quickly scan the buffer for the first byte in a signature.
|
|
*
|
|
* @param {Uint8Array} buf - The buffer to search
|
|
* @param {Object} sig - A single signature object (Not an array of signatures)
|
|
* @param {number} offset - Where to start search from
|
|
* @returns {number} The position of the match or -1 if one cannot be found.
|
|
*/
|
|
function locatePotentialSig(buf, sig, offset) {
|
|
// Find values for first key and value in sig
|
|
const k = parseInt(Object.keys(sig)[0], 10);
|
|
const v = Object.values(sig)[0];
|
|
switch (typeof v) {
|
|
case "number":
|
|
return buf.indexOf(v, offset + k) - k;
|
|
case "object":
|
|
for (let i = offset + k; i < buf.length; i++) {
|
|
if (v.indexOf(buf[i]) >= 0) return i - k;
|
|
}
|
|
return -1;
|
|
case "function":
|
|
for (let i = offset + k; i < buf.length; i++) {
|
|
if (v(buf[i])) return i - k;
|
|
}
|
|
return -1;
|
|
default:
|
|
throw new Error("Unrecognised signature type");
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Detects whether the given buffer is a file of the type specified.
|
|
*
|
|
* @param {string|RegExp} type
|
|
* @param {Uint8Array|ArrayBuffer} buf
|
|
* @returns {string|false} The mime type or false if the type does not match
|
|
*/
|
|
export function isType(type, buf) {
|
|
const types = detectFileType(buf);
|
|
|
|
if (!(types && types.length)) return false;
|
|
|
|
if (typeof type === "string") {
|
|
return types.reduce((acc, t) => {
|
|
const mime = t.mime.startsWith(type) ? t.mime : false;
|
|
return acc || mime;
|
|
}, false);
|
|
} else if (type instanceof RegExp) {
|
|
return types.reduce((acc, t) => {
|
|
const mime = type.test(t.mime) ? t.mime : false;
|
|
return acc || mime;
|
|
}, false);
|
|
} else {
|
|
throw new Error("Invalid type input.");
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Detects whether the given buffer contains an image file.
|
|
*
|
|
* @param {Uint8Array|ArrayBuffer} buf
|
|
* @returns {string|false} The mime type or false if the type does not match
|
|
*/
|
|
export function isImage(buf) {
|
|
return isType("image", buf);
|
|
}
|
|
|
|
|
|
/**
|
|
* Attempts to extract a file from a data stream given its offset and extractor function.
|
|
*
|
|
* @param {Uint8Array} bytes
|
|
* @param {Object} fileDetail
|
|
* @param {string} fileDetail.mime
|
|
* @param {string} fileDetail.extension
|
|
* @param {Function} fileDetail.extractor
|
|
* @param {number} offset
|
|
* @returns {File}
|
|
*/
|
|
export function extractFile(bytes, fileDetail, offset) {
|
|
if (fileDetail.extractor) {
|
|
sendStatusMessage(`Attempting to extract ${fileDetail.name} at pos ${offset}...`);
|
|
const fileData = fileDetail.extractor(bytes, offset);
|
|
const ext = fileDetail.extension.split(",")[0];
|
|
return new File([fileData], `extracted_at_0x${offset.toString(16)}.${ext}`, {
|
|
type: fileDetail.mime
|
|
});
|
|
}
|
|
|
|
throw new Error(`No extraction algorithm available for "${fileDetail.mime}" files`);
|
|
}
|