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
453 changes: 386 additions & 67 deletions FileInspectorX.Tests/CryptoDetectionsTests.cs

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions FileInspectorX.Tests/DetectionSettingsCollection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Xunit;

namespace FileInspectorX.Tests;

// This collection serializes tests that temporarily lower Settings.DetectionReadBudgetBytes.
[CollectionDefinition(nameof(DetectionSettingsCollection), DisableParallelization = true)]
public sealed class DetectionSettingsCollection : ICollectionFixture<DetectionSettingsFixture>
{
}

public sealed class DetectionSettingsFixture
{
}
91 changes: 76 additions & 15 deletions FileInspectorX.Tests/DetectorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -556,20 +556,30 @@ public void Detect_MachO_64LE() {
} finally { if (File.Exists(p)) File.Delete(p); }
}

[Fact]
public void Detect_Msg_Basic() {
var p = Path.GetTempFileName();
try {
var list = new List<byte>();
list.AddRange(new byte[] { 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1 });
list.AddRange(new byte[128]);
list.AddRange(System.Text.Encoding.ASCII.GetBytes("__substg1.0_007D"));
File.WriteAllBytes(p, list.ToArray());
var res = FI.Detect(p);
Assert.NotNull(res);
Assert.Equal("msg", res!.Extension);
} finally { if (File.Exists(p)) File.Delete(p); }
}
[Fact]
public void Detect_Msg_Basic() {
var p = Path.GetTempFileName();
try {
WriteSyntheticOleDirectoryFile(p, "__substg1.0_007D", "__properties_version1.0");
var res = FI.Detect(p);
Assert.NotNull(res);
Assert.Equal("msg", res!.Extension);
} finally { if (File.Exists(p)) File.Delete(p); }
}

[Fact]
public void Detect_Msg_DoesNotMatch_OleFile_With_Marker_Bytes_Outside_DirectoryEntries() {
var p = Path.GetTempFileName();
try {
var list = new List<byte>();
list.AddRange(new byte[] { 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1 });
list.AddRange(new byte[128]);
list.AddRange(System.Text.Encoding.ASCII.GetBytes("__substg1.0_007D"));
File.WriteAllBytes(p, list.ToArray());
var res = FI.Detect(p);
Assert.True(res == null || !string.Equals(res.Extension, "msg", StringComparison.OrdinalIgnoreCase));
} finally { if (File.Exists(p)) File.Delete(p); }
}

