Skip to content

Commit

Permalink
Merge pull request #89 from klimkjar/84-feature-dpapi-token-storage
Browse files Browse the repository at this point in the history
POC for DPAPI-protected storage of refresh tokens in config file
  • Loading branch information
KoenZomers authored Jun 2, 2019
2 parents 8affbb2 + 44530ad commit 4df8c54
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 4 deletions.
18 changes: 16 additions & 2 deletions KoenZomers.KeePass.OneDriveSync/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,13 @@ public static void Load()
{
windowsCredentialManagerDatabaseConfig.Value.RefreshToken = Utilities.GetRefreshTokenFromWindowsCredentialManager(windowsCredentialManagerDatabaseConfig.Key);
}

// Decrypt all database configurations which have their OneDrive Refresh Token stored encrypted in the config file
var encryptedDatabaseConfigs = PasswordDatabases.Where(pwdDb => pwdDb.Value.RefreshTokenStorage == Enums.OneDriveRefreshTokenStorage.DiskEncrypted);
foreach (var encryptedDatabaseConfig in encryptedDatabaseConfigs)
{
encryptedDatabaseConfig.Value.RefreshToken = Utilities.Unprotect(encryptedDatabaseConfig.Value.RefreshToken);
}
}

/// <summary>
Expand All @@ -200,8 +207,14 @@ public static void Save()
switch (passwordDatabase.Value.RefreshTokenStorage)
{
case Enums.OneDriveRefreshTokenStorage.Disk:
// Refresh token will be stored on disk, we can store the complete configuration instance on disk in this case
passwordDatabasesForStoring.Add(passwordDatabase.Key, passwordDatabase.Value);
case Enums.OneDriveRefreshTokenStorage.DiskEncrypted:
// Enforce encryption of tokens previously stored in plain text
passwordDatabase.Value.RefreshTokenStorage = Enums.OneDriveRefreshTokenStorage.DiskEncrypted;

// Refresh token will be stored encrypted on disk, we create a copy of the configuration and encrypt the refresh token
var diskConfiguration = (Configuration)passwordDatabase.Value.Clone();
diskConfiguration.RefreshToken = Utilities.Protect(diskConfiguration.RefreshToken);
passwordDatabasesForStoring.Add(passwordDatabase.Key, diskConfiguration);
break;

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ public enum OneDriveRefreshTokenStorage : short
/// <summary>
/// Saves the RefreshToken in the encrypted KeePass database
/// </summary>
KeePassDatabase = 2
KeePassDatabase = 2,

/// <summary>
/// Saves the RefreshToken encrypted with DPAPI in KeePass.config.xml located in %APPDATA%\KeePass
/// </summary>
DiskEncrypted = 3,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ private void OneDriveRefreshTokenStorageDialog_FormClosing(object sender, FormCl
}
if (OnDiskRadio.Checked)
{
_configuration.RefreshTokenStorage = OneDriveRefreshTokenStorage.Disk;
_configuration.RefreshTokenStorage = OneDriveRefreshTokenStorage.DiskEncrypted;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
<Private>True</Private>
</Reference>
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.Security" />
<Reference Include="System.ServiceModel" />
<Reference Include="System.Web" />
<Reference Include="System.Web.Extensions" />
Expand Down
59 changes: 59 additions & 0 deletions KoenZomers.KeePass.OneDriveSync/Utilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using CredentialManagement;
using KeePassLib;
Expand Down Expand Up @@ -262,5 +263,63 @@ public static string GetRefreshTokenFromKeePassDatabase(PwDatabase keePassDataba
}

#endregion

#region DPAPI

/// <summary>
/// Encrypts a Refresh Token using the DPAPI Protect method with current user scope
/// </summary>
/// <param name="refreshToken">The Refresh Token to encrypt</param>
/// <returns>The Base64-encoded encrypted refresh token or NULL if encryption fails</returns>
public static string Protect(string refreshToken)
{
// Token should be plain ASCII text, get raw byte data for encoding
var rawToken = Encoding.ASCII.GetBytes(refreshToken);

try
{
// Encrypt using DPAPI with user scope, can only be decrypted by currently logged-in user
var rawEncryptedToken = ProtectedData.Protect(rawToken, null, DataProtectionScope.CurrentUser);

// Base64-encode encrypted token for JSON string compatibility
var encryptedToken = Convert.ToBase64String(rawEncryptedToken);

return encryptedToken;
}
catch (Exception)
{
// If encryption fails, lose the token
return null;
}
}

/// <summary>
/// Decrypt a Refresh Token using the DPAPI Protect method with current user scope
/// </summary>
/// <param name="encryptedRefreshToken">The encrypted Refresh Token to decrypt, Base64-encoded</param>
/// <returns>The decrypted refresh token or NULL if decryption fails</returns>
public static string Unprotect(string encryptedRefreshToken)
{
// Decode Base64-encoded encrypted data
var rawEncryptedToken = Convert.FromBase64String(encryptedRefreshToken);

try
{
// Decrypt using DPAPI with user scope, only possible if the currently logged-in user is the same as the one who encrypted it
var rawToken = ProtectedData.Unprotect(rawEncryptedToken, null, DataProtectionScope.CurrentUser);

// Get string data from byte array
var token = Encoding.ASCII.GetString(rawToken);

return token;
}
catch (Exception)
{
// If decryption fails, lose the token
return null;
}
}

#endregion
}
}

0 comments on commit 4df8c54

Please sign in to comment.