Repository navigation
Expand file tree
/
Copy pathCrypto.cs
More file actions
143 lines (123 loc) · 5.2 KB
/
Copy pathCrypto.cs
File metadata and controls
143 lines (123 loc) · 5.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
using System;
using System.IO;
using System.IO.Compression;
using System.Security.Cryptography;
namespace Es3Offline
{
/// <summary>
/// Easy Save 3 AES file format:
/// file = salt[16] || AES-128-CBC(key, IV=salt, PKCS7(plaintext))
/// key = PBKDF2-HMAC-SHA1(password, salt, 100 iterations, 16 bytes)
/// The plaintext is text (JSON-ish, sometimes GZip-compressed first).
/// </summary>
static class Crypto
{
public const int SaltSize = 16;
public static byte[] Decrypt(byte[] blob, string password)
{
if (blob.Length < SaltSize + 16 || (blob.Length - SaltSize) % 16 != 0)
throw new InvalidDataException(
"file size is not 16-byte-aligned after the salt header; not an ES3 AES file");
var salt = new byte[SaltSize];
Buffer.BlockCopy(blob, 0, salt, 0, SaltSize);
using var aes = Aes.Create();
aes.Key = DeriveKey(password, salt);
aes.IV = salt;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using var dec = aes.CreateDecryptor();
return dec.TransformFinalBlock(blob, SaltSize, blob.Length - SaltSize);
}
public static byte[] Encrypt(byte[] plain, string password, bool gzip)
{
if (gzip)
plain = Gzip(plain);
var salt = RandomNumberGenerator.GetBytes(SaltSize);
using var aes = Aes.Create();
aes.Key = DeriveKey(password, salt);
aes.IV = salt;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using var enc = aes.CreateEncryptor();
var body = enc.TransformFinalBlock(plain, 0, plain.Length);
var blob = new byte[SaltSize + body.Length];
Buffer.BlockCopy(salt, 0, blob, 0, SaltSize);
Buffer.BlockCopy(body, 0, blob, SaltSize, body.Length);
return blob;
}
/// <summary>
/// Decrypt and validate. Returns null when the password is wrong (or the result
/// is not ES3-like). PKCS7 validity is probabilistic (1/256 per wrong key), so
/// with large candidate lists we also require textual-looking content:
/// printableRatio must be >= threshold, and the first byte must be JSON-ish
/// ('{', '"', '[', digit) or a GZip magic (then decompressed and re-checked).
/// </summary>
public static byte[] TryDecrypt(byte[] blob, string password, double printableThreshold)
{
try
{
var plain = Decrypt(blob, password);
var content = plain;
if (IsGzip(plain))
{
try { content = Gunzip(plain); }
catch { return null; }
}
if (content.Length < 2 || !IsTextualStart(content))
return null;
return PrintableRatio(content) >= printableThreshold ? plain : null;
}
catch (CryptographicException) { return null; } // bad PKCS7 padding: wrong password
catch (InvalidDataException) { return null; }
}
/// <summary>Unencrypted ES3 saves are already textual; detect them so the user is not asked for a password.</summary>
public static bool LooksPlaintext(byte[] blob)
{
return blob.Length >= 2 && IsTextualStart(blob) && PrintableRatio(blob) >= 0.98;
}
public static bool IsGzip(byte[] data)
{
return data.Length >= 2 && data[0] == 0x1f && data[1] == 0x8b;
}
public static byte[] GunzipIfCompressed(byte[] data)
{
return IsGzip(data) ? Gunzip(data) : data;
}
static bool IsTextualStart(byte[] p)
{
var head = p[0];
return head == '{' || head == '"' || head == '['
|| (head >= '0' && head <= '9')
|| (p.Length >= 3 && head == 'E' && p[1] == 'S' && p[2] == '3');
}
static double PrintableRatio(byte[] data)
{
if (data.Length == 0) return 0;
int good = 0;
foreach (var b in data)
if (b == 0x09 || b == 0x0a || b == 0x0d || (b >= 0x20 && b < 0x7f) || b >= 0x80) // tab/lf/cr/ascii/utf8
good++;
return (double)good / data.Length;
}
static byte[] DeriveKey(string password, byte[] salt)
{
return Rfc2898DeriveBytes.Pbkdf2(
System.Text.Encoding.UTF8.GetBytes(password), salt, 100, HashAlgorithmName.SHA1, 16);
}
static byte[] Gzip(byte[] data)
{
using var output = new MemoryStream();
using (var gz = new GZipStream(output, CompressionLevel.Optimal, leaveOpen: true))
gz.Write(data, 0, data.Length);
return output.ToArray();
}
static byte[] Gunzip(byte[] data)
{
using var input = new MemoryStream(data);
using var gz = new GZipStream(input, CompressionMode.Decompress);
using var output = new MemoryStream();
gz.CopyTo(output);
return output.ToArray();
}
}
}