1 // OpenTween - Client of Twitter
2 // Copyright (c) 2022 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
3 // All rights reserved.
5 // This file is part of OpenTween.
7 // This program is free software; you can redistribute it and/or modify it
8 // under the terms of the GNU General public License as published by the Free
9 // Software Foundation; either version 3 of the License, or (at your option)
12 // This program is distributed in the hope that it will be useful, but
13 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License
17 // You should have received a copy of the GNU General public License along
18 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
19 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
20 // Boston, MA 02110-1301, USA.
25 using System.Collections.Generic;
26 using System.Diagnostics.CodeAnalysis;
29 using System.Runtime.Serialization;
30 using System.Security.Cryptography;
32 using System.Threading.Tasks;
38 private static readonly string EncryptionPrefix = "%e%";
39 private static readonly int SaltSize = 16;
40 private static readonly int KeySize = 32;
41 private static readonly int BlockSize = 16;
42 private static readonly int IterationCount = 10_000;
43 private static readonly HashAlgorithmName HashAlgorithm = HashAlgorithmName.SHA256;
45 private readonly string rawKey;
46 private readonly Lazy<string> decryptLazy;
51 /// <exception cref="ApiKeyDecryptException" />
52 public string Value => this.decryptLazy.Value;
54 private ApiKey(string password, string rawKey)
57 this.decryptLazy = new Lazy<string>(
58 () => Decrypt(password, this.rawKey)
66 /// 成功した場合は true、暗号化された API キーの復号に失敗した場合は false を返します
68 public bool TryGetValue([NotNullWhen(true)]out string? output)
75 catch (ApiKeyDecryptException)
83 /// <see cref="ApiKey"/> インスタンスを作成します
85 public static ApiKey Create(string rawKey)
86 => Create(ApplicationSettings.EncryptionPassword, rawKey);
89 /// <see cref="ApiKey"/> インスタンスを作成します
91 public static ApiKey Create(string password, string rawKey)
92 => new(password, rawKey);
95 /// 指定された文字列を暗号化して返します
97 public static string Encrypt(string password, string plainText)
99 var salt = GenerateSalt();
100 var (encryptionKey, iv, macKey) = GenerateKeyAndIV(password, salt);
102 using var aes = CreateAes();
103 aes.Key = encryptionKey;
106 var plainBytes = Encoding.UTF8.GetBytes(plainText);
107 using var encryptor = aes.CreateEncryptor();
108 using var memoryStream = new MemoryStream(plainBytes);
109 using var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Read);
111 using var cipherStream = new MemoryStream(capacity: plainBytes.Length + BlockSize + SaltSize);
112 cryptoStream.CopyTo(cipherStream);
114 var cipherBytes = cipherStream.ToArray();
115 var macBytes = GenerateMAC(plainBytes, macKey);
116 var saltText = Convert.ToBase64String(salt);
117 var cipherText = Convert.ToBase64String(cipherBytes);
118 var macText = Convert.ToBase64String(macBytes);
120 return $"{EncryptionPrefix}{saltText}%{cipherText}%{macText}";
126 /// <exception cref="ApiKeyDecryptException" />
127 public static string Decrypt(string password, string encryptedText)
129 // 先頭が "%e%" から始まっていない場合、APIキーは平文のまま書かれているものとして扱う
130 if (!encryptedText.StartsWith(EncryptionPrefix))
131 return encryptedText;
133 var splitted = encryptedText.Split('%');
134 if (splitted.Length != 5)
135 throw new ApiKeyDecryptException("暗号文のフォーマットが不正です");
137 byte[] salt, cipherBytes, macBytes;
140 // e.g. "%e%...salt...%...cipher...%...mac..."
141 salt = Convert.FromBase64String(splitted[2]);
142 cipherBytes = Convert.FromBase64String(splitted[3]);
143 macBytes = Convert.FromBase64String(splitted[4]);
145 catch (FormatException ex)
147 throw new ApiKeyDecryptException("不正な Base64 フォーマットです", ex);
150 var (encryptionKey, iv, macKey) = GenerateKeyAndIV(password, salt);
151 using var aes = CreateAes();
152 aes.Key = encryptionKey;
155 byte[] decryptedBytes;
158 using var decryptor = aes.CreateDecryptor();
159 using var memoryStream = new MemoryStream(cipherBytes);
160 using var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read);
162 using var decryptedStream = new MemoryStream(capacity: cipherBytes.Length);
163 cryptoStream.CopyTo(decryptedStream);
165 decryptedBytes = decryptedStream.ToArray();
167 catch (CryptographicException ex)
169 throw new ApiKeyDecryptException("API キーの復号に失敗しました", ex);
172 var macBytesExpected = GenerateMAC(decryptedBytes, macKey);
174 var isValid = macBytes.Length == macBytesExpected.Length &&
175 Enumerable.Zip(macBytes, macBytesExpected, (x, y) => x == y).All(x => x);
177 throw new ApiKeyDecryptException("ダイジェストが一致しません");
179 return Encoding.UTF8.GetString(decryptedBytes);
182 private static byte[] GenerateSalt()
184 using var random = new RNGCryptoServiceProvider();
185 var salt = new byte[SaltSize];
186 random.GetBytes(salt);
190 private static (byte[] EncryptionKey, byte[] IV, byte[] MacKey) GenerateKeyAndIV(string password, byte[] salt)
192 using var generator = new Rfc2898DeriveBytes(password, salt, IterationCount, HashAlgorithm);
193 var encryptionKey = generator.GetBytes(KeySize);
194 var iv = generator.GetBytes(BlockSize);
195 var macKey = generator.GetBytes(KeySize);
196 return (encryptionKey, iv, macKey);
199 private static byte[] GenerateMAC(byte[] source, byte[] key)
201 using var hmac = new HMACSHA256(key);
202 return hmac.ComputeHash(source);
205 private static Aes CreateAes()
207 var aes = Aes.Create();
208 aes.KeySize = KeySize * 8;
209 aes.BlockSize = BlockSize * 8;
210 aes.Mode = CipherMode.CBC;
211 aes.Padding = PaddingMode.PKCS7;
216 public static class ApiKeyExtensions
218 #pragma warning disable SA1141
219 public static bool TryGetValue(this ValueTuple<ApiKey, ApiKey> apiKeys, out ValueTuple<string, string> decryptedKeys)
220 #pragma warning restore SA1141
222 var (apiKey1, apiKey2) = apiKeys;
223 if (apiKey1.TryGetValue(out var decrypted1) && apiKey2.TryGetValue(out var decrypted2))
225 decryptedKeys = (decrypted1, decrypted2);
229 decryptedKeys = ("", "");
235 public class ApiKeyDecryptException : Exception
237 public ApiKeyDecryptException()
241 public ApiKeyDecryptException(string message)
246 public ApiKeyDecryptException(string message, Exception innerException)
247 : base(message, innerException)
251 protected ApiKeyDecryptException(SerializationInfo info, StreamingContext context)
252 : base(info, context)