OpenXML 使用密码保护电子表格工作簿

OpenXML Protect Spreadsheet Workbook with Password

我正在尝试使用 OpenXML SDK 创建受保护的电子表格文档。但是,生成的 WorkbookHashValue 不正确,因此工作簿无法解除保护。

var password = Encoding.UTF8.GetBytes("123");
var salt = new byte[16];
new RNGCryptoServiceProvider().GetNonZeroBytes(salt);
var spinCount = 100000U;

using (var document = SpreadsheetDocument.Create("text.xlsx", SpreadsheetDocumentType.Workbook))
{
    var workbookPart = document.AddWorkbookPart();
    var workbook = new Workbook();
    WorkbookProtection workbookProtection = new WorkbookProtection()
    {
        LockStructure = true,
        WorkbookAlgorithmName = "SHA-512",
        WorkbookHashValue = Convert.ToBase64String(GetPasswordHash(password, salt, spinCount)),
        WorkbookSaltValue = Convert.ToBase64String(salt),
        WorkbookSpinCount = spinCount
    };
    var sheets = new Sheets();
    var sheet = new Sheet
    {
        Name = "Sheet 1",
        SheetId = 1U,
        Id = "rId1"
    };
    sheets.Append(sheet);
    workbook.Append(workbookProtection);
    workbook.Append(sheets);
    workbookPart.Workbook = workbook;

    var worksheetPart = workbookPart.AddNewPart<WorksheetPart>("rId1");
    var worksheet = new Worksheet();
    var sheetData = new SheetData();
    worksheet.Append(sheetData);
    worksheetPart.Worksheet = worksheet;
}
private byte[] GetPasswordHash(byte[] password, byte[] salt, uint spinCount)
{
    using (var sha512 = SHA512.Create())
    {
        var buffer = new byte[salt.Length + password.Length];
        Array.Copy(salt, buffer, salt.Length);
        Array.Copy(password, 0, buffer, salt.Length, password.Length);
        byte[] hash = sha512.ComputeHash(buffer);
        buffer = new byte[hash.Length + 4];
        for (var i = 0U; i < spinCount; i++)
        {
            Array.Copy(hash, buffer, hash.Length);
            Array.Copy(BitConverter.GetBytes(i), 0, buffer, hash.Length, 4);
            hash = sha512.ComputeHash(buffer);
        }
        return hash;
    }
}

使用 salt VAQd0dyl7U67APquHio1lQ== 的密码 123 的正确哈希值应该是 2ZwXmW83qax0iUfzSkbhwAOVSDHAm6S/v9irWWTzdoFDgzO2Kc82P3Z9BAwbWqFLzN4rKaL0APOMzQ5tA7TBDw==,但是上面的代码生成了 z5ebojaXN/sD4ps9yurRCpSTDp+kSuTz+HN2PyKmGuicNgszAPKxfsE+kTgOEbGhT/VqSbwTd++oyAJxJh0L3A==.

我尝试对比了Apache POI的源代码,没有发现任何错误

public static byte[] hashPassword(String password, HashAlgorithm hashAlgorithm, byte[] salt, int spinCount, boolean iteratorFirst) {
    // If no password was given, use the default
    if (password == null) {
        password = Decryptor.DEFAULT_PASSWORD;
    }

    MessageDigest hashAlg = getMessageDigest(hashAlgorithm);

    hashAlg.update(salt);
    byte[] hash = hashAlg.digest(StringUtil.getToUnicodeLE(password));
    byte[] iterator = new byte[LittleEndianConsts.INT_SIZE];

    byte[] first = (iteratorFirst ? iterator : hash);
    byte[] second = (iteratorFirst ? hash : iterator);

    try {
        for (int i = 0; i < spinCount; i++) {
            LittleEndian.putInt(iterator, 0, i);
            hashAlg.reset();
            hashAlg.update(first);
            hashAlg.update(second);
            hashAlg.digest(hash, 0, hash.length); // don't create hash buffer everytime new
        }
    } catch (DigestException e) {
        throw new EncryptedDocumentException("error in password hashing");
    }

    return hash;
}

iteratorFirst 对于工作簿保护是错误的)

用于获取密码字节的编码应该是UTF16LE

Encoding.Unicode.GetBytes("123");