// Copyright Keysight Technologies 2012-2019
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at http://mozilla.org/MPL/2.0/.
using OpenTap.Cli;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Threading;
using System.Xml.Serialization;
namespace OpenTap.Package
{
/// SHA1 hashes the files of a TapPackage and includes it in the package.xml file.
internal class FileHashPackageAction : ICustomPackageAction
{
static TraceSource log = Log.CreateSource("Verify");
/// Returns PackageActionStage.Create.
public PackageActionStage ActionStage => PackageActionStage.Create;
internal static byte[] hashFile(string file)
{
try
{
if (false == File.Exists(file))
return Array.Empty();
var sha1 = SHA1.Create();
using (var fstr = File.OpenRead(file))
return sha1.ComputeHash(fstr);
}
catch (Exception e)
{
log.Error("{0}", e.Message);
log.Debug(e);
return Array.Empty();
}
}
/// SHA1 hash of a file in a package.
public class Hash : ICustomPackageData
{
/// Creates a new instance of Hash.
public Hash(byte[] hash)
{
Value = BitConverter.ToString(hash).Replace("-", "");
}
/// Creates a new instance of Hash.
public Hash() => Value = "";
/// The Base64 converted hash value.
[XmlText]
public string Value { get; set; }
byte[] GetBytes()
{
if (Value.Length == 40)
return StringToByteArray(Value);
else if (Value.Length == 28)
return Convert.FromBase64String(Value);
else if (Value.Length == 0)
return Array.Empty();
else
throw new FormatException("Value should be a hex or base64 encoded SHA1 hash");
}
/// Compares two hashes and returns true if they are the same.
public override bool Equals(object obj)
{
if (obj is Hash hsh)
{
return hsh.GetBytes().SequenceEqual(this.GetBytes());
}
return false;
}
/// Custom GetHashCode implementation.
public override int GetHashCode() => -1937169414 + EqualityComparer.Default.GetHashCode(Value);
}
public static byte[] StringToByteArray(string hex)
{
return Enumerable.Range(0, hex.Length)
.Where(x => x % 2 == 0)
.Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
.ToArray();
}
bool ICustomPackageAction.Execute(PackageDef package, CustomPackageActionArgs customActionArgs)
{
package.Files.AsParallel().ForAll(x => x.CustomData.Add(new Hash(hashFile(x.FileName))));
if (string.IsNullOrEmpty(package.Hash))
{
package.Hash = package.ComputeHash();
}
return true;
}
int ICustomPackageAction.Order() => 1001; // This should come after everything. Sign has 1000 (assumed it would be the last).
}
/// CLI Action to verify the installed packages by checking their hashes.
[Display("verify", "Verify the integrity of one or all installed packages by checking their fingerprints.", "package")]
class VerifyPackageHashes : ICliAction
{
static TraceSource log = Log.CreateSource("Verify");
static string Target = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
/// Verify a specific package.
[UnnamedCommandLineArgument("package", Required = false, Description = "The package to verify the hash for.")]
public string Package { get; set; }
int exitCode;
void verifyPackage(PackageDef pkg)
{
log.Debug("Verifying package: {0}", pkg.Name);
bool ok = true;
bool inconclusive = false;
var brokenFiles = new List<(PackageFile, string)>();
foreach (var file in pkg.Files)
{
var fn = Path.GetFileName(file.FileName);
var hash = file.CustomData.OfType().FirstOrDefault();
if (hash == null)
{
// hash not calculated for this package:
brokenFiles.Add((file, "is missing checksum information."));
inconclusive = true;
}
else
{
// Make file.FileName Linux friendly
if (file.FileName.Contains('\\'))
{
string repl = file.FileName.Replace('\\', '/');
log.Debug("Replacing '\\' with '/' in {0}", file.FileName);
file.FileName = repl;
}
string fullpath =
Path.Combine(pkg.IsSystemWide() ? PackageDef.SystemWideInstallationDirectory : Target,
file.FileName);
var hash2 = new FileHashPackageAction.Hash(FileHashPackageAction.hashFile(fullpath));
if (false == hash2.Equals(hash))
{
if (File.Exists(fullpath))
{
brokenFiles.Add((file, "has non-matching checksum."));
log.Debug("Hash does not match for '{0}'", file.FileName);
}
else
{
brokenFiles.Add((file, "is missing."));
log.Debug("File '{0}' is missing", file.FileName);
}
ok = false;
}
else
{
log.Debug("Hash matches for '{0}'", file.FileName);
}
}
}
void print_issues()
{
foreach (var x in brokenFiles)
log.Info("File '{0}' {1}", x.Item1.FileName, x.Item2);
}
if (!ok)
{
exitCode = (int)PackageExitCodes.InvalidPackageDefinition;
log.Error("Package '{0}' not verified.", pkg.Name);
print_issues();
}
else
{
if (inconclusive)
{
exitCode = (int)PackageExitCodes.InvalidPackageDefinition;
log.Warning("Package '{0}' is missing SHA1 checksum for verification.", pkg.Name);
print_issues();
}
else
{
log.Info("Package '{0}' verified.", pkg.Name);
}
}
}
int ICliAction.Execute(CancellationToken cancellationToken)
{
var installation = new Installation(Target);
var packages = installation.GetPackages();
if (string.IsNullOrEmpty(Package))
{
foreach (var package in packages)
verifyPackage(package);
}
else
{
var pkg = packages.FirstOrDefault(p => p.Name == Package);
if (pkg == null)
{
log.Error("Unable to locate package '{0}'", Package);
log.Info("Installed packages: {0}", string.Join(", ", packages.Select(x => x.Name)));
return (int)PackageExitCodes.InvalidPackageName;
}
verifyPackage(pkg);
}
return exitCode;
}
public static List<(PackageDef Package, PackageFile File, PackageDef OffendingPackage)> CalculatePackageInstallConflicts(IEnumerable installedPackages, IEnumerable newPackages)
{
var conflicts = new List<(PackageDef, PackageFile, PackageDef)>();
var allFiles = installedPackages.SelectMany(pkg => pkg.Files.Select(file => (pkg, file)))
.ToLookup(x => x.file.FileName, StringComparer.CurrentCultureIgnoreCase);
foreach (var package in newPackages)
{
var possibleConflicts = getConflictedFiles(allFiles, package);
conflicts.AddRange(possibleConflicts
// remove conflicts that came from the same package name. These may be overwritten.
.Where(x => x.Item1.Name != package.Name)
// remove conflicts in files that came from Dependencies.
.Where(x => x.Item2.FileName.StartsWith("Dependencies/", StringComparison.OrdinalIgnoreCase) == false)
.Select(x => (x.Item1, x.Item2, package)));
}
return conflicts;
}
static IEnumerable<(PackageDef, PackageFile)> getConflictedFiles(ILookup installedFilesLookup, PackageDef package)
{
var conflictedFiles = new List<(PackageDef, PackageFile)>();
foreach (var file in package.Files)
{
foreach (var installedFile in installedFilesLookup[file.FileName])
{
var hash1 = installedFile.Item2.CustomData.OfType().FirstOrDefault();
// Hash information does not exist. Lets just ignore it then.
if (hash1 == null) continue;
var hash = file.CustomData.OfType().FirstOrDefault();
// Hash information does not exist. Lets just ignore it then.
// We also cannot compare the binary files, because they might not have been downloaded yet.
if (hash == null) continue;
if (hash1.Equals(hash) == false)
{
// conflict detected!!
conflictedFiles.Add(installedFile);
}
}
}
return conflictedFiles;
}
}
}