mirror of
https://github.com/Thealexbarney/LibHac.git
synced 2024-11-14 10:49:41 +01:00
Merge pull request #164 from Xpl0itR/misc
Misc changes to StorageExtensions and CryptoOld - Changes to StorageExtensions - Use LibHac's RentedArray<T> struct - Expose bufferSize as a variable in CopyTo and CopyToStream - Changes to CryptoOld - Remove AES functions and replace references to said functions with functions from the Crypto.Aes class - Edit and move the AES-CMAC function into the Crypto.Aes class - Move BigInteger functions to the Utilities class - Reduce duplication between the RSA functions - Edit and move the Rsa parameter recovery function into the Crypto.Rsa class
This commit is contained in:
commit
5f755bc76f
15 changed files with 366 additions and 410 deletions
|
@ -1,6 +1,6 @@
|
|||
// ReSharper disable AssignmentIsFullyDiscarded
|
||||
using System;
|
||||
|
||||
using LibHac.Diag;
|
||||
#if HAS_INTRINSICS
|
||||
using LibHac.Crypto.Detail;
|
||||
|
||||
|
@ -265,5 +265,73 @@ namespace LibHac.Crypto
|
|||
|
||||
cipher.Transform(input, output);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the CMAC of the provided data using AES-128.
|
||||
/// </summary>
|
||||
/// <param name="mac">The buffer where the generated MAC will be placed. Must be at least 16 bytes long.</param>
|
||||
/// <param name="data">The message on which the MAC will be calculated.</param>
|
||||
/// <param name="key">The 128-bit AES key used to calculate the MAC.</param>
|
||||
/// <remarks>https://tools.ietf.org/html/rfc4493</remarks>
|
||||
public static void CalculateCmac(Span<byte> mac, ReadOnlySpan<byte> data, ReadOnlySpan<byte> key)
|
||||
{
|
||||
ReadOnlySpan<byte> zero = stackalloc byte[16];
|
||||
int len = data.Length;
|
||||
|
||||
// Step 1, AES-128 with key K is applied to an all-zero input block.
|
||||
Span<byte> l = stackalloc byte[16];
|
||||
EncryptCbc128(zero, l, key, zero);
|
||||
|
||||
// Step 2, K1 is derived through the following operation:
|
||||
Span<byte> k1 = stackalloc byte[16];
|
||||
LeftShiftBytes(l, k1);
|
||||
if ((l[0] & 0x80) == 0x80) // If the most significant bit of L is equal to 0, K1 is the left-shift of L by 1 bit.
|
||||
k1[15] ^= 0x87; // Otherwise, K1 is the XOR of const_Rb and the left-shift of L by 1 bit.
|
||||
|
||||
// Step 3, K2 is derived through the following operation:
|
||||
Span<byte> k2 = stackalloc byte[16];
|
||||
LeftShiftBytes(k1, k2);
|
||||
if ((k1[0] & 0x80) == 0x80) // If the most significant bit of K1 is equal to 0, K2 is the left-shift of K1 by 1 bit.
|
||||
k2[15] ^= 0x87; // Otherwise, K2 is the XOR of const_Rb and the left-shift of K1 by 1 bit.
|
||||
|
||||
// ReSharper disable once RedundantAssignment
|
||||
Span<byte> paddedMessage = l;
|
||||
|
||||
if (len != 0 && len % 16 == 0) // If the size of the input message block is equal to a positive multiple of the block size (namely, 128 bits),
|
||||
{ // the last block shall be XOR'ed with K1 before processing
|
||||
paddedMessage = len < 0x800 ? stackalloc byte[len] : new byte[len];
|
||||
data.CopyTo(paddedMessage);
|
||||
|
||||
for (int j = 0; j < k1.Length; j++)
|
||||
paddedMessage[paddedMessage.Length - 16 + j] ^= k1[j];
|
||||
}
|
||||
else // Otherwise, the last block shall be padded with 10^i and XOR'ed with K2.
|
||||
{
|
||||
int paddedLength = len + (16 - len % 16);
|
||||
paddedMessage = paddedLength < 0x800 ? stackalloc byte[paddedLength] : new byte[paddedLength];
|
||||
paddedMessage[len] = 0x80;
|
||||
data.CopyTo(paddedMessage);
|
||||
|
||||
for (int j = 0; j < k2.Length; j++)
|
||||
paddedMessage[paddedMessage.Length - 16 + j] ^= k2[j];
|
||||
}
|
||||
|
||||
EncryptCbc128(paddedMessage, paddedMessage, key, zero); // The result of the previous process will be the input of the last encryption.
|
||||
paddedMessage.Slice(paddedMessage.Length - 0x10).CopyTo(mac);
|
||||
}
|
||||
|
||||
private static void LeftShiftBytes(ReadOnlySpan<byte> input, Span<byte> output)
|
||||
{
|
||||
Assert.AssertTrue(output.Length >= input.Length);
|
||||
|
||||
byte carry = 0;
|
||||
|
||||
for (int i = input.Length - 1; i >= 0; i--)
|
||||
{
|
||||
ushort b = (ushort)(input[i] << 1);
|
||||
output[i] = (byte)((b & 0xff) + carry);
|
||||
carry = (byte)((b & 0xff00) >> 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace LibHac.Crypto
|
||||
|
@ -6,7 +7,15 @@ namespace LibHac.Crypto
|
|||
public static class Rsa
|
||||
{
|
||||
public static bool VerifyRsa2048PssSha256(ReadOnlySpan<byte> signature, ReadOnlySpan<byte> modulus,
|
||||
ReadOnlySpan<byte> exponent, ReadOnlySpan<byte> message)
|
||||
ReadOnlySpan<byte> exponent, ReadOnlySpan<byte> message) =>
|
||||
VerifyRsa2048Sha256(signature, modulus, exponent, message, RSASignaturePadding.Pss);
|
||||
|
||||
public static bool VerifyRsa2048Pkcs1Sha256(ReadOnlySpan<byte> signature, ReadOnlySpan<byte> modulus,
|
||||
ReadOnlySpan<byte> exponent, ReadOnlySpan<byte> message) =>
|
||||
VerifyRsa2048Sha256(signature, modulus, exponent, message, RSASignaturePadding.Pkcs1);
|
||||
|
||||
private static bool VerifyRsa2048Sha256(ReadOnlySpan<byte> signature, ReadOnlySpan<byte> modulus,
|
||||
ReadOnlySpan<byte> exponent, ReadOnlySpan<byte> message, RSASignaturePadding padding)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -14,7 +23,7 @@ namespace LibHac.Crypto
|
|||
|
||||
using (var rsa = RSA.Create(param))
|
||||
{
|
||||
return rsa.VerifyData(message, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
|
||||
return rsa.VerifyData(message, signature, HashAlgorithmName.SHA256, padding);
|
||||
}
|
||||
}
|
||||
catch (CryptographicException)
|
||||
|
@ -40,5 +49,129 @@ namespace LibHac.Crypto
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <param name="n">The RSA Modulus (n)</param>
|
||||
/// <param name="e">The RSA Public Exponent (e)</param>
|
||||
/// <param name="d">The RSA Private Exponent (d)</param>
|
||||
public static RSAParameters RecoverParameters(BigInteger n, BigInteger e, BigInteger d)
|
||||
{
|
||||
(BigInteger p, BigInteger q) = DeriveRsaPrimeNumberPair(n, e, d);
|
||||
|
||||
BigInteger dp = d % (p - BigInteger.One);
|
||||
BigInteger dq = d % (q - BigInteger.One);
|
||||
BigInteger inverseQ = Utilities.ModInverse(q, p);
|
||||
|
||||
int modLen = n.ToByteArray().Length;
|
||||
int halfModLen = (modLen + 1) / 2;
|
||||
|
||||
return new RSAParameters
|
||||
{
|
||||
Modulus = n.GetBytes(modLen),
|
||||
Exponent = e.GetBytes(-1),
|
||||
D = d.GetBytes(modLen),
|
||||
P = p.GetBytes(halfModLen),
|
||||
Q = q.GetBytes(halfModLen),
|
||||
DP = dp.GetBytes(halfModLen),
|
||||
DQ = dq.GetBytes(halfModLen),
|
||||
InverseQ = inverseQ.GetBytes(halfModLen)
|
||||
};
|
||||
}
|
||||
|
||||
/// <param name="n">The RSA Modulus (n)</param>
|
||||
/// <param name="e">The RSA Public Exponent (e)</param>
|
||||
/// <param name="d">The RSA Private Exponent (d)</param>
|
||||
public static RSAParameters RecoverParameters(ReadOnlySpan<byte> n, ReadOnlySpan<byte> e, ReadOnlySpan<byte> d) =>
|
||||
RecoverParameters(n.GetBigInteger(), e.GetBigInteger(), d.GetBigInteger());
|
||||
|
||||
/// <summary>
|
||||
/// Derive RSA Prime Number Pair (p, q) from RSA Modulus (n), RSA Public Exponent (e) and RSA Private Exponent (d)
|
||||
/// </summary>
|
||||
/// <param name="n">The RSA Modulus (n)</param>
|
||||
/// <param name="e">The RSA Public Exponent (e)</param>
|
||||
/// <param name="d">The RSA Private Exponent (d)</param>
|
||||
/// <returns>RSA Prime Number Pair</returns>
|
||||
private static (BigInteger p, BigInteger q) DeriveRsaPrimeNumberPair(BigInteger n, BigInteger e, BigInteger d)
|
||||
{
|
||||
BigInteger k = d * e - BigInteger.One;
|
||||
|
||||
if (!k.IsEven)
|
||||
{
|
||||
throw new InvalidOperationException("d*e - 1 is odd");
|
||||
}
|
||||
|
||||
BigInteger two = BigInteger.One + BigInteger.One;
|
||||
BigInteger t = BigInteger.One;
|
||||
|
||||
BigInteger r = k / two;
|
||||
|
||||
while (r.IsEven)
|
||||
{
|
||||
t++;
|
||||
r /= two;
|
||||
}
|
||||
|
||||
byte[] rndBuf = n.ToByteArray();
|
||||
|
||||
if (rndBuf[^1] == 0)
|
||||
{
|
||||
rndBuf = new byte[rndBuf.Length - 1];
|
||||
}
|
||||
|
||||
BigInteger nMinusOne = n - BigInteger.One;
|
||||
|
||||
bool cracked = false;
|
||||
BigInteger y = BigInteger.Zero;
|
||||
|
||||
using (var rng = RandomNumberGenerator.Create())
|
||||
{
|
||||
for (int i = 0; i < 100 && !cracked; i++)
|
||||
{
|
||||
BigInteger g;
|
||||
|
||||
do
|
||||
{
|
||||
rng.GetBytes(rndBuf);
|
||||
g = Utilities.GetBigInteger(rndBuf);
|
||||
}
|
||||
while (g >= n);
|
||||
|
||||
y = BigInteger.ModPow(g, r, n);
|
||||
|
||||
if (y.IsOne || y == nMinusOne)
|
||||
{
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (BigInteger j = BigInteger.One; j < t; j++)
|
||||
{
|
||||
BigInteger x = BigInteger.ModPow(y, two, n);
|
||||
|
||||
if (x.IsOne)
|
||||
{
|
||||
cracked = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (x == nMinusOne)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
y = x;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!cracked)
|
||||
{
|
||||
throw new InvalidOperationException("Prime factors not found");
|
||||
}
|
||||
|
||||
BigInteger p = BigInteger.GreatestCommonDivisor(y - BigInteger.One, n);
|
||||
BigInteger q = n / p;
|
||||
|
||||
return (p, q);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,103 +1,33 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using System.Security.Cryptography;
|
||||
using LibHac.Crypto;
|
||||
using LibHac.FsSystem;
|
||||
|
||||
using Aes = LibHac.Crypto.Aes;
|
||||
|
||||
namespace LibHac
|
||||
{
|
||||
public static class CryptoOld
|
||||
{
|
||||
internal const int Aes128Size = 0x10;
|
||||
|
||||
public static void DecryptEcb(byte[] key, byte[] src, int srcIndex, byte[] dest, int destIndex, int length)
|
||||
{
|
||||
using (var aes = Aes.Create())
|
||||
{
|
||||
if (aes == null) throw new CryptographicException("Unable to create AES object");
|
||||
aes.Key = key;
|
||||
aes.Mode = CipherMode.ECB;
|
||||
aes.Padding = PaddingMode.None;
|
||||
Array.Copy(aes.CreateDecryptor().TransformFinalBlock(src, srcIndex, length), 0, dest, destIndex, length);
|
||||
}
|
||||
}
|
||||
|
||||
public static void DecryptEcb(byte[] key, byte[] src, byte[] dest, int length) =>
|
||||
DecryptEcb(key, src, 0, dest, 0, length);
|
||||
|
||||
public static void EncryptEcb(byte[] key, byte[] src, int srcIndex, byte[] dest, int destIndex, int length)
|
||||
{
|
||||
using (var aes = Aes.Create())
|
||||
{
|
||||
if (aes == null) throw new CryptographicException("Unable to create AES object");
|
||||
aes.Key = key;
|
||||
aes.Mode = CipherMode.ECB;
|
||||
aes.Padding = PaddingMode.None;
|
||||
Array.Copy(aes.CreateEncryptor().TransformFinalBlock(src, srcIndex, length), 0, dest, destIndex, length);
|
||||
}
|
||||
}
|
||||
|
||||
public static void EncryptEcb(byte[] key, byte[] src, byte[] dest, int length) =>
|
||||
EncryptEcb(key, src, 0, dest, 0, length);
|
||||
|
||||
public static void DecryptCbc(byte[] key, byte[] iv, byte[] src, int srcIndex, byte[] dest, int destIndex, int length)
|
||||
{
|
||||
using (var aes = Aes.Create())
|
||||
{
|
||||
if (aes == null) throw new CryptographicException("Unable to create AES object");
|
||||
aes.Key = key;
|
||||
aes.IV = iv;
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.None;
|
||||
Array.Copy(aes.CreateDecryptor().TransformFinalBlock(src, srcIndex, length), 0, dest, destIndex, length);
|
||||
}
|
||||
}
|
||||
|
||||
public static void DecryptCbc(byte[] key, byte[] iv, byte[] src, byte[] dest, int length) =>
|
||||
DecryptCbc(key, iv, src, 0, dest, 0, length);
|
||||
|
||||
public static void EncryptCbc(byte[] key, byte[] iv, byte[] src, int srcIndex, byte[] dest, int destIndex, int length)
|
||||
{
|
||||
using (var aes = Aes.Create())
|
||||
{
|
||||
if (aes == null) throw new CryptographicException("Unable to create AES object");
|
||||
aes.Key = key;
|
||||
aes.IV = iv;
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.None;
|
||||
Array.Copy(aes.CreateEncryptor().TransformFinalBlock(src, srcIndex, length), 0, dest, destIndex, length);
|
||||
}
|
||||
}
|
||||
|
||||
public static void EncryptCbc(byte[] key, byte[] iv, byte[] src, byte[] dest, int length) =>
|
||||
EncryptCbc(key, iv, src, 0, dest, 0, length);
|
||||
|
||||
public static void GenerateKek(byte[] key, byte[] src, byte[] dest, byte[] kekSeed, byte[] keySeed)
|
||||
{
|
||||
var kek = new byte[Aes128Size];
|
||||
var srcKek = new byte[Aes128Size];
|
||||
var kek = new byte[Aes.KeySize128];
|
||||
var srcKek = new byte[Aes.KeySize128];
|
||||
|
||||
DecryptEcb(key, kekSeed, kek, Aes128Size);
|
||||
DecryptEcb(kek, src, srcKek, Aes128Size);
|
||||
Aes.DecryptEcb128(kekSeed, kek, key);
|
||||
Aes.DecryptEcb128(src, srcKek, kek);
|
||||
|
||||
if (keySeed != null)
|
||||
{
|
||||
DecryptEcb(srcKek, keySeed, dest, Aes128Size);
|
||||
Aes.DecryptEcb128(keySeed, dest, srcKek);
|
||||
}
|
||||
else
|
||||
{
|
||||
Array.Copy(srcKek, dest, Aes128Size);
|
||||
Array.Copy(srcKek, dest, Aes.KeySize128);
|
||||
}
|
||||
}
|
||||
|
||||
internal static BigInteger GetBigInteger(ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
var signPadded = new byte[bytes.Length + 1];
|
||||
bytes.CopyTo(signPadded.AsSpan(1));
|
||||
Array.Reverse(signPadded);
|
||||
return new BigInteger(signPadded);
|
||||
}
|
||||
|
||||
public static RSAParameters DecryptRsaKey(byte[] encryptedKey, byte[] kek)
|
||||
{
|
||||
var counter = new byte[0x10];
|
||||
|
@ -114,11 +44,7 @@ namespace LibHac
|
|||
Array.Copy(key, 0x100, n, 0, 0x100);
|
||||
Array.Copy(key, 0x200, e, 0, 4);
|
||||
|
||||
BigInteger dInt = GetBigInteger(d);
|
||||
BigInteger nInt = GetBigInteger(n);
|
||||
BigInteger eInt = GetBigInteger(e);
|
||||
|
||||
RSAParameters rsaParams = RecoverRsaParameters(nInt, eInt, dInt);
|
||||
RSAParameters rsaParams = Rsa.RecoverParameters(n, e, d);
|
||||
TestRsaKey(rsaParams);
|
||||
return rsaParams;
|
||||
}
|
||||
|
@ -138,43 +64,15 @@ namespace LibHac
|
|||
}
|
||||
}
|
||||
|
||||
public static Validity Rsa2048Pkcs1Verify(byte[] data, byte[] signature, byte[] modulus)
|
||||
{
|
||||
using (var rsa = RSA.Create())
|
||||
{
|
||||
try
|
||||
{
|
||||
rsa.ImportParameters(new RSAParameters { Exponent = new byte[] { 1, 0, 1 }, Modulus = modulus });
|
||||
public static Validity Rsa2048Pkcs1Verify(byte[] data, byte[] signature, byte[] modulus) =>
|
||||
Rsa.VerifyRsa2048Pkcs1Sha256(signature, modulus, new byte[] { 1, 0, 1 }, data)
|
||||
? Validity.Valid
|
||||
: Validity.Invalid;
|
||||
|
||||
return rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)
|
||||
? Validity.Valid
|
||||
: Validity.Invalid;
|
||||
}
|
||||
catch (CryptographicException)
|
||||
{
|
||||
return Validity.Invalid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Validity Rsa2048PssVerify(byte[] data, byte[] signature, byte[] modulus)
|
||||
{
|
||||
using (var rsa = RSA.Create())
|
||||
{
|
||||
try
|
||||
{
|
||||
rsa.ImportParameters(new RSAParameters { Exponent = new byte[] { 1, 0, 1 }, Modulus = modulus });
|
||||
|
||||
return rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pss)
|
||||
? Validity.Valid
|
||||
: Validity.Invalid;
|
||||
}
|
||||
catch (CryptographicException)
|
||||
{
|
||||
return Validity.Invalid;
|
||||
}
|
||||
}
|
||||
}
|
||||
public static Validity Rsa2048PssVerify(byte[] data, byte[] signature, byte[] modulus) =>
|
||||
Rsa.VerifyRsa2048PssSha256(signature, modulus, new byte[] { 1, 0, 1 }, data)
|
||||
? Validity.Valid
|
||||
: Validity.Invalid;
|
||||
|
||||
public static byte[] DecryptRsaOaep(byte[] data, RSAParameters rsaParams)
|
||||
{
|
||||
|
@ -201,222 +99,5 @@ namespace LibHac
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static RSAParameters RecoverRsaParameters(ReadOnlySpan<byte> modulus, ReadOnlySpan<byte> exponent)
|
||||
{
|
||||
BigInteger dInt = GetBigInteger(exponent);
|
||||
BigInteger nInt = GetBigInteger(modulus);
|
||||
BigInteger eInt = GetBigInteger(new byte[] { 1, 0, 1 });
|
||||
|
||||
return RecoverRsaParameters(nInt, eInt, dInt);
|
||||
}
|
||||
|
||||
private static RSAParameters RecoverRsaParameters(BigInteger n, BigInteger e, BigInteger d)
|
||||
{
|
||||
using (var rng = RandomNumberGenerator.Create())
|
||||
{
|
||||
BigInteger k = d * e - 1;
|
||||
|
||||
if (!k.IsEven)
|
||||
{
|
||||
throw new InvalidOperationException("d*e - 1 is odd");
|
||||
}
|
||||
|
||||
BigInteger two = 2;
|
||||
BigInteger t = BigInteger.One;
|
||||
|
||||
BigInteger r = k / two;
|
||||
|
||||
while (r.IsEven)
|
||||
{
|
||||
t++;
|
||||
r /= two;
|
||||
}
|
||||
|
||||
byte[] rndBuf = n.ToByteArray();
|
||||
|
||||
if (rndBuf[rndBuf.Length - 1] == 0)
|
||||
{
|
||||
rndBuf = new byte[rndBuf.Length - 1];
|
||||
}
|
||||
|
||||
BigInteger nMinusOne = n - BigInteger.One;
|
||||
|
||||
bool cracked = false;
|
||||
BigInteger y = BigInteger.Zero;
|
||||
|
||||
for (int i = 0; i < 100 && !cracked; i++)
|
||||
{
|
||||
BigInteger g;
|
||||
|
||||
do
|
||||
{
|
||||
rng.GetBytes(rndBuf);
|
||||
g = GetBigInteger(rndBuf);
|
||||
}
|
||||
while (g >= n);
|
||||
|
||||
y = BigInteger.ModPow(g, r, n);
|
||||
|
||||
if (y.IsOne || y == nMinusOne)
|
||||
{
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (BigInteger j = BigInteger.One; j < t; j++)
|
||||
{
|
||||
BigInteger x = BigInteger.ModPow(y, two, n);
|
||||
|
||||
if (x.IsOne)
|
||||
{
|
||||
cracked = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (x == nMinusOne)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
y = x;
|
||||
}
|
||||
}
|
||||
|
||||
if (!cracked)
|
||||
{
|
||||
throw new InvalidOperationException("Prime factors not found");
|
||||
}
|
||||
|
||||
BigInteger p = BigInteger.GreatestCommonDivisor(y - BigInteger.One, n);
|
||||
BigInteger q = n / p;
|
||||
BigInteger dp = d % (p - BigInteger.One);
|
||||
BigInteger dq = d % (q - BigInteger.One);
|
||||
BigInteger inverseQ = ModInverse(q, p);
|
||||
|
||||
int modLen = rndBuf.Length;
|
||||
int halfModLen = (modLen + 1) / 2;
|
||||
|
||||
return new RSAParameters
|
||||
{
|
||||
Modulus = GetBytes(n, modLen),
|
||||
Exponent = GetBytes(e, -1),
|
||||
D = GetBytes(d, modLen),
|
||||
P = GetBytes(p, halfModLen),
|
||||
Q = GetBytes(q, halfModLen),
|
||||
DP = GetBytes(dp, halfModLen),
|
||||
DQ = GetBytes(dq, halfModLen),
|
||||
InverseQ = GetBytes(inverseQ, halfModLen)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] GetBytes(BigInteger value, int size)
|
||||
{
|
||||
byte[] bytes = value.ToByteArray();
|
||||
|
||||
if (size == -1)
|
||||
{
|
||||
size = bytes.Length;
|
||||
}
|
||||
|
||||
if (bytes.Length > size + 1)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot squeeze value {value} to {size} bytes from {bytes.Length}.");
|
||||
}
|
||||
|
||||
if (bytes.Length == size + 1 && bytes[bytes.Length - 1] != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot squeeze value {value} to {size} bytes from {bytes.Length}.");
|
||||
}
|
||||
|
||||
Array.Resize(ref bytes, size);
|
||||
Array.Reverse(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static BigInteger ModInverse(BigInteger e, BigInteger n)
|
||||
{
|
||||
BigInteger r = n;
|
||||
BigInteger newR = e;
|
||||
BigInteger t = 0;
|
||||
BigInteger newT = 1;
|
||||
|
||||
while (newR != 0)
|
||||
{
|
||||
BigInteger quotient = r / newR;
|
||||
BigInteger temp;
|
||||
|
||||
temp = t;
|
||||
t = newT;
|
||||
newT = temp - quotient * newT;
|
||||
|
||||
temp = r;
|
||||
r = newR;
|
||||
newR = temp - quotient * newR;
|
||||
}
|
||||
|
||||
if (t < 0)
|
||||
{
|
||||
t = t + n;
|
||||
}
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
public static void CalculateAesCmac(byte[] key, byte[] src, int srcIndex, byte[] dest, int destIndex, int length)
|
||||
{
|
||||
var l = new byte[16];
|
||||
EncryptCbc(key, l, l, l, 0x10);
|
||||
byte[] paddedMessage;
|
||||
int paddedLength = length;
|
||||
|
||||
byte[] firstSubkey = Rol(l);
|
||||
if ((l[0] & 0x80) == 0x80)
|
||||
firstSubkey[15] ^= 0x87;
|
||||
|
||||
byte[] secondSubkey = Rol(firstSubkey);
|
||||
if ((firstSubkey[0] & 0x80) == 0x80)
|
||||
secondSubkey[15] ^= 0x87;
|
||||
|
||||
if (length != 0 && length % 16 == 0)
|
||||
{
|
||||
paddedMessage = new byte[paddedLength];
|
||||
Array.Copy(src, srcIndex, paddedMessage, 0, length);
|
||||
|
||||
for (int j = 0; j < firstSubkey.Length; j++)
|
||||
paddedMessage[length - 16 + j] ^= firstSubkey[j];
|
||||
}
|
||||
else
|
||||
{
|
||||
paddedLength += 16 - length % 16;
|
||||
paddedMessage = new byte[paddedLength];
|
||||
paddedMessage[length] = 0x80;
|
||||
Array.Copy(src, srcIndex, paddedMessage, 0, length);
|
||||
|
||||
for (int j = 0; j < secondSubkey.Length; j++)
|
||||
paddedMessage[paddedLength - 16 + j] ^= secondSubkey[j];
|
||||
}
|
||||
|
||||
var encResult = new byte[paddedMessage.Length];
|
||||
EncryptCbc(key, new byte[16], paddedMessage, encResult, paddedLength);
|
||||
|
||||
Array.Copy(encResult, paddedLength - 0x10, dest, destIndex, 0x10);
|
||||
}
|
||||
|
||||
private static byte[] Rol(byte[] b)
|
||||
{
|
||||
var r = new byte[b.Length];
|
||||
byte carry = 0;
|
||||
|
||||
for (int i = b.Length - 1; i >= 0; i--)
|
||||
{
|
||||
ushort u = (ushort)(b[i] << 1);
|
||||
r[i] = (byte)((u & 0xff) + carry);
|
||||
carry = (byte)((u & 0xff00) >> 8);
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,6 +5,8 @@ using System.Text;
|
|||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
|
||||
using Aes = LibHac.Crypto.Aes;
|
||||
|
||||
namespace LibHac.FsSystem
|
||||
{
|
||||
public class AesXtsFileHeader
|
||||
|
@ -79,14 +81,14 @@ namespace LibHac.FsSystem
|
|||
|
||||
private void DecryptKeys()
|
||||
{
|
||||
CryptoOld.DecryptEcb(Kek1, EncryptedKey1, DecryptedKey1, 0x10);
|
||||
CryptoOld.DecryptEcb(Kek2, EncryptedKey2, DecryptedKey2, 0x10);
|
||||
Aes.DecryptEcb128(EncryptedKey1, DecryptedKey1, Kek1);
|
||||
Aes.DecryptEcb128(EncryptedKey2, DecryptedKey2, Kek2);
|
||||
}
|
||||
|
||||
private void EncryptKeys()
|
||||
{
|
||||
CryptoOld.EncryptEcb(Kek1, DecryptedKey1, EncryptedKey1, 0x10);
|
||||
CryptoOld.EncryptEcb(Kek2, DecryptedKey2, EncryptedKey2, 0x10);
|
||||
Aes.EncryptEcb128(DecryptedKey1, EncryptedKey1, Kek1);
|
||||
Aes.EncryptEcb128(DecryptedKey2, EncryptedKey2, Kek2);
|
||||
}
|
||||
|
||||
private void GenerateKek(byte[] kekSeed, string path)
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using LibHac.Common;
|
||||
using LibHac.Crypto;
|
||||
using LibHac.Diag;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
|
@ -50,9 +51,9 @@ namespace LibHac.FsSystem.NcaUtils
|
|||
}
|
||||
|
||||
byte[] encryptedKey = Header.GetEncryptedKey(index).ToArray();
|
||||
var decryptedKey = new byte[CryptoOld.Aes128Size];
|
||||
var decryptedKey = new byte[Aes.KeySize128];
|
||||
|
||||
CryptoOld.DecryptEcb(keyAreaKey, encryptedKey, decryptedKey, CryptoOld.Aes128Size);
|
||||
Aes.DecryptEcb128(encryptedKey, decryptedKey, keyAreaKey);
|
||||
|
||||
return decryptedKey;
|
||||
}
|
||||
|
@ -76,9 +77,9 @@ namespace LibHac.FsSystem.NcaUtils
|
|||
}
|
||||
|
||||
byte[] encryptedKey = accessKey.Value.ToArray();
|
||||
var decryptedKey = new byte[CryptoOld.Aes128Size];
|
||||
var decryptedKey = new byte[Aes.KeySize128];
|
||||
|
||||
CryptoOld.DecryptEcb(titleKek, encryptedKey, decryptedKey, CryptoOld.Aes128Size);
|
||||
Aes.DecryptEcb128(encryptedKey, decryptedKey, titleKek);
|
||||
|
||||
return decryptedKey;
|
||||
}
|
||||
|
|
|
@ -160,8 +160,8 @@ namespace LibHac.FsSystem.NcaUtils
|
|||
throw new ArgumentOutOfRangeException($"Key index must be between 0 and 3. Actual: {index}");
|
||||
}
|
||||
|
||||
int offset = NcaHeaderStruct.KeyAreaOffset + CryptoOld.Aes128Size * index;
|
||||
return _header.Span.Slice(offset, CryptoOld.Aes128Size);
|
||||
int offset = NcaHeaderStruct.KeyAreaOffset + Aes.KeySize128 * index;
|
||||
return _header.Span.Slice(offset, Aes.KeySize128);
|
||||
}
|
||||
|
||||
public NcaFsHeader GetFsHeader(int index)
|
||||
|
|
|
@ -91,11 +91,11 @@ namespace LibHac.FsSystem.Save
|
|||
|
||||
private Validity ValidateSignature(Keyset keyset)
|
||||
{
|
||||
var calculatedCmac = new byte[0x10];
|
||||
Span<byte> calculatedCmac = stackalloc byte[0x10];
|
||||
|
||||
CryptoOld.CalculateAesCmac(keyset.SaveMacKey, Data, 0x100, calculatedCmac, 0, 0x200);
|
||||
Aes.CalculateCmac(calculatedCmac, Data.AsSpan(0x100, 0x200), keyset.SaveMacKey);
|
||||
|
||||
return Utilities.ArraysEqual(calculatedCmac, Cmac) ? Validity.Valid : Validity.Invalid;
|
||||
return CryptoUtil.IsSameBytes(calculatedCmac, Cmac, Aes.BlockSize) ? Validity.Valid : Validity.Invalid;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -269,7 +269,7 @@ namespace LibHac.FsSystem.Save
|
|||
headerStream.Position = 0x100;
|
||||
headerStream.Read(cmacData, 0, 0x200);
|
||||
|
||||
CryptoOld.CalculateAesCmac(keyset.SaveMacKey, cmacData, 0, cmac, 0, 0x200);
|
||||
Aes.CalculateCmac(cmac, cmacData, keyset.SaveMacKey);
|
||||
|
||||
headerStream.Position = 0;
|
||||
headerStream.Write(cmac, 0, 0x10);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using LibHac.Common;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
|
||||
|
@ -71,10 +71,8 @@ namespace LibHac.FsSystem
|
|||
|
||||
public static IFile AsFile(this IStorage storage, OpenMode mode) => new StorageFile(storage, mode);
|
||||
|
||||
public static void CopyTo(this IStorage input, IStorage output, IProgressReport progress = null)
|
||||
public static void CopyTo(this IStorage input, IStorage output, IProgressReport progress = null, int bufferSize = 81920)
|
||||
{
|
||||
const int bufferSize = 81920;
|
||||
|
||||
input.GetSize(out long inputSize).ThrowIfFailure();
|
||||
output.GetSize(out long outputSize).ThrowIfFailure();
|
||||
|
||||
|
@ -84,25 +82,20 @@ namespace LibHac.FsSystem
|
|||
|
||||
long pos = 0;
|
||||
|
||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
|
||||
try
|
||||
{
|
||||
while (remaining > 0)
|
||||
{
|
||||
int toCopy = (int)Math.Min(bufferSize, remaining);
|
||||
Span<byte> buf = buffer.AsSpan(0, toCopy);
|
||||
input.Read(pos, buf);
|
||||
output.Write(pos, buf);
|
||||
using var buffer = new RentedArray<byte>(bufferSize);
|
||||
int rentedBufferSize = buffer.Array.Length;
|
||||
|
||||
remaining -= toCopy;
|
||||
pos += toCopy;
|
||||
|
||||
progress?.ReportAdd(toCopy);
|
||||
}
|
||||
}
|
||||
finally
|
||||
while (remaining > 0)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
int toCopy = (int)Math.Min(rentedBufferSize, remaining);
|
||||
Span<byte> buf = buffer.Array.AsSpan(0, toCopy);
|
||||
input.Read(pos, buf);
|
||||
output.Write(pos, buf);
|
||||
|
||||
remaining -= toCopy;
|
||||
pos += toCopy;
|
||||
|
||||
progress?.ReportAdd(toCopy);
|
||||
}
|
||||
|
||||
progress?.SetTotal(0);
|
||||
|
@ -140,27 +133,22 @@ namespace LibHac.FsSystem
|
|||
|
||||
long pos = offset;
|
||||
|
||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
|
||||
try
|
||||
using var buffer = new RentedArray<byte>(bufferSize);
|
||||
int rentedBufferSize = buffer.Array.Length;
|
||||
|
||||
buffer.Array.AsSpan(0, (int)Math.Min(remaining, rentedBufferSize)).Fill(value);
|
||||
|
||||
while (remaining > 0)
|
||||
{
|
||||
buffer.AsSpan(0, (int)Math.Min(remaining, bufferSize)).Fill(value);
|
||||
int toFill = (int)Math.Min(rentedBufferSize, remaining);
|
||||
Span<byte> buf = buffer.Array.AsSpan(0, toFill);
|
||||
|
||||
while (remaining > 0)
|
||||
{
|
||||
int toFill = (int)Math.Min(bufferSize, remaining);
|
||||
Span<byte> buf = buffer.AsSpan(0, toFill);
|
||||
input.Write(pos, buf);
|
||||
|
||||
input.Write(pos, buf);
|
||||
remaining -= toFill;
|
||||
pos += toFill;
|
||||
|
||||
remaining -= toFill;
|
||||
pos += toFill;
|
||||
|
||||
progress?.ReportAdd(toFill);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
progress?.ReportAdd(toFill);
|
||||
}
|
||||
|
||||
progress?.SetTotal(0);
|
||||
|
@ -200,30 +188,32 @@ namespace LibHac.FsSystem
|
|||
return arr;
|
||||
}
|
||||
|
||||
public static void CopyToStream(this IStorage input, Stream output, long length, IProgressReport progress = null)
|
||||
public static void CopyToStream(this IStorage input, Stream output, long length, IProgressReport progress = null, int bufferSize = 0x8000)
|
||||
{
|
||||
const int bufferSize = 0x8000;
|
||||
long remaining = length;
|
||||
long inOffset = 0;
|
||||
var buffer = new byte[bufferSize];
|
||||
|
||||
using var buffer = new RentedArray<byte>(bufferSize);
|
||||
int rentedBufferSize = buffer.Array.Length;
|
||||
|
||||
progress?.SetTotal(length);
|
||||
|
||||
while (remaining > 0)
|
||||
{
|
||||
int toWrite = (int)Math.Min(buffer.Length, remaining);
|
||||
input.Read(inOffset, buffer.AsSpan(0, toWrite));
|
||||
int toWrite = (int)Math.Min(rentedBufferSize, remaining);
|
||||
input.Read(inOffset, buffer.Array.AsSpan(0, toWrite));
|
||||
|
||||
output.Write(buffer, 0, toWrite);
|
||||
output.Write(buffer.Array, 0, toWrite);
|
||||
remaining -= toWrite;
|
||||
inOffset += toWrite;
|
||||
progress?.ReportAdd(toWrite);
|
||||
}
|
||||
}
|
||||
|
||||
public static void CopyToStream(this IStorage input, Stream output)
|
||||
public static void CopyToStream(this IStorage input, Stream output, int bufferSize = 0x8000)
|
||||
{
|
||||
input.GetSize(out long inputSize).ThrowIfFailure();
|
||||
CopyToStream(input, output, inputSize);
|
||||
CopyToStream(input, output, inputSize, bufferSize: bufferSize);
|
||||
}
|
||||
|
||||
public static IStorage AsStorage(this Stream stream)
|
||||
|
|
|
@ -4,6 +4,7 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using LibHac.Crypto;
|
||||
using LibHac.Fs;
|
||||
using LibHac.FsSrv;
|
||||
using LibHac.Spl;
|
||||
|
@ -81,7 +82,7 @@ namespace LibHac
|
|||
if (_nca0RsaKeyAreaKey.HasValue)
|
||||
return _nca0RsaKeyAreaKey.Value;
|
||||
|
||||
_nca0RsaKeyAreaKey = CryptoOld.RecoverRsaParameters(BetaNca0Modulus, BetaNca0Exponent);
|
||||
_nca0RsaKeyAreaKey = Rsa.RecoverParameters(BetaNca0Modulus, BetaNca0Exponent, new byte[] { 1, 0, 1 });
|
||||
return _nca0RsaKeyAreaKey.Value;
|
||||
}
|
||||
}
|
||||
|
@ -349,7 +350,7 @@ namespace LibHac
|
|||
}
|
||||
|
||||
Array.Copy(EncryptedKeyblobs[i], expectedCmac, 0x10);
|
||||
CryptoOld.CalculateAesCmac(KeyblobMacKeys[i], EncryptedKeyblobs[i], 0x10, cmac, 0, 0xa0);
|
||||
Aes.CalculateCmac(cmac, EncryptedKeyblobs[i].AsSpan(0x10, 0xA0), KeyblobMacKeys[i]);
|
||||
|
||||
if (!Utilities.ArraysEqual(cmac, expectedCmac))
|
||||
{
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
@ -500,6 +500,67 @@ namespace LibHac
|
|||
return value > 0 && ResetLeastSignificantOneBit(value) == 0;
|
||||
}
|
||||
|
||||
public static BigInteger GetBigInteger(this ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
var signPadded = new byte[bytes.Length + 1];
|
||||
bytes.CopyTo(signPadded.AsSpan(1));
|
||||
Array.Reverse(signPadded);
|
||||
return new BigInteger(signPadded);
|
||||
}
|
||||
|
||||
public static byte[] GetBytes(this BigInteger value, int size)
|
||||
{
|
||||
byte[] bytes = value.ToByteArray();
|
||||
|
||||
if (size == -1)
|
||||
{
|
||||
size = bytes.Length;
|
||||
}
|
||||
|
||||
if (bytes.Length > size + 1)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot squeeze value {value} to {size} bytes from {bytes.Length}.");
|
||||
}
|
||||
|
||||
if (bytes.Length == size + 1 && bytes[bytes.Length - 1] != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot squeeze value {value} to {size} bytes from {bytes.Length}.");
|
||||
}
|
||||
|
||||
Array.Resize(ref bytes, size);
|
||||
Array.Reverse(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public static BigInteger ModInverse(BigInteger e, BigInteger n)
|
||||
{
|
||||
BigInteger r = n;
|
||||
BigInteger newR = e;
|
||||
BigInteger t = 0;
|
||||
BigInteger newT = 1;
|
||||
|
||||
while (newR != 0)
|
||||
{
|
||||
BigInteger quotient = r / newR;
|
||||
BigInteger temp;
|
||||
|
||||
temp = t;
|
||||
t = newT;
|
||||
newT = temp - quotient * newT;
|
||||
|
||||
temp = r;
|
||||
r = newR;
|
||||
newR = temp - quotient * newR;
|
||||
}
|
||||
|
||||
if (t < 0)
|
||||
{
|
||||
t = t + n;
|
||||
}
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int ResetLeastSignificantOneBit(int value)
|
||||
{
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using LibHac.Crypto;
|
||||
|
@ -96,7 +96,7 @@ namespace LibHac
|
|||
Flags = (GameCardAttribute)reader.ReadByte();
|
||||
PackageId = reader.ReadUInt64();
|
||||
ValidDataEndPage = reader.ReadInt64();
|
||||
AesCbcIv = reader.ReadBytes(CryptoOld.Aes128Size);
|
||||
AesCbcIv = reader.ReadBytes(Aes.KeySize128);
|
||||
Array.Reverse(AesCbcIv);
|
||||
RootPartitionOffset = reader.ReadInt64();
|
||||
RootPartitionHeaderSize = reader.ReadInt64();
|
||||
|
@ -111,7 +111,7 @@ namespace LibHac
|
|||
{
|
||||
byte[] encHeader = reader.ReadBytes(EncryptedHeaderSize);
|
||||
var decHeader = new byte[EncryptedHeaderSize];
|
||||
CryptoOld.DecryptCbc(keyset.XciHeaderKey, AesCbcIv, encHeader, decHeader, EncryptedHeaderSize);
|
||||
Aes.DecryptCbc128(encHeader, decHeader, keyset.XciHeaderKey, AesCbcIv);
|
||||
|
||||
using (var decreader = new BinaryReader(new MemoryStream(decHeader)))
|
||||
{
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
using LibHac;
|
||||
using LibHac.Fs;
|
||||
using LibHac.FsSystem.NcaUtils;
|
||||
using LibHac.Ncm;
|
||||
using ContentType = LibHac.Ncm.ContentType;
|
||||
|
||||
namespace hactoolnet
|
||||
{
|
||||
|
@ -96,5 +98,20 @@ namespace hactoolnet
|
|||
_ => value.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
public static string Print(this SaveDataType value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
SaveDataType.System => nameof(SaveDataType.System),
|
||||
SaveDataType.Account => nameof(SaveDataType.Account),
|
||||
SaveDataType.Bcat => nameof(SaveDataType.Bcat),
|
||||
SaveDataType.Device => nameof(SaveDataType.Device),
|
||||
SaveDataType.Temporary => nameof(SaveDataType.Temporary),
|
||||
SaveDataType.Cache => nameof(SaveDataType.Cache),
|
||||
SaveDataType.SystemBcat => nameof(SaveDataType.SystemBcat),
|
||||
_ => value.ToString()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -330,7 +330,7 @@ namespace hactoolnet
|
|||
PrintItem(sb, colLen, "Title ID:", $"{save.Header.ExtraData.TitleId:x16}");
|
||||
PrintItem(sb, colLen, "User ID:", save.Header.ExtraData.UserId);
|
||||
PrintItem(sb, colLen, "Save ID:", $"{save.Header.ExtraData.SaveId:x16}");
|
||||
PrintItem(sb, colLen, "Save Type:", $"{save.Header.ExtraData.Type}");
|
||||
PrintItem(sb, colLen, "Save Type:", $"{save.Header.ExtraData.Type.Print()}");
|
||||
PrintItem(sb, colLen, "Owner ID:", $"{save.Header.ExtraData.SaveOwnerId:x16}");
|
||||
PrintItem(sb, colLen, "Timestamp:", $"{DateTimeOffset.FromUnixTimeSeconds(save.Header.ExtraData.Timestamp):yyyy-MM-dd HH:mm:ss} UTC");
|
||||
PrintItem(sb, colLen, "Save Data Size:", $"0x{save.Header.ExtraData.DataSize:x16} ({Utilities.GetBytesReadable(save.Header.ExtraData.DataSize)})");
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
using Xunit;
|
||||
using System;
|
||||
using LibHac.Crypto;
|
||||
using Xunit;
|
||||
|
||||
namespace LibHac.Tests
|
||||
{
|
||||
|
@ -57,7 +59,7 @@ namespace LibHac.Tests
|
|||
{
|
||||
var actual = new byte[0x10];
|
||||
|
||||
CryptoOld.CalculateAesCmac(data.Key, data.Message, data.Start, actual, 0, data.Length);
|
||||
Aes.CalculateCmac(actual, data.Message.AsSpan(data.Start, data.Length), data.Key);
|
||||
|
||||
Assert.Equal(data.Expected, actual);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue