private static bool IsOwnerPasswordRevision5And6(byte[] passwordBytes, EncryptionDictionary encryptionDictionary) { // Test the password against the user key by computing the SHA-256 hash of the UTF-8 password concatenated with the 8 bytes of Owner Validation Salt and the 48 byte U string. // If the 32 byte result matches the first 32 bytes of the O string, this is the user password. var truncatedPassword = TruncatePasswordTo127Bytes(passwordBytes); // The 48-byte string consisting of the 32-byte hash followed by the Owner Validation Salt followed by the Owner Key Salt is stored as the O key. var ownerHash = new byte[32]; var validationSalt = new byte[8]; Array.Copy(encryptionDictionary.OwnerBytes, ownerHash, ownerHash.Length); Array.Copy(encryptionDictionary.OwnerBytes, ownerHash.Length, validationSalt, 0, validationSalt.Length); byte[] result; if (encryptionDictionary.Revision == 6) { result = ComputeStupidIsoHash(truncatedPassword, validationSalt, encryptionDictionary.UserBytes); } else { result = ComputeSha256Hash(truncatedPassword, validationSalt, encryptionDictionary.UserBytes); } return(result.SequenceEqual(ownerHash)); }
private static bool IsUserPassword(byte[] passwordBytes, EncryptionDictionary encryptionDictionary, int length, byte[] documentIdBytes) { if (encryptionDictionary.Revision == 5 || encryptionDictionary.Revision == 6) { return(IsUserPasswordRevision5And6(passwordBytes, encryptionDictionary)); } // 1. Create an encryption key based on the user password string. var calculatedEncryptionKey = CalculateKeyRevisions2To4(passwordBytes, encryptionDictionary, length, documentIdBytes); byte[] output; if (encryptionDictionary.Revision >= 3) { using (var md5 = MD5.Create()) { // 2. Initialize the MD5 hash function and pass the 32-byte padding string. UpdateMd5(md5, PaddingBytes); // 3. Pass the first element of the file identifier array to the hash function and finish the hash. UpdateMd5(md5, documentIdBytes); md5.TransformFinalBlock(EmptyArray <byte> .Instance, 0, 0); var result = md5.Hash; // 4. Encrypt the 16-byte result of the hash, using an RC4 encryption function with the encryption key from step 1. var temp = RC4.Encrypt(calculatedEncryptionKey, result); // 5. Do the following 19 times: for (byte i = 1; i <= 19; i++) { // Take the output from the previous invocation of the RC4 function // and pass it as input to a new invocation of the function // Use an encryption key generated by taking each byte of the original encryption key (from step 1) // and performing an XOR operation between that byte and the single-byte value of the iteration counter. var key = calculatedEncryptionKey.Select(x => (byte)(x ^ i)).ToArray(); temp = RC4.Encrypt(key, temp); } output = temp; } } else { // 2. Encrypt the 32-byte padding string using an RC4 encryption function with the encryption key from the preceding step. output = RC4.Encrypt(calculatedEncryptionKey, PaddingBytes); } if (encryptionDictionary.Revision >= 3) { return(encryptionDictionary.UserBytes.Take(16).SequenceEqual(output.Take(16))); } return(encryptionDictionary.UserBytes.SequenceEqual(output)); }
private static byte[] CalculateEncryptionKey(byte[] password, EncryptionDictionary encryptionDictionary, int length, byte[] documentId, bool isUserPassword) { if (encryptionDictionary.Revision >= 2 && encryptionDictionary.Revision <= 4) { return(CalculateKeyRevisions2To4(password, encryptionDictionary, length, documentId)); } if (encryptionDictionary.Revision <= 6) { return(CalculateKeyRevisions5And6(password, encryptionDictionary, isUserPassword)); } throw new PdfDocumentEncryptedException($"PDF encrypted with unrecognized revision: {encryptionDictionary}."); }
private static byte[] CalculateKeyRevisions5And6(byte[] password, EncryptionDictionary encryptionDictionary, bool isUserPassword) { // Truncate the UTF-8 representation of the password to 127 bytes if it is longer than 127 bytes password = TruncatePasswordTo127Bytes(password); byte[] intermediateKey; byte[] encryptedFileKey; // If the password is the owner password: // Compute an intermediate owner key by computing the SHA-256 hash of the UTF-8 password concatenated with the 8 bytes of owner Key Salt, // concatenated with the 48-byte U string. The 32-byte result is the key used to decrypt the 32-byte OE string using AES-256 in CBC mode // with no padding and an initialization vector of zero. The 32-byte result is the file encryption key. if (!isUserPassword) { var ownerKeySalt = new byte[8]; Array.Copy(encryptionDictionary.OwnerBytes, 40, ownerKeySalt, 0, ownerKeySalt.Length); if (encryptionDictionary.Revision == 6) { intermediateKey = ComputeStupidIsoHash(password, ownerKeySalt, encryptionDictionary.UserBytes); } else { intermediateKey = ComputeSha256Hash(password, ownerKeySalt, encryptionDictionary.UserBytes); } encryptedFileKey = encryptionDictionary.OwnerEncryptionBytes; } else { // If the password is the user password: // Compute an intermediate user key by computing the SHA-256 hash of the UTF-8 password concatenated with the 8 bytes of user Key Salt. // The 32-byte result is the key used to decrypt the 32-byte UE string using AES-256 in CBC mode with no padding and an initialization vector of zero. // The 32-byte result is the file encryption key. var userKeySalt = new byte[8]; Array.Copy(encryptionDictionary.UserBytes, 40, userKeySalt, 0, 8); if (encryptionDictionary.Revision == 6) { intermediateKey = ComputeStupidIsoHash(password, userKeySalt, null); } else { intermediateKey = ComputeSha256Hash(password, userKeySalt); } encryptedFileKey = encryptionDictionary.UserEncryptionBytes; } var iv = new byte[16]; using (var rijndaelManaged = new RijndaelManaged { Key = intermediateKey, IV = iv, Mode = CipherMode.CBC, Padding = PaddingMode.None }) using (var memoryStream = new MemoryStream(encryptedFileKey)) using (var output = new MemoryStream()) using (var cryptoStream = new CryptoStream(memoryStream, rijndaelManaged.CreateDecryptor(intermediateKey, iv), CryptoStreamMode.Read)) { cryptoStream.CopyTo(output); var result = output.ToArray(); return(result); } }
private static byte[] CalculateKeyRevisions2To4(byte[] password, EncryptionDictionary encryptionDictionary, int length, byte[] documentId) { // 1. Pad or truncate the password string to exactly 32 bytes. var passwordFull = GetPaddedPassword(password); var revision = encryptionDictionary.Revision; using (var md5 = MD5.Create()) { // 2. Initialize the MD5 hash function and pass the result of step 1 as input to this function. UpdateMd5(md5, passwordFull); // 3. Pass the value of the encryption dictionary's owner key entry to the MD5 hash function. UpdateMd5(md5, encryptionDictionary.OwnerBytes); // 4. Treat the value of the P entry as an unsigned 4-byte integer. var unsigned = (uint)encryptionDictionary.UserAccessPermissions; // 4. Pass these bytes to the MD5 hash function, low-order byte first. UpdateMd5(md5, new[] { (byte)(unsigned) }); UpdateMd5(md5, new[] { (byte)(unsigned >> 8) }); UpdateMd5(md5, new[] { (byte)(unsigned >> 16) }); UpdateMd5(md5, new[] { (byte)(unsigned >> 24) }); // 5. Pass the first element of the file's file identifier array to the hash. UpdateMd5(md5, documentId); // 6. (Revision 4 or greater) If document metadata is not being encrypted, pass 4 bytes // with the value 0xFFFFFFFF to the MD5 hash function. if (revision >= 4 && !encryptionDictionary.EncryptMetadata) { UpdateMd5(md5, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }); } // 7. Do the following 50 times: Take the output from the previous MD5 hash and // pass the first n bytes of the output as input into a new MD5 hash, // where n is the number of bytes of the encryption key as defined by the value // of the encryption dictionary's Length entry. if (revision == 3 || revision == 4) { var n = length; md5.TransformFinalBlock(EmptyArray <byte> .Instance, 0, 0); var input = md5.Hash; using (var newMd5 = MD5.Create()) { for (var i = 0; i < 50; i++) { input = newMd5.ComputeHash(input.Take(n).ToArray()); } } var result = new byte[length]; Array.Copy(input, result, length); return(result); } else { md5.TransformFinalBlock(EmptyArray <byte> .Instance, 0, 0); var result = new byte[length]; Array.Copy(md5.Hash, result, length); return(result); } } }
public EncryptionHandler(EncryptionDictionary encryptionDictionary, TrailerDictionary trailerDictionary, IReadOnlyList <string> passwords) { this.encryptionDictionary = encryptionDictionary; passwords = passwords ?? new[] { string.Empty }; if (!passwords.Contains(string.Empty)) { passwords = new List <string>(passwords) { string.Empty }; } byte[] documentIdBytes; if (trailerDictionary.Identifier != null && trailerDictionary.Identifier.Count == 2) { var token = trailerDictionary.Identifier[0]; switch (token) { case HexToken hex: documentIdBytes = hex.Bytes.ToArray(); break; default: documentIdBytes = OtherEncodings.StringAsLatin1Bytes(token.Data); break; } } else { documentIdBytes = EmptyArray <byte> .Instance; } if (encryptionDictionary == null) { return; } useAes = false; if (encryptionDictionary.EncryptionAlgorithmCode == EncryptionAlgorithmCode.SecurityHandlerInDocument || encryptionDictionary.EncryptionAlgorithmCode == EncryptionAlgorithmCode.SecurityHandlerInDocument256) { if (!encryptionDictionary.TryGetCryptHandler(out var cryptHandlerLocal)) { throw new PdfDocumentEncryptedException("Document encrypted with security handler in document but no crypt dictionary found.", encryptionDictionary); } cryptHandler = cryptHandlerLocal; useAes = cryptHandlerLocal?.StreamDictionary?.Name == CryptDictionary.Method.AesV2 || cryptHandlerLocal?.StreamDictionary?.Name == CryptDictionary.Method.AesV3; } var charset = OtherEncodings.Iso88591; if (encryptionDictionary.Revision == 5 || encryptionDictionary.Revision == 6) { // ReSharper disable once RedundantAssignment charset = Encoding.UTF8; } var length = encryptionDictionary.EncryptionAlgorithmCode == EncryptionAlgorithmCode.Rc4OrAes40BitKey ? 5 : encryptionDictionary.KeyLength.GetValueOrDefault() / 8; var foundPassword = false; foreach (var password in passwords) { var passwordBytes = charset.GetBytes(password); var isUserPassword = false; byte[] decryptionPasswordBytes; if (IsOwnerPassword(passwordBytes, encryptionDictionary, length, documentIdBytes, out var userPassBytes)) { if (encryptionDictionary.Revision == 5 || encryptionDictionary.Revision == 6) { decryptionPasswordBytes = passwordBytes; } else { decryptionPasswordBytes = userPassBytes; } } else if (IsUserPassword(passwordBytes, encryptionDictionary, length, documentIdBytes)) { decryptionPasswordBytes = passwordBytes; isUserPassword = true; } else { continue; } encryptionKey = CalculateEncryptionKey(decryptionPasswordBytes, encryptionDictionary, length, documentIdBytes, isUserPassword); foundPassword = true; break; } if (!foundPassword) { throw new PdfDocumentEncryptedException("The document was encrypted and none of the provided passwords were the user or owner password.", encryptionDictionary); } }
private static bool IsOwnerPassword(byte[] passwordBytes, EncryptionDictionary encryptionDictionary, int length, byte[] documentIdBytes, out byte[] userPassword) { userPassword = null; if (encryptionDictionary.Revision == 5 || encryptionDictionary.Revision == 6) { return(IsOwnerPasswordRevision5And6(passwordBytes, encryptionDictionary)); } // 1. Pad or truncate the owner password string, if there is no owner password use the user password instead. var paddedPassword = GetPaddedPassword(passwordBytes); using (var md5 = MD5.Create()) { // 2. Initialize the MD5 hash function and pass the result of step 1 as input. var hash = md5.ComputeHash(paddedPassword); // 3. (Revision 3 or greater) Do the following 50 times: if (encryptionDictionary.Revision >= 3) { // Take the output from the previous MD5 hash and pass it as input into a new MD5 hash. for (var i = 0; i < 50; i++) { hash = md5.ComputeHash(md5.Hash); } } // 4. Create an RC4 encryption key using the first n bytes of the output from the final MD5 hash, // where n is always 5 for revision 2 but for revision 3 or greater depends on the value of the encryption dictionary's Length entry. var key = hash.Take(length).ToArray(); if (encryptionDictionary.Revision == 2) { // 5. (Revision 2 only) Decrypt the value of the encryption dictionary's owner entry, // using an RC4 encryption function with the encryption key computed in step 1 - 4. userPassword = RC4.Encrypt(key, encryptionDictionary.OwnerBytes); } else { // 5. (Revision 3 or greater) Do the following 20 times: byte[] output = null; for (var i = 0; i < 20; i++) { // Generate a per iteration key by taking the original key and performing an XOR operation between each // byte of the key and the single-byte value of the iteration counter (from 19 to 0). var keyIter = key.Select(x => (byte)(x ^ (19 - i))).ToArray(); if (i == 0) { output = encryptionDictionary.OwnerBytes; } // Decrypt the value of the encryption dictionary's owner entry (first iteration) // or the output from the previous iteration using an RC4 encryption function. output = RC4.Encrypt(keyIter, output); } userPassword = output; } // 6. The result of step 5 purports to be the user password. // Authenticate this user password, if it is correct, the password supplied is the owner password. var result = IsUserPassword(userPassword, encryptionDictionary, length, documentIdBytes); return(result); } }
public EncryptionHandler(EncryptionDictionary encryptionDictionary, TrailerDictionary trailerDictionary, string password) { this.encryptionDictionary = encryptionDictionary; var documentIdBytes = trailerDictionary.Identifier != null && trailerDictionary.Identifier.Count == 2 ? OtherEncodings.StringAsLatin1Bytes(trailerDictionary.Identifier[0]) : EmptyArray <byte> .Instance; password = password ?? string.Empty; if (encryptionDictionary == null) { return; } useAes = false; if (encryptionDictionary.EncryptionAlgorithmCode == EncryptionAlgorithmCode.SecurityHandlerInDocument || encryptionDictionary.EncryptionAlgorithmCode == EncryptionAlgorithmCode.SecurityHandlerInDocument256) { if (!encryptionDictionary.TryGetCryptHandler(out var cryptHandlerLocal)) { throw new PdfDocumentEncryptedException("Document encrypted with security handler in document but no crypt dictionary found.", encryptionDictionary); } cryptHandler = cryptHandlerLocal; useAes = cryptHandlerLocal?.StreamDictionary?.Name == CryptDictionary.Method.AesV2 || cryptHandlerLocal?.StreamDictionary?.Name == CryptDictionary.Method.AesV3; } var charset = OtherEncodings.Iso88591; if (encryptionDictionary.Revision == 5 || encryptionDictionary.Revision == 6) { // ReSharper disable once RedundantAssignment charset = Encoding.UTF8; } var passwordBytes = charset.GetBytes(password); byte[] decryptionPasswordBytes; var length = encryptionDictionary.EncryptionAlgorithmCode == EncryptionAlgorithmCode.Rc4OrAes40BitKey ? 5 : encryptionDictionary.KeyLength.GetValueOrDefault() / 8; var isUserPassword = false; if (IsOwnerPassword(passwordBytes, encryptionDictionary, length, documentIdBytes, out var userPassBytes)) { if (encryptionDictionary.Revision == 5 || encryptionDictionary.Revision == 6) { decryptionPasswordBytes = passwordBytes; } else { decryptionPasswordBytes = userPassBytes; } } else if (IsUserPassword(passwordBytes, encryptionDictionary, length, documentIdBytes)) { decryptionPasswordBytes = passwordBytes; isUserPassword = true; } else { throw new PdfDocumentEncryptedException("The document was encrypted and the provided password was neither the user or owner password.", encryptionDictionary); } encryptionKey = CalculateEncryptionKey(decryptionPasswordBytes, encryptionDictionary, length, documentIdBytes, isUserPassword); }