File: Signing\Archive\SignedPackageArchiveUtility.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Packaging\NuGet.Packaging.csproj (NuGet.Packaging)
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

// Zip Spec here: http://www.pkware.com/documents/casestudies/APPNOTE.TXT

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.Pkcs;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NuGet.Common;

namespace NuGet.Packaging.Signing
{
    public static class SignedPackageArchiveUtility
    {
        private static readonly SigningSpecifications _signingSpecification = SigningSpecifications.V1;

        /// <summary>
        /// Utility method to know if a zip archive is signed.
        /// </summary>
        /// <param name="reader">Binary reader pointing to a zip archive.</param>
        /// <returns>true if the given archive is signed</returns>
        public static bool IsSigned(BinaryReader reader)
        {
            if (reader == null)
            {
                throw new ArgumentNullException(nameof(reader));
            }

            try
            {
                var endOfCentralDirectoryRecord = EndOfCentralDirectoryRecord.Read(reader);

                // Look for signature central directory record
                reader.BaseStream.Seek(endOfCentralDirectoryRecord.OffsetOfStartOfCentralDirectory, SeekOrigin.Begin);

                while (CentralDirectoryHeader.TryRead(reader, out CentralDirectoryHeader? centralDirectoryHeader))
                {
                    if (IsPackageSignatureFileEntry(
                        centralDirectoryHeader.FileName,
                        centralDirectoryHeader.GeneralPurposeBitFlag))
                    {
                        // Go to local file header
                        reader.BaseStream.Seek(centralDirectoryHeader.RelativeOffsetOfLocalHeader, SeekOrigin.Begin);

                        // Make sure local file header exists
                        if (!LocalFileHeader.TryRead(reader, out LocalFileHeader? localFileHeader))
                        {
                            throw new InvalidDataException(Strings.ErrorInvalidPackageArchive);
                        }

                        return IsPackageSignatureFileEntry(
                            localFileHeader.FileName,
                            localFileHeader.GeneralPurposeBitFlag);
                    }
                }
            }
            // Ignore any exception. If something is thrown it means the archive is either not valid or not signed
            catch { }

            return false;
        }

        /// <summary>
        /// Opens a read-only stream for the package signature file.
        /// </summary>
        /// <remarks>Callers should first verify that a package is signed before calling this method.</remarks>
        /// <param name="reader">A binary reader for a signed package.</param>
        /// <returns>A readable stream.</returns>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="reader" /> is <see langword="null" />.</exception>
        /// <exception cref="SignatureException">Thrown if a package signature file is invalid or missing.</exception>
        public static Stream OpenPackageSignatureFileStream(BinaryReader reader)
        {
            if (reader == null)
            {
                throw new ArgumentNullException(nameof(reader));
            }

            var metadata = SignedPackageArchiveIOUtility.ReadSignedArchiveMetadata(reader);
            var signatureCentralDirectoryHeader = metadata.GetPackageSignatureFileCentralDirectoryHeaderMetadata();

            return GetPackageSignatureFile(reader, signatureCentralDirectoryHeader);
        }

        private static Stream GetPackageSignatureFile(
            BinaryReader reader,
            CentralDirectoryHeaderMetadata signatureCentralDirectoryHeader)
        {
            var localFileHeader = ReadPackageSignatureFileLocalFileHeader(reader, signatureCentralDirectoryHeader);
            var offsetToData = signatureCentralDirectoryHeader.OffsetToLocalFileHeader +
                LocalFileHeader.SizeInBytesOfFixedLengthFields +
                localFileHeader.FileNameLength +
                localFileHeader.ExtraFieldLength;

            var buffer = new byte[localFileHeader.UncompressedSize];

            reader.BaseStream.Seek(offsetToData, SeekOrigin.Begin);
#if NET
            reader.BaseStream.ReadExactly(buffer, offset: 0, count: buffer.Length);
#else
            int count = buffer.Length;
            int offset = 0;
            while (count > 0)
            {
                int read = reader.BaseStream.Read(buffer, offset, count);
                if (read <= 0)
                {
                    throw new EndOfStreamException();
                }
                offset += read;
                count -= read;
            }
#endif

            return new MemoryStream(buffer, writable: false);
        }

