OSDN Git Service

PostMultipartRequestクラスを追加
[opentween/open-tween.git] / OpenTween / ApiKey.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2022 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
3 // All rights reserved.
4 //
5 // This file is part of OpenTween.
6 //
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)
10 // any later version.
11 //
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
15 // for more details.
16 //
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.
21
22 #nullable enable
23
24 using System;
25 using System.Collections.Generic;
26 using System.Diagnostics.CodeAnalysis;
27 using System.IO;
28 using System.Linq;
29 using System.Runtime.Serialization;
30 using System.Security.Cryptography;
31 using System.Text;
32 using System.Threading.Tasks;
33
34 namespace OpenTween
35 {
36     public class ApiKey
37     {
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;
44
45         private readonly string rawKey;
46         private readonly Lazy<string> decryptLazy;
47
48         /// <summary>
49         /// 平文の API キー
50         /// </summary>
51         /// <exception cref="ApiKeyDecryptException" />
52         public string Value => this.decryptLazy.Value;
53
54         private ApiKey(string password, string rawKey)
55         {
56             this.rawKey = rawKey;
57             this.decryptLazy = new Lazy<string>(
58                 () => Decrypt(password, this.rawKey)
59             );
60         }
61
62         /// <summary>
63         /// 平文の API キーを返します
64         /// </summary>
65         /// <returns>
66         /// 成功した場合は true、暗号化された API キーの復号に失敗した場合は false を返します
67         /// </returns>
68         public bool TryGetValue([NotNullWhen(true)]out string? output)
69         {
70             try
71             {
72                 output = this.Value;
73                 return true;
74             }
75             catch (ApiKeyDecryptException)
76             {
77                 output = null;
78                 return false;
79             }
80         }
81
82         /// <summary>
83         /// <see cref="ApiKey"/> インスタンスを作成します
84         /// </summary>
85         public static ApiKey Create(string rawKey)
86             => Create(ApplicationSettings.EncryptionPassword, rawKey);
87
88         /// <summary>
89         /// <see cref="ApiKey"/> インスタンスを作成します
90         /// </summary>
91         public static ApiKey Create(string password, string rawKey)
92             => new(password, rawKey);
93
94         /// <summary>
95         /// 指定された文字列を暗号化して返します
96         /// </summary>
97         public static string Encrypt(string password, string plainText)
98         {
99             var salt = GenerateSalt();
100             var (encryptionKey, iv, macKey) = GenerateKeyAndIV(password, salt);
101
102             using var aes = CreateAes();
103             aes.Key = encryptionKey;
104             aes.IV = iv;
105
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);
110
111             using var cipherStream = new MemoryStream(capacity: plainBytes.Length + BlockSize + SaltSize);
112             cryptoStream.CopyTo(cipherStream);
113
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);
119
120             return $"{EncryptionPrefix}{saltText}%{cipherText}%{macText}";
121         }
122
123         /// <summary>
124         /// 暗号化された文字列を復号します
125         /// </summary>
126         /// <exception cref="ApiKeyDecryptException" />
127         public static string Decrypt(string password, string encryptedText)
128         {
129             // 先頭が "%e%" から始まっていない場合、APIキーは平文のまま書かれているものとして扱う
130             if (!encryptedText.StartsWith(EncryptionPrefix))
131                 return encryptedText;
132
133             var splitted = encryptedText.Split('%');
134             if (splitted.Length != 5)
135                 throw new ApiKeyDecryptException("暗号文のフォーマットが不正です");
136
137             byte[] salt, cipherBytes, macBytes;
138             try
139             {
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]);
144             }
145             catch (FormatException ex)
146             {
147                 throw new ApiKeyDecryptException("不正な Base64 フォーマットです", ex);
148             }
149
150             var (encryptionKey, iv, macKey) = GenerateKeyAndIV(password, salt);
151             using var aes = CreateAes();
152             aes.Key = encryptionKey;
153             aes.IV = iv;
154
155             byte[] decryptedBytes;
156             try
157             {
158                 using var decryptor = aes.CreateDecryptor();
159                 using var memoryStream = new MemoryStream(cipherBytes);
160                 using var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read);
161
162                 using var decryptedStream = new MemoryStream(capacity: cipherBytes.Length);
163                 cryptoStream.CopyTo(decryptedStream);
164
165                 decryptedBytes = decryptedStream.ToArray();
166             }
167             catch (CryptographicException ex)
168             {
169                 throw new ApiKeyDecryptException("API キーの復号に失敗しました", ex);
170             }
171
172             var macBytesExpected = GenerateMAC(decryptedBytes, macKey);
173
174             var isValid = macBytes.Length == macBytesExpected.Length &&
175                 Enumerable.Zip(macBytes, macBytesExpected, (x, y) => x == y).All(x => x);
176             if (!isValid)
177                 throw new ApiKeyDecryptException("ダイジェストが一致しません");
178
179             return Encoding.UTF8.GetString(decryptedBytes);
180         }
181
182         private static byte[] GenerateSalt()
183         {
184             using var random = new RNGCryptoServiceProvider();
185             var salt = new byte[SaltSize];
186             random.GetBytes(salt);
187             return salt;
188         }
189
190         private static (byte[] EncryptionKey, byte[] IV, byte[] MacKey) GenerateKeyAndIV(string password, byte[] salt)
191         {
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);
197         }
198
199         private static byte[] GenerateMAC(byte[] source, byte[] key)
200         {
201             using var hmac = new HMACSHA256(key);
202             return hmac.ComputeHash(source);
203         }
204
205         private static Aes CreateAes()
206         {
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;
212             return aes;
213         }
214     }
215
216     public static class ApiKeyExtensions
217     {
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
221         {
222             var (apiKey1, apiKey2) = apiKeys;
223             if (apiKey1.TryGetValue(out var decrypted1) && apiKey2.TryGetValue(out var decrypted2))
224             {
225                 decryptedKeys = (decrypted1, decrypted2);
226                 return true;
227             }
228
229             decryptedKeys = ("", "");
230             return false;
231         }
232     }
233
234     [Serializable]
235     public class ApiKeyDecryptException : Exception
236     {
237         public ApiKeyDecryptException()
238         {
239         }
240
241         public ApiKeyDecryptException(string message)
242             : base(message)
243         {
244         }
245
246         public ApiKeyDecryptException(string message, Exception innerException)
247             : base(message, innerException)
248         {
249         }
250
251         protected ApiKeyDecryptException(SerializationInfo info, StreamingContext context)
252             : base(info, context)
253         {
254         }
255     }
256 }