[Fact]
public void Classify_ContentKind_Works() {
Expand Down Expand Up @@ -599,7 +609,7 @@ public void Guess_Zip_Subtypes_Jar() {
}

[Fact]
public void Guess_Zip_Subtypes_Epub() {
public void Guess_Zip_Subtypes_Epub() {
var p = Path.GetTempFileName();
var zip = p + ".zip";
try {
Expand Down Expand Up @@ -633,6 +643,57 @@ public void Guess_Zip_Subtypes_Vsix() {
} finally { if (File.Exists(p)) File.Delete(p); if (File.Exists(zip)) File.Delete(zip); }
}

private static void WriteSyntheticOleDirectoryFile(string path, params string[] directoryNames)
{
var header = new byte[512];
var sig = new byte[]{0xD0,0xCF,0x11,0xE0,0xA1,0xB1,0x1A,0xE1};
Array.Copy(sig, 0, header, 0, sig.Length);
header[0x1E] = 0x09;
header[0x1F] = 0x00;
// Our mini CFBF reader maps sector N to offset 512 + ((N + 1) * 512),
// so FAT sector SID 0 lives at 1024 and directory sector SID 2 lives at 2048.
WriteLe32(header, 0x2C, 1);
WriteLe32(header, 0x30, 2);
WriteLe32(header, 0x4C, 0);

var fat = new byte[512];
const int ENDOFCHAIN = unchecked((int)0xFFFFFFFE);
WriteLe32(fat, 2 * 4, ENDOFCHAIN);

var dir = new byte[512];
for (int i = 0; i < directoryNames.Length && i < 4; i++)
{
WriteDirName(dir, i * 128, directoryNames[i]);
}

using var fs = File.Create(path);
fs.Write(header, 0, header.Length);
fs.Position = 1024;
fs.Write(fat, 0, fat.Length);
fs.Position = 2048;
fs.Write(dir, 0, dir.Length);

static void WriteDirName(byte[] buf, int offset, string name)
{
var bytes = System.Text.Encoding.Unicode.GetBytes(name + "\0");
Array.Copy(bytes, 0, buf, offset, Math.Min(bytes.Length, 64));
ushort len = (ushort)Math.Min(bytes.Length, 128);
buf[offset + 0x40] = (byte)(len & 0xFF);
buf[offset + 0x41] = (byte)((len >> 8) & 0xFF);
}

static void WriteLe32(byte[] buffer, int offset, int value)
{
unchecked
{
buffer[offset] = (byte)(value & 0xFF);
buffer[offset + 1] = (byte)((value >> 8) & 0xFF);
buffer[offset + 2] = (byte)((value >> 16) & 0xFF);
buffer[offset + 3] = (byte)((value >> 24) & 0xFF);
}
}
}

[Fact]
public void Analyze_Zip_With_Inner_Zip_Flags_Nested_Archive_Subtype() {
var p = Path.GetTempFileName();
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
00  *†H†÷
Expand Down
Binary file not shown.
Binary file not shown.
14 changes: 7 additions & 7 deletions FileInspectorX/Analysis/FileAnalysis.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,13 +175,13 @@ public class FileAnalysis {
public CertificateInfo? Certificate { get; set; }

/// <summary>
/// For PKCS#7 certificate bundles (.p7b/.spc): number of certificates and their subjects (best-effort).
/// </summary>
public int? CertificateBundleCount { get; set; }
/// <summary>
/// Subjects present in a PKCS#7 certificate bundle, when parsed.
/// </summary>
public IReadOnlyList<string>? CertificateBundleSubjects { get; set; }
/// For PKCS#7 certificate/signature payloads (.p7b/.spc/.p7s): number of certificates and their subjects (best-effort).
/// </summary>
public int? CertificateBundleCount { get; set; }
/// <summary>
/// Subjects present in a PKCS#7 certificate/signature payload, when parsed.
/// </summary>
public IReadOnlyList<string>? CertificateBundleSubjects { get; set; }

/// <summary>
/// When content appears encoded (e.g., base64, hex), indicates the encoding kind.
Expand Down
205 changes: 152 additions & 53 deletions FileInspectorX/Analysis/FileInspector.Analyze.cs
Original file line number Diff line number Diff line change
Expand Up @@ -841,11 +841,11 @@ bool IsTextLike(ContentTypeDetectionResult? d)
if ((options?.IncludeAuthenticode != false) && (det?.Extension is "exe" or "dll" or "sys" or "cpl")) {
TryPopulateAuthenticode(path, res);
}
// PKCS#7 certificate bundle (.p7b/.spc)
if (det?.Extension is "p7b" or "spc")
{
TryParseP7b(path, res);
}
// PKCS#7 certificate/signature payload (.p7b/.spc/.p7s)
if (det?.Extension is "p7b" or "spc" or "p7s")
{
TryParseP7b(path, res);
}
// MSI package properties (Windows only)
if ((options?.IncludeInstaller != false) && (det?.Extension?.Equals("msi", StringComparison.OrdinalIgnoreCase) ?? false))
{
Expand Down Expand Up @@ -2411,56 +2411,155 @@ private static void TryInspectTar(
} catch { }
}

private static bool TryLoadCertificateFromFile(string path, string ext, out X509Certificate2 cert)
{
cert = null!;
try
{
#if NET5_0_OR_GREATER || NET8_0_OR_GREATER
if (string.Equals(ext, "pem", StringComparison.OrdinalIgnoreCase))
{
cert = X509Certificate2.CreateFromPemFile(path);
return cert != null;
}
#endif
// DER/CRT/CER or fallback for PEM (manual decode)
if (string.Equals(ext, "pem", StringComparison.OrdinalIgnoreCase))
{
var text = System.IO.File.ReadAllText(path);
const string begin = "-----BEGIN CERTIFICATE-----";
const string end = "-----END CERTIFICATE-----";
int s = text.IndexOf(begin, StringComparison.OrdinalIgnoreCase);
int e = text.IndexOf(end, StringComparison.OrdinalIgnoreCase);
if (s >= 0 && e > s)
{
var b64 = text.Substring(s + begin.Length, e - (s + begin.Length)).Replace("\r", string.Empty).Replace("\n", string.Empty).Trim();
var der = System.Convert.FromBase64String(b64);
cert = new X509Certificate2(der);
return true;
}
return false;
}
cert = new X509Certificate2(path);
return cert != null;
}
catch { return false; }
}

private static string GetExtension(string name) {
var i = name.LastIndexOf('.');
if (i < 0) return string.Empty;
return name.Substring(i + 1).ToLowerInvariant();
private static bool TryLoadCertificateFromFile(string path, string ext, out X509Certificate2 cert)
{
cert = null!;
try
{
if (string.Equals(ext, "pem", StringComparison.OrdinalIgnoreCase) ||
string.Equals(ext, "crt", StringComparison.OrdinalIgnoreCase) ||
string.Equals(ext, "cer", StringComparison.OrdinalIgnoreCase))
{
if (!TryReadPemCertificateBlock(path, out var pemBlock, out var derBytes))
{
if (string.Equals(ext, "pem", StringComparison.OrdinalIgnoreCase))
{
return false;
}

// .crt/.cer may still be DER-encoded, so fall through to the bounded raw-byte import below.
}
else
{
#if NET5_0_OR_GREATER
try
{
cert = X509Certificate2.CreateFromPem(pemBlock);
return cert != null;
}
catch
{
// Fall back to DER import below when PEM parsing is unavailable or rejects the block.
}
#endif
cert = new X509Certificate2(derBytes);
return cert != null;
}
}

if (!TryReadFileBytesWithinBudget(path, GetCertificateParseReadBudgetBytes(), out var rawBytes))
{
return false;
}

cert = new X509Certificate2(rawBytes);
return cert != null;
}
catch { return false; }
}

private static int GetCertificateParseReadBudgetBytes()
{
long budget = Settings.DetectionReadBudgetBytes;
if (budget <= 0) budget = 1_000_000;
if (budget > 8L * 1024L * 1024L) budget = 8L * 1024L * 1024L;
return (int)Math.Max(256, budget);
}

private static bool TryReadFileBytesWithinBudget(string path, int maxBytes, out byte[] data)
{
data = Array.Empty<byte>();
try
{
var fileInfo = new FileInfo(path);
if (!fileInfo.Exists || fileInfo.Length <= 0 || fileInfo.Length > maxBytes)
{
return false;
}

using var fs = File.OpenRead(path);
data = new byte[(int)fileInfo.Length];
var offset = 0;
while (offset < data.Length)
{
var read = fs.Read(data, offset, data.Length - offset);
if (read <= 0) break;
offset += read;
}

if (offset <= 0)
{
data = Array.Empty<byte>();
return false;
}

if (offset != data.Length)
{
Array.Resize(ref data, offset);
}

return true;
}
catch
{
data = Array.Empty<byte>();
return false;
}
}

private static bool TryReadPemCertificateBlock(string path, out string pemBlock, out byte[] derBytes)
{
pemBlock = string.Empty;
derBytes = Array.Empty<byte>();
try
{
var text = ReadHeadText(path, GetCertificateParseReadBudgetBytes());
if (string.IsNullOrWhiteSpace(text))
{
return false;
}

const string begin = "-----BEGIN CERTIFICATE-----";
const string end = "-----END CERTIFICATE-----";
int start = text.IndexOf(begin, StringComparison.OrdinalIgnoreCase);
int endIndex = text.IndexOf(end, StringComparison.OrdinalIgnoreCase);
if (start < 0 || endIndex <= start)
{
return false;
}

var pemEnd = endIndex + end.Length;
pemBlock = text.Substring(start, pemEnd - start);
var b64 = text.Substring(start + begin.Length, endIndex - (start + begin.Length))
.Replace("\r", string.Empty)
.Replace("\n", string.Empty)
.Trim();
derBytes = System.Convert.FromBase64String(b64);
return derBytes.Length > 0;
}
catch
{
pemBlock = string.Empty;
derBytes = Array.Empty<byte>();
return false;
}
}

private static string GetExtension(string name) {
var i = name.LastIndexOf('.');
if (i < 0) return string.Empty;
return name.Substring(i + 1).ToLowerInvariant();
}

private static string Latin1String(byte[] bytes)
{
try
{
#if NET5_0_OR_GREATER || NET8_0_OR_GREATER
return System.Text.Encoding.Latin1.GetString(bytes);
#else
return System.Text.Encoding.GetEncoding(28591).GetString(bytes); // ISO-8859-1 for .NET Framework / netstandard
#endif
private static string Latin1String(byte[] bytes)
{
try
{
#if NET5_0_OR_GREATER
return System.Text.Encoding.Latin1.GetString(bytes);
#else
return System.Text.Encoding.GetEncoding(28591).GetString(bytes); // ISO-8859-1 for .NET Framework / netstandard
#endif
}
catch { return System.Text.Encoding.ASCII.GetString(bytes); }
}
Expand Down
Loading
Loading