Skip to content
This repository was archived by the owner on Apr 1, 2025. It is now read-only.

Commit 4df8c54

Browse files
authored
Merge pull request #89 from klimkjar/84-feature-dpapi-token-storage
POC for DPAPI-protected storage of refresh tokens in config file
2 parents 8affbb2 + 44530ad commit 4df8c54

File tree

5 files changed

+83
-4
lines changed

5 files changed

+83
-4
lines changed

KoenZomers.KeePass.OneDriveSync/Configuration.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,13 @@ public static void Load()
184184
{
185185
windowsCredentialManagerDatabaseConfig.Value.RefreshToken = Utilities.GetRefreshTokenFromWindowsCredentialManager(windowsCredentialManagerDatabaseConfig.Key);
186186
}
187+
188+
// Decrypt all database configurations which have their OneDrive Refresh Token stored encrypted in the config file
189+
var encryptedDatabaseConfigs = PasswordDatabases.Where(pwdDb => pwdDb.Value.RefreshTokenStorage == Enums.OneDriveRefreshTokenStorage.DiskEncrypted);
190+
foreach (var encryptedDatabaseConfig in encryptedDatabaseConfigs)
191+
{
192+
encryptedDatabaseConfig.Value.RefreshToken = Utilities.Unprotect(encryptedDatabaseConfig.Value.RefreshToken);
193+
}
187194
}
188195

189196
/// <summary>
@@ -200,8 +207,14 @@ public static void Save()
200207
switch (passwordDatabase.Value.RefreshTokenStorage)
201208
{
202209
case Enums.OneDriveRefreshTokenStorage.Disk:
203-
// Refresh token will be stored on disk, we can store the complete configuration instance on disk in this case
204-
passwordDatabasesForStoring.Add(passwordDatabase.Key, passwordDatabase.Value);
210+
case Enums.OneDriveRefreshTokenStorage.DiskEncrypted:
211+
// Enforce encryption of tokens previously stored in plain text
212+
passwordDatabase.Value.RefreshTokenStorage = Enums.OneDriveRefreshTokenStorage.DiskEncrypted;
213+
214+
// Refresh token will be stored encrypted on disk, we create a copy of the configuration and encrypt the refresh token
215+
var diskConfiguration = (Configuration)passwordDatabase.Value.Clone();
216+
diskConfiguration.RefreshToken = Utilities.Protect(diskConfiguration.RefreshToken);
217+
passwordDatabasesForStoring.Add(passwordDatabase.Key, diskConfiguration);
205218
break;
206219

207220
case Enums.OneDriveRefreshTokenStorage.KeePassDatabase:
@@ -258,6 +271,7 @@ public static void DeleteConfig(string localPasswordDatabasePath)
258271
switch (config.RefreshTokenStorage)
259272
{
260273
case Enums.OneDriveRefreshTokenStorage.Disk:
274+
case Enums.OneDriveRefreshTokenStorage.DiskEncrypted:
261275
// No action required as it will be removed as part of removing the complete configuration
262276
break;
263277

KoenZomers.KeePass.OneDriveSync/Enums/OneDriveRefreshTokenStorage.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ public enum OneDriveRefreshTokenStorage : short
1818
/// <summary>
1919
/// Saves the RefreshToken in the encrypted KeePass database
2020
/// </summary>
21-
KeePassDatabase = 2
21+
KeePassDatabase = 2,
22+
23+
/// <summary>
24+
/// Saves the RefreshToken encrypted with DPAPI in KeePass.config.xml located in %APPDATA%\KeePass
25+
/// </summary>
26+
DiskEncrypted = 3,
2227
}
2328
}

KoenZomers.KeePass.OneDriveSync/Forms/OneDriveRefreshTokenStorageDialog.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ private void OneDriveRefreshTokenStorageDialog_FormClosing(object sender, FormCl
3737
}
3838
if (OnDiskRadio.Checked)
3939
{
40-
_configuration.RefreshTokenStorage = OneDriveRefreshTokenStorage.Disk;
40+
_configuration.RefreshTokenStorage = OneDriveRefreshTokenStorage.DiskEncrypted;
4141
}
4242
}
4343
}

KoenZomers.KeePass.OneDriveSync/KoenZomers.KeePass.OneDriveSync.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
<Private>True</Private>
7070
</Reference>
7171
<Reference Include="System.Runtime.Serialization" />
72+
<Reference Include="System.Security" />
7273
<Reference Include="System.ServiceModel" />
7374
<Reference Include="System.Web" />
7475
<Reference Include="System.Web.Extensions" />

KoenZomers.KeePass.OneDriveSync/Utilities.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.IO;
33
using System.Net;
44
using System.Security.Cryptography;
5+
using System.Text;
56
using System.Threading.Tasks;
67
using CredentialManagement;
78
using KeePassLib;
@@ -262,5 +263,63 @@ public static string GetRefreshTokenFromKeePassDatabase(PwDatabase keePassDataba
262263
}
263264

264265
#endregion
266+
267+
#region DPAPI
268+
269+
/// <summary>
270+
/// Encrypts a Refresh Token using the DPAPI Protect method with current user scope
271+
/// </summary>
272+
/// <param name="refreshToken">The Refresh Token to encrypt</param>
273+
/// <returns>The Base64-encoded encrypted refresh token or NULL if encryption fails</returns>
274+
public static string Protect(string refreshToken)
275+
{
276+
// Token should be plain ASCII text, get raw byte data for encoding
277+
var rawToken = Encoding.ASCII.GetBytes(refreshToken);
278+
279+
try
280+
{
281+
// Encrypt using DPAPI with user scope, can only be decrypted by currently logged-in user
282+
var rawEncryptedToken = ProtectedData.Protect(rawToken, null, DataProtectionScope.CurrentUser);
283+
284+
// Base64-encode encrypted token for JSON string compatibility
285+
var encryptedToken = Convert.ToBase64String(rawEncryptedToken);
286+
287+
return encryptedToken;
288+
}
289+
catch (Exception)
290+
{
291+
// If encryption fails, lose the token
292+
return null;
293+
}
294+
}
295+
296+
/// <summary>
297+
/// Decrypt a Refresh Token using the DPAPI Protect method with current user scope
298+
/// </summary>
299+
/// <param name="encryptedRefreshToken">The encrypted Refresh Token to decrypt, Base64-encoded</param>
300+
/// <returns>The decrypted refresh token or NULL if decryption fails</returns>
301+
public static string Unprotect(string encryptedRefreshToken)
302+
{
303+
// Decode Base64-encoded encrypted data
304+
var rawEncryptedToken = Convert.FromBase64String(encryptedRefreshToken);
305+
306+
try
307+
{
308+
// Decrypt using DPAPI with user scope, only possible if the currently logged-in user is the same as the one who encrypted it
309+
var rawToken = ProtectedData.Unprotect(rawEncryptedToken, null, DataProtectionScope.CurrentUser);
310+
311+
// Get string data from byte array
312+
var token = Encoding.ASCII.GetString(rawToken);
313+
314+
return token;
315+
}
316+
catch (Exception)
317+
{
318+
// If decryption fails, lose the token
319+
return null;
320+
}
321+
}
322+
323+
#endregion
265324
}
266325
}

0 commit comments

Comments
 (0)