        private static LocalFileHeader ReadPackageSignatureFileLocalFileHeader(
            BinaryReader reader,
            CentralDirectoryHeaderMetadata signatureCentralDirectoryHeader)
        {
            reader.BaseStream.Seek(signatureCentralDirectoryHeader.OffsetToLocalFileHeader, SeekOrigin.Begin);

            if (!LocalFileHeader.TryRead(reader, out LocalFileHeader? header))
            {
                throw new SignatureException(NuGetLogCode.NU3005, Strings.InvalidPackageSignatureFile);
            }

            return header;
        }

        internal static bool IsPackageSignatureFileEntry(byte[] fileName, ushort generalPurposeBitFlag)
        {
            if (fileName == null || IsUtf8(generalPurposeBitFlag))
            {
                return false;
            }

            var expectedFileName = Encoding.ASCII.GetBytes(_signingSpecification.SignaturePath);

            // The ZIP format specification says the code page should be IBM code page 437 (CP437)
            // if bit 11 of the general purpose bit flag field is not set.  CP437 is not the same
            // as ASCII, but there is overlap.
            // All characters in the package signature file name are in that overlap, so we can
            // use the ASCII decoder instead of pulling in a new package for full CP437 support.
            return fileName.SequenceEqual(expectedFileName);
        }

        public static bool IsZip64(BinaryReader reader)
        {
            if (reader == null)
            {
                throw new ArgumentNullException(nameof(reader));
            }

            var endOfCentralDirectoryRecord = EndOfCentralDirectoryRecord.Read(reader);

            if (endOfCentralDirectoryRecord.NumberOfThisDisk != endOfCentralDirectoryRecord.NumberOfTheDiskWithTheStartOfTheCentralDirectory)
            {
                return false;
            }

            var offset = endOfCentralDirectoryRecord.OffsetFromStart - Zip64EndOfCentralDirectoryLocator.SizeInBytes;

            if (offset >= 0)
            {
                reader.BaseStream.Seek(offset, SeekOrigin.Begin);

                if (Zip64EndOfCentralDirectoryLocator.Exists(reader))
                {
                    return true;
                }
            }

            reader.BaseStream.Seek(endOfCentralDirectoryRecord.OffsetOfStartOfCentralDirectory, SeekOrigin.Begin);

            while (CentralDirectoryHeader.TryRead(reader, out CentralDirectoryHeader? centralDirectoryHeader))
            {
                if (HasZip64ExtendedInformationExtraField(centralDirectoryHeader))
                {
                    return true;
                }

                if (centralDirectoryHeader.DiskNumberStart != endOfCentralDirectoryRecord.NumberOfThisDisk)
                {
                    continue;
                }

                var savedPosition = reader.BaseStream.Position;

                reader.BaseStream.Position = centralDirectoryHeader.RelativeOffsetOfLocalHeader;

                if (LocalFileHeader.TryRead(reader, out LocalFileHeader? localFileHeader) &&
                    HasZip64ExtendedInformationExtraField(localFileHeader))
                {
                    return true;
                }

                reader.BaseStream.Position = savedPosition;
            }

            return false;
        }

        private static bool HasZip64ExtendedInformationExtraField(CentralDirectoryHeader header)
        {
            if (ExtraField.TryRead(header, out IReadOnlyList<ExtraField>? extraFields))
            {
                return extraFields.Any(extraField => extraField is Zip64ExtendedInformationExtraField);
            }

            return false;
        }

        private static bool HasZip64ExtendedInformationExtraField(LocalFileHeader header)
        {
            if (ExtraField.TryRead(header, out IReadOnlyList<ExtraField>? extraFields))
            {
                return extraFields.Any(extraField => extraField is Zip64ExtendedInformationExtraField);
            }

            return false;
        }

