mirror of
https://github.com/UglyToad/PdfPig.git
synced 2025-09-22 20:13:58 +08:00
start adding support for revision 5 aes-256 encrypted documents #34
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
using Exceptions;
|
using Exceptions;
|
||||||
using Tokens;
|
using Tokens;
|
||||||
using Util;
|
using Util;
|
||||||
|
using Util.JetBrains.Annotations;
|
||||||
|
|
||||||
internal class EncryptionDictionary
|
internal class EncryptionDictionary
|
||||||
{
|
{
|
||||||
@@ -13,7 +14,7 @@
|
|||||||
|
|
||||||
public int? KeyLength { get; }
|
public int? KeyLength { get; }
|
||||||
|
|
||||||
public int StandardSecurityHandlerRevision { get; }
|
public int Revision { get; }
|
||||||
|
|
||||||
public string OwnerPasswordCheck { get; }
|
public string OwnerPasswordCheck { get; }
|
||||||
|
|
||||||
@@ -23,6 +24,18 @@
|
|||||||
|
|
||||||
public byte[] UserBytes { get; }
|
public byte[] UserBytes { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Required if <see cref="Revision"/> is 5 or above. A 32-byte string, based on the owner and user passwords that is used in computing the encryption key.
|
||||||
|
/// </summary>
|
||||||
|
[CanBeNull]
|
||||||
|
public byte[] OwnerEncryptionBytes { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Required if <see cref="Revision"/> is 5 or above. A 32-byte string, based on the user password that is used in computing the encryption key.
|
||||||
|
/// </summary>
|
||||||
|
[CanBeNull]
|
||||||
|
public byte[] UserEncryptionBytes { get; }
|
||||||
|
|
||||||
public UserAccessPermissions UserAccessPermissions { get; }
|
public UserAccessPermissions UserAccessPermissions { get; }
|
||||||
|
|
||||||
public bool IsStandardFilter => string.Equals(Filter, "Standard", StringComparison.OrdinalIgnoreCase);
|
public bool IsStandardFilter => string.Equals(Filter, "Standard", StringComparison.OrdinalIgnoreCase);
|
||||||
@@ -33,9 +46,11 @@
|
|||||||
|
|
||||||
public EncryptionDictionary(string filter, EncryptionAlgorithmCode encryptionAlgorithmCode,
|
public EncryptionDictionary(string filter, EncryptionAlgorithmCode encryptionAlgorithmCode,
|
||||||
int? keyLength,
|
int? keyLength,
|
||||||
int standardSecurityHandlerRevision,
|
int revision,
|
||||||
string ownerPasswordCheck,
|
string ownerPasswordCheck,
|
||||||
string userPasswordCheck,
|
string userPasswordCheck,
|
||||||
|
byte[] ownerEncryptionBytes,
|
||||||
|
byte[] userEncryptionBytes,
|
||||||
UserAccessPermissions userAccessPermissions,
|
UserAccessPermissions userAccessPermissions,
|
||||||
DictionaryToken dictionary,
|
DictionaryToken dictionary,
|
||||||
bool encryptMetadata)
|
bool encryptMetadata)
|
||||||
@@ -43,9 +58,11 @@
|
|||||||
Filter = filter;
|
Filter = filter;
|
||||||
EncryptionAlgorithmCode = encryptionAlgorithmCode;
|
EncryptionAlgorithmCode = encryptionAlgorithmCode;
|
||||||
KeyLength = keyLength;
|
KeyLength = keyLength;
|
||||||
StandardSecurityHandlerRevision = standardSecurityHandlerRevision;
|
Revision = revision;
|
||||||
OwnerPasswordCheck = ownerPasswordCheck;
|
OwnerPasswordCheck = ownerPasswordCheck;
|
||||||
UserPasswordCheck = userPasswordCheck;
|
UserPasswordCheck = userPasswordCheck;
|
||||||
|
OwnerEncryptionBytes = ownerEncryptionBytes;
|
||||||
|
UserEncryptionBytes = userEncryptionBytes;
|
||||||
UserAccessPermissions = userAccessPermissions;
|
UserAccessPermissions = userAccessPermissions;
|
||||||
Dictionary = dictionary;
|
Dictionary = dictionary;
|
||||||
EncryptMetadata = encryptMetadata;
|
EncryptMetadata = encryptMetadata;
|
||||||
|
@@ -46,9 +46,23 @@
|
|||||||
access = (UserAccessPermissions) accessToken.Int;
|
access = (UserAccessPermissions) accessToken.Int;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
byte[] userEncryptionBytes = null, ownerEncryptionBytes = null;
|
||||||
|
if (revision >= 5)
|
||||||
|
{
|
||||||
|
var oe = encryptionDictionary.Get<StringToken>(NameToken.Oe, tokenScanner);
|
||||||
|
var ue = encryptionDictionary.Get<StringToken>(NameToken.Ue, tokenScanner);
|
||||||
|
|
||||||
|
ownerEncryptionBytes = OtherEncodings.StringAsLatin1Bytes(oe.Data);
|
||||||
|
userEncryptionBytes = OtherEncodings.StringAsLatin1Bytes(ue.Data);
|
||||||
|
}
|
||||||
|
|
||||||
encryptionDictionary.TryGetOptionalTokenDirect(NameToken.EncryptMetaData, tokenScanner, out BooleanToken encryptMetadata);
|
encryptionDictionary.TryGetOptionalTokenDirect(NameToken.EncryptMetaData, tokenScanner, out BooleanToken encryptMetadata);
|
||||||
|
|
||||||
return new EncryptionDictionary(filter.Data, code, length, revision, ownerString, userString, access, encryptionDictionary,
|
return new EncryptionDictionary(filter.Data, code, length, revision, ownerString, userString,
|
||||||
|
ownerEncryptionBytes,
|
||||||
|
userEncryptionBytes,
|
||||||
|
access,
|
||||||
|
encryptionDictionary,
|
||||||
encryptMetadata?.Data ?? true);
|
encryptMetadata?.Data ?? true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
{
|
{
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -68,13 +69,10 @@
|
|||||||
|
|
||||||
var charset = OtherEncodings.Iso88591;
|
var charset = OtherEncodings.Iso88591;
|
||||||
|
|
||||||
if (encryptionDictionary.StandardSecurityHandlerRevision == 5 || encryptionDictionary.StandardSecurityHandlerRevision == 6)
|
if (encryptionDictionary.Revision == 5 || encryptionDictionary.Revision == 6)
|
||||||
{
|
{
|
||||||
// ReSharper disable once RedundantAssignment
|
// ReSharper disable once RedundantAssignment
|
||||||
charset = Encoding.UTF8;
|
charset = Encoding.UTF8;
|
||||||
|
|
||||||
throw new PdfDocumentEncryptedException($"Revision of {encryptionDictionary.StandardSecurityHandlerRevision} not supported, please raise an issue.",
|
|
||||||
encryptionDictionary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var passwordBytes = charset.GetBytes(password);
|
var passwordBytes = charset.GetBytes(password);
|
||||||
@@ -85,13 +83,15 @@
|
|||||||
? 5
|
? 5
|
||||||
: encryptionDictionary.KeyLength.GetValueOrDefault() / 8;
|
: encryptionDictionary.KeyLength.GetValueOrDefault() / 8;
|
||||||
|
|
||||||
|
var isUserPassword = false;
|
||||||
if (IsUserPassword(passwordBytes, encryptionDictionary, length, documentIdBytes))
|
if (IsUserPassword(passwordBytes, encryptionDictionary, length, documentIdBytes))
|
||||||
{
|
{
|
||||||
decryptionPasswordBytes = passwordBytes;
|
decryptionPasswordBytes = passwordBytes;
|
||||||
|
isUserPassword = true;
|
||||||
}
|
}
|
||||||
else if (IsOwnerPassword(passwordBytes, encryptionDictionary, length, documentIdBytes, out var userPassBytes))
|
else if (IsOwnerPassword(passwordBytes, encryptionDictionary, length, documentIdBytes, out var userPassBytes))
|
||||||
{
|
{
|
||||||
if (encryptionDictionary.StandardSecurityHandlerRevision == 5 || encryptionDictionary.StandardSecurityHandlerRevision == 6)
|
if (encryptionDictionary.Revision == 5 || encryptionDictionary.Revision == 6)
|
||||||
{
|
{
|
||||||
decryptionPasswordBytes = passwordBytes;
|
decryptionPasswordBytes = passwordBytes;
|
||||||
}
|
}
|
||||||
@@ -105,19 +105,25 @@
|
|||||||
throw new PdfDocumentEncryptedException("The document was encrypted and the provided password was neither the user or owner password.", encryptionDictionary);
|
throw new PdfDocumentEncryptedException("The document was encrypted and the provided password was neither the user or owner password.", encryptionDictionary);
|
||||||
}
|
}
|
||||||
|
|
||||||
encryptionKey = CalculateKeyRevisions2To4(decryptionPasswordBytes, encryptionDictionary,
|
encryptionKey = CalculateEncryptionKey(decryptionPasswordBytes, encryptionDictionary,
|
||||||
length,
|
length,
|
||||||
documentIdBytes);
|
documentIdBytes,
|
||||||
|
isUserPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsUserPassword(byte[] passwordBytes, EncryptionDictionary encryptionDictionary, int length, byte[] documentIdBytes)
|
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.
|
// 1. Create an encryption key based on the user password string.
|
||||||
var calculatedEncryptionKey = CalculateKeyRevisions2To4(passwordBytes, encryptionDictionary, length, documentIdBytes);
|
var calculatedEncryptionKey = CalculateKeyRevisions2To4(passwordBytes, encryptionDictionary, length, documentIdBytes);
|
||||||
|
|
||||||
byte[] output;
|
byte[] output;
|
||||||
|
|
||||||
if (encryptionDictionary.StandardSecurityHandlerRevision >= 3)
|
if (encryptionDictionary.Revision >= 3)
|
||||||
{
|
{
|
||||||
using (var md5 = MD5.Create())
|
using (var md5 = MD5.Create())
|
||||||
{
|
{
|
||||||
@@ -154,7 +160,7 @@
|
|||||||
output = RC4.Encrypt(calculatedEncryptionKey, PaddingBytes);
|
output = RC4.Encrypt(calculatedEncryptionKey, PaddingBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (encryptionDictionary.StandardSecurityHandlerRevision >= 3)
|
if (encryptionDictionary.Revision >= 3)
|
||||||
{
|
{
|
||||||
return encryptionDictionary.UserBytes.Take(16).SequenceEqual(output.Take(16));
|
return encryptionDictionary.UserBytes.Take(16).SequenceEqual(output.Take(16));
|
||||||
}
|
}
|
||||||
@@ -162,11 +168,38 @@
|
|||||||
return encryptionDictionary.UserBytes.SequenceEqual(output);
|
return encryptionDictionary.UserBytes.SequenceEqual(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsUserPasswordRevision5And6(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 User Validation Salt.
|
||||||
|
// If the 32-byte result matches the first 32 bytes of the U string, this is the user password
|
||||||
|
var truncatedPassword = TruncatePasswordTo127Bytes(passwordBytes);
|
||||||
|
|
||||||
|
// The 48-byte string consisting of the 32-byte hash followed by the User Validation Salt followed by the User Key Salt is stored as the U key
|
||||||
|
var userPasswordHash = new byte[32];
|
||||||
|
var userValidationSalt = new byte[8];
|
||||||
|
Array.Copy(encryptionDictionary.UserBytes, userPasswordHash, 32);
|
||||||
|
Array.Copy(encryptionDictionary.UserBytes, 32, userValidationSalt, 0, 8);
|
||||||
|
|
||||||
|
if (encryptionDictionary.Revision == 6)
|
||||||
|
{
|
||||||
|
throw new PdfDocumentEncryptedException($"Support for revision 6 encryption not implemented: {encryptionDictionary}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = ComputeSha256Hash(truncatedPassword, userValidationSalt);
|
||||||
|
|
||||||
|
return result.SequenceEqual(userPasswordHash);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsOwnerPassword(byte[] passwordBytes, EncryptionDictionary encryptionDictionary, int length, byte[] documentIdBytes,
|
private static bool IsOwnerPassword(byte[] passwordBytes, EncryptionDictionary encryptionDictionary, int length, byte[] documentIdBytes,
|
||||||
out byte[] userPassword)
|
out byte[] userPassword)
|
||||||
{
|
{
|
||||||
userPassword = null;
|
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.
|
// 1. Pad or truncate the owner password string, if there is no owner password use the user password instead.
|
||||||
var paddedPassword = GetPaddedPassword(passwordBytes);
|
var paddedPassword = GetPaddedPassword(passwordBytes);
|
||||||
|
|
||||||
@@ -176,7 +209,7 @@
|
|||||||
var hash = md5.ComputeHash(paddedPassword);
|
var hash = md5.ComputeHash(paddedPassword);
|
||||||
|
|
||||||
// 3. (Revision 3 or greater) Do the following 50 times:
|
// 3. (Revision 3 or greater) Do the following 50 times:
|
||||||
if (encryptionDictionary.StandardSecurityHandlerRevision >= 3)
|
if (encryptionDictionary.Revision >= 3)
|
||||||
{
|
{
|
||||||
// Take the output from the previous MD5 hash and pass it as input into a new MD5 hash.
|
// 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++)
|
for (var i = 0; i < 50; i++)
|
||||||
@@ -189,7 +222,7 @@
|
|||||||
// 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.
|
// 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();
|
var key = hash.Take(length).ToArray();
|
||||||
|
|
||||||
if (encryptionDictionary.StandardSecurityHandlerRevision == 2)
|
if (encryptionDictionary.Revision == 2)
|
||||||
{
|
{
|
||||||
// 5. (Revision 2 only) Decrypt the value of the encryption dictionary's owner entry,
|
// 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.
|
// using an RC4 encryption function with the encryption key computed in step 1 - 4.
|
||||||
@@ -226,6 +259,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (encryptionDictionary.Revision == 6)
|
||||||
|
{
|
||||||
|
throw new PdfDocumentEncryptedException($"Support for revision 6 encryption not implemented: {encryptionDictionary}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = ComputeSha256Hash(truncatedPassword, validationSalt);
|
||||||
|
|
||||||
|
return result.SequenceEqual(ownerHash);
|
||||||
|
}
|
||||||
|
|
||||||
public IToken Decrypt(IndirectReference reference, IToken token)
|
public IToken Decrypt(IndirectReference reference, IToken token)
|
||||||
{
|
{
|
||||||
if (token == null)
|
if (token == null)
|
||||||
@@ -408,12 +464,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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[] CalculateKeyRevisions2To4(byte[] password, EncryptionDictionary encryptionDictionary, int length, byte[] documentId)
|
private static byte[] CalculateKeyRevisions2To4(byte[] password, EncryptionDictionary encryptionDictionary, int length, byte[] documentId)
|
||||||
{
|
{
|
||||||
// 1. Pad or truncate the password string to exactly 32 bytes.
|
// 1. Pad or truncate the password string to exactly 32 bytes.
|
||||||
var passwordFull = GetPaddedPassword(password);
|
var passwordFull = GetPaddedPassword(password);
|
||||||
|
|
||||||
var revision = encryptionDictionary.StandardSecurityHandlerRevision;
|
var revision = encryptionDictionary.Revision;
|
||||||
|
|
||||||
using (var md5 = MD5.Create())
|
using (var md5 = MD5.Create())
|
||||||
{
|
{
|
||||||
@@ -480,6 +551,63 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
throw new PdfDocumentEncryptedException($"Unsupported owner key encryption with revision: {encryptionDictionary.Revision}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
var intermediateKey = ComputeSha256Hash(password, userKeySalt);
|
||||||
|
|
||||||
|
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(encryptionDictionary.UserEncryptionBytes))
|
||||||
|
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[] ComputeSha256Hash(byte[] input1, byte[] input2, byte[] input3 = null)
|
||||||
|
{
|
||||||
|
using (var sha = SHA256.Create())
|
||||||
|
{
|
||||||
|
sha.TransformBlock(input1, 0, input1.Length, null, 0);
|
||||||
|
sha.TransformBlock(input2, 0, input2.Length, null, 0);
|
||||||
|
|
||||||
|
if (input3 != null)
|
||||||
|
{
|
||||||
|
sha.TransformFinalBlock(input3, 0, input3.Length);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sha.TransformFinalBlock(EmptyArray<byte>.Instance, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sha.Hash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void UpdateMd5(MD5 md5, byte[] data)
|
private static void UpdateMd5(MD5 md5, byte[] data)
|
||||||
{
|
{
|
||||||
md5.TransformBlock(data, 0, data.Length, null, 0);
|
md5.TransformBlock(data, 0, data.Length, null, 0);
|
||||||
@@ -507,5 +635,17 @@
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static byte[] TruncatePasswordTo127Bytes(byte[] password)
|
||||||
|
{
|
||||||
|
if (password.Length <= 127)
|
||||||
|
{
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new byte[127];
|
||||||
|
Array.Copy(password, result, 127);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user