// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
namespace WixToolset
{
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using WixToolset.Bind.Bundles;
using WixToolset.Data;
using WixToolset.Msi;
using WixToolset.Core.Native;
///
/// Converts a wixout representation of an MSM database into a ComponentGroup the form of WiX source.
///
public sealed class Inscriber : IMessageHandler
{
// private TempFileCollection tempFiles;
private TableDefinitionCollection tableDefinitions;
public Inscriber()
{
this.tableDefinitions = new TableDefinitionCollection(WindowsInstallerStandard.GetTableDefinitions());
}
///
/// Gets or sets the temp files collection.
///
/// The temp files collection.
// public TempFileCollection TempFiles
// {
// get { return this.tempFiles; }
// set { this.tempFiles = value; }
// }
///
/// Gets or sets the path to the temp files location.
///
/// The path to the temp files location.
public string TempFilesLocation
{
get
{
// if (null == this.tempFiles)
// {
// return null;
// }
// else
// {
// return this.tempFiles.BasePath;
// }
return Path.GetTempPath();
}
// set
// {
// this.DeleteTempFiles();
// if (null == value)
// {
// this.tempFiles = new TempFileCollection();
// }
// else
// {
// this.tempFiles = new TempFileCollection(value);
// }
// // ensure the base path exists
// Directory.CreateDirectory(this.tempFiles.BasePath);
// }
}
///
/// Extracts engine from attached container and updates engine with detached container signatures.
///
/// Bundle with attached container.
/// Bundle engine only.
/// True if bundle was updated.
public bool InscribeBundleEngine(string bundleFile, string outputFile)
{
string tempFile = Path.Combine(this.TempFilesLocation, "bundle_engine_unsigned.exe");
using (BurnReader reader = BurnReader.Open(bundleFile))
using (FileStream writer = File.Open(tempFile, FileMode.Create, FileAccess.Write, FileShare.Read | FileShare.Delete))
{
reader.Stream.Seek(0, SeekOrigin.Begin);
byte[] buffer = new byte[4 * 1024];
int total = 0;
int read = 0;
do
{
read = Math.Min(buffer.Length, (int)reader.EngineSize - total);
read = reader.Stream.Read(buffer, 0, read);
writer.Write(buffer, 0, read);
total += read;
} while (total < reader.EngineSize && 0 < read);
if (total != reader.EngineSize)
{
throw new InvalidOperationException("Failed to copy engine out of bundle.");
}
// TODO: update writer with detached container signatures.
}
Directory.CreateDirectory(Path.GetDirectoryName(outputFile));
if (File.Exists(outputFile))
{
File.Delete(outputFile);
}
File.Move(tempFile, outputFile);
WixToolset.Core.Native.NativeMethods.ResetAcls(new string[] { outputFile }, 1);
return true;
}
///
/// Updates engine with attached container information and adds attached container again.
///
/// Bundle with attached container.
/// Signed bundle engine.
/// Signed engine with attached container.
/// True if bundle was updated.
public bool InscribeBundle(string bundleFile, string signedEngineFile, string outputFile)
{
bool inscribed = false;
string tempFile = Path.Combine(this.TempFilesLocation, "bundle_engine_signed.exe");
using (BurnReader reader = BurnReader.Open(bundleFile))
{
File.Copy(signedEngineFile, tempFile, true);
// If there was an attached container on the original (unsigned) bundle, put it back.
if (reader.AttachedContainerSize > 0)
{
reader.Stream.Seek(reader.AttachedContainerAddress, SeekOrigin.Begin);
using (BurnWriter writer = BurnWriter.Open(tempFile))
{
writer.RememberThenResetSignature();
writer.AppendContainer(reader.Stream, reader.AttachedContainerSize, BurnCommon.Container.Attached);
inscribed = true;
}
}
}
Directory.CreateDirectory(Path.GetDirectoryName(outputFile));
if (File.Exists(outputFile))
{
File.Delete(outputFile);
}
File.Move(tempFile, outputFile);
WixToolset.Core.Native.NativeMethods.ResetAcls(new string[] { outputFile }, 1);
return inscribed;
}
///
/// Updates database with signatures from external cabinets.
///
/// Path to MSI database.
/// Ouput for updated MSI database.
/// Clean up files.
/// True if database is updated.
public bool InscribeDatabase(string databaseFile, string outputFile, bool tidy)
{
// Keeps track of whether we've encountered at least one signed cab or not - we'll throw a warning if no signed cabs were encountered
bool foundUnsignedExternals = false;
bool shouldCommit = false;
FileAttributes attributes = File.GetAttributes(databaseFile);
if (FileAttributes.ReadOnly == (attributes & FileAttributes.ReadOnly))
{
this.OnMessage(WixErrors.ReadOnlyOutputFile(databaseFile));
return shouldCommit;
}
using (Database database = new Database(databaseFile, OpenDatabase.Transact))
{
// Just use the English codepage, because the tables we're importing only have binary streams / MSI identifiers / other non-localizable content
int codepage = 1252;
// list of certificates for this database (hash/identifier)
Dictionary certificates = new Dictionary();
// Reset the in-memory tables for this new database
Table digitalSignatureTable = new Table(null, this.tableDefinitions["MsiDigitalSignature"]);
Table digitalCertificateTable = new Table(null, this.tableDefinitions["MsiDigitalCertificate"]);
// If any digital signature records exist that are not of the media type, preserve them
if (database.TableExists("MsiDigitalSignature"))
{
using (View digitalSignatureView = database.OpenExecuteView("SELECT `Table`, `SignObject`, `DigitalCertificate_`, `Hash` FROM `MsiDigitalSignature` WHERE `Table` <> 'Media'"))
{
while (true)
{
using (Record digitalSignatureRecord = digitalSignatureView.Fetch())
{
if (null == digitalSignatureRecord)
{
break;
}
Row digitalSignatureRow = null;
digitalSignatureRow = digitalSignatureTable.CreateRow(null);
string table = digitalSignatureRecord.GetString(0);
string signObject = digitalSignatureRecord.GetString(1);
digitalSignatureRow[0] = table;
digitalSignatureRow[1] = signObject;
digitalSignatureRow[2] = digitalSignatureRecord.GetString(2);
if (false == digitalSignatureRecord.IsNull(3))
{
// Export to a file, because the MSI API's require us to provide a file path on disk
string hashPath = Path.Combine(this.TempFilesLocation, "MsiDigitalSignature");
string hashFileName = string.Concat(table, ".", signObject, ".bin");
Directory.CreateDirectory(hashPath);
hashPath = Path.Combine(hashPath, hashFileName);
using (FileStream fs = File.Create(hashPath))
{
int bytesRead;
byte[] buffer = new byte[1024 * 4];
while (0 != (bytesRead = digitalSignatureRecord.GetStream(3, buffer, buffer.Length)))
{
fs.Write(buffer, 0, bytesRead);
}
}
digitalSignatureRow[3] = hashFileName;
}
}
}
}
}
// If any digital certificates exist, extract and preserve them
if (database.TableExists("MsiDigitalCertificate"))
{
using (View digitalCertificateView = database.OpenExecuteView("SELECT * FROM `MsiDigitalCertificate`"))
{
while (true)
{
using (Record digitalCertificateRecord = digitalCertificateView.Fetch())
{
if (null == digitalCertificateRecord)
{
break;
}
string certificateId = digitalCertificateRecord.GetString(1); // get the identifier of the certificate
// Export to a file, because the MSI API's require us to provide a file path on disk
string certPath = Path.Combine(this.TempFilesLocation, "MsiDigitalCertificate");
Directory.CreateDirectory(certPath);
certPath = Path.Combine(certPath, string.Concat(certificateId, ".cer"));
using (FileStream fs = File.Create(certPath))
{
int bytesRead;
byte[] buffer = new byte[1024 * 4];
while (0 != (bytesRead = digitalCertificateRecord.GetStream(2, buffer, buffer.Length)))
{
fs.Write(buffer, 0, bytesRead);
}
}
// Add it to our "add to MsiDigitalCertificate" table dictionary
Row digitalCertificateRow = digitalCertificateTable.CreateRow(null);
digitalCertificateRow[0] = certificateId;
// Now set the file path on disk where this binary stream will be picked up at import time
digitalCertificateRow[1] = string.Concat(certificateId, ".cer");
// Load the cert to get it's thumbprint
X509Certificate cert = X509Certificate.CreateFromCertFile(certPath);
X509Certificate2 cert2 = new X509Certificate2(cert);
certificates.Add(cert2.Thumbprint, certificateId);
}
}
}
}
using (View mediaView = database.OpenExecuteView("SELECT * FROM `Media`"))
{
while (true)
{
using (Record mediaRecord = mediaView.Fetch())
{
if (null == mediaRecord)
{
break;
}
X509Certificate2 cert2 = null;
Row digitalSignatureRow = null;
string cabName = mediaRecord.GetString(4); // get the name of the cab
// If there is no cabinet or it's an internal cab, skip it.
if (String.IsNullOrEmpty(cabName) || cabName.StartsWith("#", StringComparison.Ordinal))
{
continue;
}
string cabId = mediaRecord.GetString(1); // get the ID of the cab
string cabPath = Path.Combine(Path.GetDirectoryName(databaseFile), cabName);
// If the cabs aren't there, throw an error but continue to catch the other errors
if (!File.Exists(cabPath))
{
this.OnMessage(WixErrors.WixFileNotFound(cabPath));
continue;
}
try
{
// Get the certificate from the cab
X509Certificate signedFileCert = X509Certificate.CreateFromSignedFile(cabPath);
cert2 = new X509Certificate2(signedFileCert);
}
catch (System.Security.Cryptography.CryptographicException e)
{
uint HResult = unchecked((uint)Marshal.GetHRForException(e));
// If the file has no cert, continue, but flag that we found at least one so we can later give a warning
if (0x80092009 == HResult) // CRYPT_E_NO_MATCH
{
foundUnsignedExternals = true;
continue;
}
// todo: exactly which HRESULT corresponds to this issue?
// If it's one of these exact platforms, warn the user that it may be due to their OS.
if ((5 == Environment.OSVersion.Version.Major && 2 == Environment.OSVersion.Version.Minor) || // W2K3
(5 == Environment.OSVersion.Version.Major && 1 == Environment.OSVersion.Version.Minor)) // XP
{
this.OnMessage(WixErrors.UnableToGetAuthenticodeCertOfFileDownlevelOS(cabPath, String.Format(CultureInfo.InvariantCulture, "HRESULT: 0x{0:x8}", HResult)));
}
else // otherwise, generic error
{
this.OnMessage(WixErrors.UnableToGetAuthenticodeCertOfFile(cabPath, String.Format(CultureInfo.InvariantCulture, "HRESULT: 0x{0:x8}", HResult)));
}
}
// If we haven't added this cert to the MsiDigitalCertificate table, set it up to be added
if (!certificates.ContainsKey(cert2.Thumbprint))
{
// generate a stable identifier
string certificateGeneratedId = Common.GenerateIdentifier("cer", cert2.Thumbprint);
// Add it to our "add to MsiDigitalCertificate" table dictionary
Row digitalCertificateRow = digitalCertificateTable.CreateRow(null);
digitalCertificateRow[0] = certificateGeneratedId;
// Export to a file, because the MSI API's require us to provide a file path on disk
string certPath = Path.Combine(this.TempFilesLocation, "MsiDigitalCertificate");
Directory.CreateDirectory(certPath);
certPath = Path.Combine(certPath, string.Concat(cert2.Thumbprint, ".cer"));
File.Delete(certPath);
using (BinaryWriter writer = new BinaryWriter(File.Open(certPath, FileMode.Create)))
{
writer.Write(cert2.RawData);
writer.Close();
}
// Now set the file path on disk where this binary stream will be picked up at import time
digitalCertificateRow[1] = string.Concat(cert2.Thumbprint, ".cer");
certificates.Add(cert2.Thumbprint, certificateGeneratedId);
}
digitalSignatureRow = digitalSignatureTable.CreateRow(null);
digitalSignatureRow[0] = "Media";
digitalSignatureRow[1] = cabId;
digitalSignatureRow[2] = certificates[cert2.Thumbprint];
}
}
}
if (digitalCertificateTable.Rows.Count > 0)
{
database.ImportTable(codepage, digitalCertificateTable, this.TempFilesLocation, true);
shouldCommit = true;
}
if (digitalSignatureTable.Rows.Count > 0)
{
database.ImportTable(codepage, digitalSignatureTable, this.TempFilesLocation, true);
shouldCommit = true;
}
// TODO: if we created the table(s), then we should add the _Validation records for them.
certificates = null;
// If we did find external cabs but none of them were signed, give a warning
if (foundUnsignedExternals)
{
this.OnMessage(WixWarnings.ExternalCabsAreNotSigned(databaseFile));
}
if (shouldCommit)
{
database.Commit();
}
}
return shouldCommit;
}
///
/// Cleans up the temp files used by the Inscriber.
///
/// True if all files were deleted, false otherwise.
public bool DeleteTempFiles()
{
#if REDO_IN_NETCORE
if (null == this.tempFiles)
{
return true; // no work to do
}
else
{
bool deleted = Common.DeleteTempFiles(this.TempFilesLocation, this);
if (deleted)
{
((IDisposable)this.tempFiles).Dispose();
this.tempFiles = null; // temp files have been deleted, no need to remember this now
}
return deleted;
}
#endif
return true;
}
public void OnMessage(MessageEventArgs e)
{
Messaging.Instance.OnMessage(e);
}
}
}