        /// <summary>
        /// Removes repository primary signature (if it exists) or any repository countersignature (if it exists).
        /// </summary>
        /// <param name="input">A readable stream for a signed package.</param>
        /// <param name="output">A read/write stream for receiving an updated package.</param>
        /// <param name="cancellationToken">A cancellation token.</param>
        /// <returns>A flag indicating whether or not a signature was removed.</returns>
        public static async Task<bool> RemoveRepositorySignaturesAsync(
            Stream input,
            Stream output,
            CancellationToken cancellationToken)
        {
            if (input == null)
            {
                throw new ArgumentNullException(nameof(input));
            }

            if (output == null)
            {
                throw new ArgumentNullException(nameof(output));
            }

            cancellationToken.ThrowIfCancellationRequested();

            PrimarySignature? primarySignature;

            using (var packageReader = new PackageArchiveReader(input, leaveStreamOpen: true))
            {
                primarySignature = await packageReader.GetPrimarySignatureAsync(cancellationToken);
            }

            if (primarySignature == null)
            {
                return false;
            }

            switch (primarySignature.Type)
            {
                case SignatureType.Repository:
                    await RemoveRepositoryPrimarySignatureAsync(input, output, cancellationToken);

                    return true;

                default:
                    return await RemoveRepositoryCountersignaturesAsync(
                        input,
                        output,
                        primarySignature.SignedCms,
                        cancellationToken);
            }
        }

        private static Task RemoveRepositoryPrimarySignatureAsync(
            Stream input,
            Stream output,
            CancellationToken cancellationToken)
        {
            using (var package = new SignedPackageArchive(input, output))
            {
                return package.RemoveSignatureAsync(cancellationToken);
            }
        }

        private static async Task<bool> RemoveRepositoryCountersignaturesAsync(
            Stream input,
            Stream output,
            SignedCms signedCms,
            CancellationToken cancellationToken)
        {
            if (TryRemoveRepositoryCountersignatures(signedCms, out var updatedSignedCms))
            {
                var primarySignature = PrimarySignature.Load(updatedSignedCms.Encode());

                using (var unsignedPackage = new MemoryStream())
                {
                    using (var package = new SignedPackageArchive(input, unsignedPackage))
                    {
                        await package.RemoveSignatureAsync(cancellationToken);
                    }

                    using (var package = new SignedPackageArchive(unsignedPackage, output))
                    using (var signatureStream = new MemoryStream(primarySignature.GetBytes()))
                    {
                        await package.AddSignatureAsync(signatureStream, cancellationToken);
                    }
                }

                return true;
            }

            return false;
        }

        private static bool TryRemoveRepositoryCountersignatures(SignedCms signedCms, [NotNullWhen(returnValue: true)] out SignedCms? updatedSignedCms)
        {
            updatedSignedCms = null;

            // SignerInfo.CouterSignerInfos returns a mutable copy of countersigners.  This copy does not reflect
            // the removal of countersigners via SignerInfo.RemoveCounterSignature(...).
            // Also, SignerInfo.UnsignedAttributes is defined as an ASN.1 SET, which is an unordered collection.
            // The underlying platform may reorder elements when modifying the collection, so obtaining updated
            // indicies is necessary after removing an attribute.
            var tempSignedCms = new SignedCms();

            tempSignedCms = Reencode(signedCms);

            while (true)
            {
                var repositoryCountersignatureFound = false;
                var primarySigner = tempSignedCms.SignerInfos[0];
                var countersigners = primarySigner.CounterSignerInfos;

                for (var i = 0; i < countersigners.Count; ++i)
                {
                    var countersigner = countersigners[i];

                    if (AttributeUtility.GetSignatureType(countersigner.SignedAttributes) == SignatureType.Repository)
                    {
                        repositoryCountersignatureFound = true;

                        primarySigner.RemoveCounterSignature(i);

                        // This is a workaround to SignerInfo.CounterSignerInfos not reflecting changes in the signed CMS.
                        tempSignedCms = Reencode(tempSignedCms);
                        updatedSignedCms = tempSignedCms;

                        // Indices of other countersignatures may have changed unexpectedly as a result
                        // of removing a countersignature.
                        break;
                    }
                }

                if (!repositoryCountersignatureFound)
                {
                    break;
                }
            }

            return updatedSignedCms != null;
        }

        private static SignedCms Reencode(SignedCms signedCms)
        {
            var newSignedCms = new SignedCms();

            newSignedCms.Decode(signedCms.Encode());

            return newSignedCms;
        }

        /// <summary>
        /// Signs a Zip with the contents in the SignatureStream using the writer.
        /// The reader is used to read the exisiting contents for the Zip.
        /// </summary>
        /// <param name="signatureStream">MemoryStream of the signature to be inserted into the zip.</param>
        /// <param name="reader">BinaryReader to be used to read the existing zip data.</param>
        /// <param name="writer">BinaryWriter to be used to write the signature into the zip.</param>
        internal static void SignZip(MemoryStream signatureStream, BinaryReader reader, BinaryWriter writer)
        {
            SignedPackageArchiveIOUtility.WriteSignatureIntoZip(signatureStream, reader, writer);
        }

        internal static void UnsignZip(BinaryReader reader, BinaryWriter writer)
        {
            if (reader == null)
            {
                throw new ArgumentNullException(nameof(reader));
            }

            if (writer == null)
            {
                throw new ArgumentNullException(nameof(writer));
            }

            SignedPackageArchiveIOUtility.RemoveSignature(reader, writer);
        }

        internal static void HashUInt16(HashAlgorithm hashAlgorithm, ushort value)
        {
            byte[] array = BitConverter.GetBytes(value);
            if (!BitConverter.IsLittleEndian)
            {
                Array.Reverse(array);
            }
            SignedPackageArchiveIOUtility.HashBytes(hashAlgorithm, array);
        }

        internal static void HashUInt32(HashAlgorithm hashAlgorithm, uint value)
        {
            byte[] array = BitConverter.GetBytes(value);
            if (!BitConverter.IsLittleEndian)
            {
                Array.Reverse(array);
            }
            SignedPackageArchiveIOUtility.HashBytes(hashAlgorithm, array);
        }

        /// <summary>
        /// Verifies that a signed package archive's signature is valid and it has not been tampered with.
        ///
        /// </summary>
        /// <param name="reader">Signed package to verify</param>
        /// <param name="hashAlgorithm">Hash algorithm to be used to hash data.</param>
        /// <param name="expectedHash">Hash value of the original data.</param>
        /// <returns>True if package archive's hash matches the expected hash</returns>
        internal static bool VerifySignedPackageIntegrity(BinaryReader reader, HashAlgorithm hashAlgorithm, byte[] expectedHash)
        {
            if (reader == null)
            {
                throw new ArgumentNullException(nameof(reader));
            }

            if (hashAlgorithm == null)
            {
                throw new ArgumentNullException(nameof(hashAlgorithm));
            }

            if (expectedHash == null || expectedHash.Length == 0)
            {
                throw new ArgumentException(Strings.ArgumentCannotBeNullOrEmpty, nameof(reader));
            }

            // Make sure it is signed with a valid signature file
            if (!IsSigned(reader))
            {
                throw new SignatureException(NuGetLogCode.NU3003, Strings.SignedPackageNotSignedOnVerify);
            }

            var metadata = SignedPackageArchiveIOUtility.ReadSignedArchiveMetadata(reader);
            var signatureCentralDirectoryHeader = metadata.GetPackageSignatureFileCentralDirectoryHeaderMetadata();
            var centralDirectoryRecordsWithoutSignature = RemoveSignatureAndOrderByOffset(metadata);

            try
            {
                // Read and hash from the start of the archive to the start of the file headers
                reader.BaseStream.Seek(offset: 0, origin: SeekOrigin.Begin);
                SignedPackageArchiveIOUtility.ReadAndHashUntilPosition(reader, hashAlgorithm, metadata.StartOfLocalFileHeaders);

                // Read and hash file headers
                foreach (var record in centralDirectoryRecordsWithoutSignature)
                {
                    reader.BaseStream.Seek(offset: record.OffsetToLocalFileHeader, origin: SeekOrigin.Begin);
                    SignedPackageArchiveIOUtility.ReadAndHashUntilPosition(reader, hashAlgorithm, record.OffsetToLocalFileHeader + record.FileEntryTotalSize);
                }

                // Order central directory records by their position
                centralDirectoryRecordsWithoutSignature.Sort((x, y) => x.Position.CompareTo(y.Position));

                // Update offset of any central directory record that has a file entry after signature
                foreach (var record in centralDirectoryRecordsWithoutSignature)
                {
                    reader.BaseStream.Seek(offset: record.Position, origin: SeekOrigin.Begin);
                    // Hash from the start of the central directory record until the relative offset of local file header (42 from the start of central directory record, including signature length)
                    SignedPackageArchiveIOUtility.ReadAndHashUntilPosition(reader, hashAlgorithm, reader.BaseStream.Position + 42);

                    var relativeOffsetOfLocalFileHeader = (uint)(reader.ReadUInt32() + record.ChangeInOffset);
                    HashUInt32(hashAlgorithm, relativeOffsetOfLocalFileHeader);

                    // Continue hashing file name, extra field, and file comment fields.
                    SignedPackageArchiveIOUtility.ReadAndHashUntilPosition(reader, hashAlgorithm, reader.BaseStream.Position + record.HeaderSize - CentralDirectoryHeader.SizeInBytesOfFixedLengthFields);
                }

                reader.BaseStream.Seek(offset: metadata.EndOfCentralDirectory, origin: SeekOrigin.Begin);

                // Hash until total entries in end of central directory record (8 bytes from the start of EOCDR)
                SignedPackageArchiveIOUtility.ReadAndHashUntilPosition(reader, hashAlgorithm, metadata.EndOfCentralDirectory + 8);

                var eocdrTotalEntries = (ushort)(reader.ReadUInt16() - 1);
                var eocdrTotalEntriesOnDisk = (ushort)(reader.ReadUInt16() - 1);

                HashUInt16(hashAlgorithm, eocdrTotalEntries);
                HashUInt16(hashAlgorithm, eocdrTotalEntriesOnDisk);

                // update the central directory size by substracting the size of the package signature file's central directory header
                var eocdrSizeOfCentralDirectory = (uint)(reader.ReadUInt32() - signatureCentralDirectoryHeader.HeaderSize);
                HashUInt32(hashAlgorithm, eocdrSizeOfCentralDirectory);

                var eocdrOffsetOfCentralDirectory = reader.ReadUInt32() - (uint)signatureCentralDirectoryHeader.FileEntryTotalSize;
                HashUInt32(hashAlgorithm, eocdrOffsetOfCentralDirectory);

                // Hash until the end of the reader
                SignedPackageArchiveIOUtility.ReadAndHashUntilPosition(reader, hashAlgorithm, reader.BaseStream.Length);

                hashAlgorithm.TransformFinalBlock(Array.Empty<byte>(), inputOffset: 0, inputCount: 0);

                return CompareHash(expectedHash, hashAlgorithm.Hash!);
            }
            // If exception is throw in means the archive was not a valid package. It has been tampered, return false.
            catch { }

            return false;
        }

        private static List<CentralDirectoryHeaderMetadata> RemoveSignatureAndOrderByOffset(SignedPackageArchiveMetadata metadata)
        {
            // Remove signature cdr
            var centralDirectoryRecordsList = metadata.CentralDirectoryHeaders.Where((v, i) => i != metadata.SignatureCentralDirectoryHeaderIndex).ToList();

            // Sort by order of file entries
            centralDirectoryRecordsList.Sort((x, y) => x.OffsetToLocalFileHeader.CompareTo(y.OffsetToLocalFileHeader));

            // Update offsets with removed signature
            var previousRecordFileEntryEnd = 0L;
            foreach (var centralDirectoryRecord in centralDirectoryRecordsList)
            {
                centralDirectoryRecord.ChangeInOffset = previousRecordFileEntryEnd - centralDirectoryRecord.OffsetToLocalFileHeader;

                previousRecordFileEntryEnd = centralDirectoryRecord.OffsetToLocalFileHeader + centralDirectoryRecord.FileEntryTotalSize + centralDirectoryRecord.ChangeInOffset;
            }

            return centralDirectoryRecordsList;
        }

        internal static void HashUInt16(Sha512HashFunction hashFunc, ushort value)
        {
            byte[] array = BitConverter.GetBytes(value);
            if (!BitConverter.IsLittleEndian)
            {
                Array.Reverse(array);
            }
            SignedPackageArchiveIOUtility.HashBytes(hashFunc, array);
        }

        internal static void HashUInt32(Sha512HashFunction hashFunc, uint value)
        {
            byte[] array = BitConverter.GetBytes(value);
            if (!BitConverter.IsLittleEndian)
            {
                Array.Reverse(array);
            }
            SignedPackageArchiveIOUtility.HashBytes(hashFunc, array);
        }

        internal static string GetPackageContentHash(BinaryReader reader)
        {
            using (var hashFunc = new Sha512HashFunction())
            {
                // skip validating signature entry since we're just trying to get the content hash here instead of
                // verifying signature entry.
                var metadata = SignedPackageArchiveIOUtility.ReadSignedArchiveMetadata(reader, validateSignatureEntry: false);
                var signatureCentralDirectoryHeader = metadata.GetPackageSignatureFileCentralDirectoryHeaderMetadata();
                var centralDirectoryRecordsWithoutSignature = RemoveSignatureAndOrderByOffset(metadata);

                // Read and hash from the start of the archive to the start of the file headers
                reader.BaseStream.Seek(offset: 0, origin: SeekOrigin.Begin);
                SignedPackageArchiveIOUtility.ReadAndHashUntilPosition(reader, hashFunc, metadata.StartOfLocalFileHeaders);

                // Read and hash file headers
                foreach (var record in centralDirectoryRecordsWithoutSignature)
                {
                    reader.BaseStream.Seek(offset: record.OffsetToLocalFileHeader, origin: SeekOrigin.Begin);
                    SignedPackageArchiveIOUtility.ReadAndHashUntilPosition(reader, hashFunc, record.OffsetToLocalFileHeader + record.FileEntryTotalSize);
                }

                // Order central directory records by their position
                centralDirectoryRecordsWithoutSignature.Sort((x, y) => x.Position.CompareTo(y.Position));

                // Update offset of any central directory record that has a file entry after signature
                foreach (var record in centralDirectoryRecordsWithoutSignature)
                {
                    reader.BaseStream.Seek(offset: record.Position, origin: SeekOrigin.Begin);
                    // Hash from the start of the central directory record until the relative offset of local file header (42 from the start of central directory record, including signature length)
                    SignedPackageArchiveIOUtility.ReadAndHashUntilPosition(reader, hashFunc, reader.BaseStream.Position + 42);

                    var relativeOffsetOfLocalFileHeader = (uint)(reader.ReadUInt32() + record.ChangeInOffset);
                    HashUInt32(hashFunc, relativeOffsetOfLocalFileHeader);

                    // Continue hashing file name, extra field, and file comment fields.
                    SignedPackageArchiveIOUtility.ReadAndHashUntilPosition(reader, hashFunc, reader.BaseStream.Position + record.HeaderSize - CentralDirectoryHeader.SizeInBytesOfFixedLengthFields);
                }

                reader.BaseStream.Seek(offset: metadata.EndOfCentralDirectory, origin: SeekOrigin.Begin);

                // Hash until total entries in end of central directory record (8 bytes from the start of EOCDR)
                SignedPackageArchiveIOUtility.ReadAndHashUntilPosition(reader, hashFunc, metadata.EndOfCentralDirectory + 8);

                var eocdrTotalEntries = (ushort)(reader.ReadUInt16() - 1);
                var eocdrTotalEntriesOnDisk = (ushort)(reader.ReadUInt16() - 1);

                HashUInt16(hashFunc, eocdrTotalEntries);
                HashUInt16(hashFunc, eocdrTotalEntriesOnDisk);

                // update the central directory size by substracting the size of the package signature file's central directory header
                var eocdrSizeOfCentralDirectory = (uint)(reader.ReadUInt32() - signatureCentralDirectoryHeader.HeaderSize);
                HashUInt32(hashFunc, eocdrSizeOfCentralDirectory);

                var eocdrOffsetOfCentralDirectory = reader.ReadUInt32() - (uint)signatureCentralDirectoryHeader.FileEntryTotalSize;
                HashUInt32(hashFunc, eocdrOffsetOfCentralDirectory);

                // Hash until the end of the reader
                SignedPackageArchiveIOUtility.ReadAndHashUntilPosition(reader, hashFunc, reader.BaseStream.Length);

                hashFunc.Update(Array.Empty<byte>(), offset: 0, count: 0);

                return hashFunc.GetHash();
            }
        }

        internal static bool IsUtf8(ushort generalPurposeBitFlags)
        {
            return (generalPurposeBitFlags & (1 << 11)) != 0;
        }

        private static bool CompareHash(byte[] expectedHash, byte[] actualHash)
        {
            if (expectedHash.Length != actualHash.Length)
            {
                return false;
            }

            for (var i = 0; i < expectedHash.Length; i++)
            {
                if (expectedHash[i] != actualHash[i])
                {
                    return false;
                }
            }
            return true;
        }

    }
}