Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Optional encryption of settings.json to prevent exposing password as cleartext. #2635

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
100 changes: 96 additions & 4 deletions src/Libraries/Nop.Core/Data/DataSettings.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Reflection;

namespace Nop.Core.Data
{
Expand All @@ -8,12 +10,14 @@ namespace Nop.Core.Data
/// </summary>
public partial class DataSettings
{
protected ProtectedDataService ProtectedDataService = new ProtectedDataService();

/// <summary>
/// Ctor
/// </summary>
public DataSettings()
{
RawDataSettings=new Dictionary<string, string>();
RawDataSettings = new Dictionary<string, string>();
}

/// <summary>
Expand All @@ -26,6 +30,11 @@ public DataSettings()
/// </summary>
public string DataConnectionString { get; set; }

/// <summary>
/// Should DataConnectionString be encrypted
/// </summary>
public bool EncryptStringSettings { get; set; }

/// <summary>
/// Raw settings file
/// </summary>
Expand All @@ -37,7 +46,90 @@ public DataSettings()
/// <returns></returns>
public bool IsValid()
{
return !string.IsNullOrEmpty(this.DataProvider) && !string.IsNullOrEmpty(this.DataConnectionString);
return !String.IsNullOrEmpty(this.DataProvider) && !String.IsNullOrEmpty(this.DataConnectionString);
}

/// <summary>
/// Returns true if any STRING properties are in a decrypted state as determined by ProtectionDataService
/// </summary>
/// <returns></returns>
public bool HasAnyDecryptedDataSettings()
{
bool retVal = false;
PropertyInfo[] properties = this.GetType().GetProperties();

foreach (var property in properties)
{
if (property.PropertyType == typeof(string) && property.GetValue(this) != null && !ProtectedDataService.IsCipherText(property.GetValue(this).ToString()))
{
retVal = true;
}
}
return retVal;
}

/// <summary>
/// Returns true if any STRING properties are in an encrypted state as determined by ProtectionDataService
/// </summary>
/// <returns></returns>
public bool HasAnyEncryptedDataSettings()
{
bool retVal = false;
PropertyInfo[] properties = this.GetType().GetProperties();

foreach (var property in properties)
{
if (property.PropertyType == typeof(string) && property.GetValue(this) != null && ProtectedDataService.IsCipherText(property.GetValue(this).ToString()))
{
retVal = true;
}
}
return retVal;
}

/// <summary>
/// Decrypts all STRING PROPERTIES. If already decrypted then they are unchanged.
/// </summary>
/// <returns></returns>
public void Decrypt()
{
PropertyInfo[] properties = this.GetType().GetProperties();

foreach (var property in properties)
{
if (property.PropertyType == typeof(string) && property.GetValue(this) != null && ProtectedDataService.IsCipherText(property.GetValue(this).ToString()))
{
string clearText = ProtectedDataService.GetClearText(property.GetValue(this).ToString());
property.SetValue(this, clearText);
}
}
}

/// <summary>
/// Encrypts all STRING PROPERTIES. If already encrypted then they are unchanged.
/// </summary>
/// <returns></returns>
public void Encrypt()
{
PropertyInfo[] properties = this.GetType().GetProperties();

foreach (var property in properties)
{
if (property.PropertyType == typeof(string) && property.GetValue(this) != null && !ProtectedDataService.IsCipherText(property.GetValue(this).ToString()))
{
string cipherText = ProtectedDataService.GetCipherText(this.ProtectedDataService.GetCipherText(property.GetValue(this).ToString()));
property.SetValue(this, cipherText);
}
}
}

/// <summary>
///
/// </summary>
/// <returns></returns>
public DataSettings GetClone()
{
return JsonConvert.DeserializeObject<DataSettings>(JsonConvert.SerializeObject(this, Formatting.None));
}
}
}
}
134 changes: 90 additions & 44 deletions src/Libraries/Nop.Core/Data/DataSettingsManager.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System;
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json;
using Nop.Core.Infrastructure;
using System;
using System.IO;

namespace Nop.Core.Data
{
Expand All @@ -11,23 +11,18 @@ namespace Nop.Core.Data
public partial class DataSettingsManager
{
#region Const

private const string ObsoleteDataSettingsFilePath = "~/App_Data/Settings.txt";
private const string DataSettingsFilePath_ = "~/App_Data/dataSettings.json";

#endregion
#endregion Const

#region Properties

/// <summary>
/// Gets the path to file that contains data settings
/// </summary>
public static string DataSettingsFilePath => DataSettingsFilePath_;

#endregion
#endregion Properties

#region Methods

/// <summary>
/// Load settings
/// </summary>
Expand All @@ -50,41 +45,17 @@ public virtual DataSettings LoadSettings(string filePath = null, bool reloadSett
return new DataSettings();

//get data settings from the old txt file
var dataSettings = new DataSettings();
using (var reader = new StringReader(File.ReadAllText(filePath)))
{
var settingsLine = string.Empty;
while ((settingsLine = reader.ReadLine()) != null)
{
var separatorIndex = settingsLine.IndexOf(':');
if (separatorIndex == -1)
continue;

var key = settingsLine.Substring(0, separatorIndex).Trim();
var value = settingsLine.Substring(separatorIndex + 1).Trim();

switch (key)
{
case "DataProvider":
dataSettings.DataProvider = value;
continue;
case "DataConnectionString":
dataSettings.DataConnectionString = value;
continue;
default:
dataSettings.RawDataSettings.Add(key, value);
continue;
}
}
}
var dataSettings = GetSettingsFromLegacyTextFile(filePath);

//save data settings to the new file
SaveSettings(dataSettings);

//and delete the old one
File.Delete(filePath);

Singleton<DataSettings>.Instance = dataSettings;
var usableDataSettings = dataSettings.GetClone();
usableDataSettings.Decrypt();
Singleton<DataSettings>.Instance = usableDataSettings;
return Singleton<DataSettings>.Instance;
}

Expand All @@ -93,7 +64,13 @@ public virtual DataSettings LoadSettings(string filePath = null, bool reloadSett
return new DataSettings();

//get data settings from the JSON file
Singleton<DataSettings>.Instance = JsonConvert.DeserializeObject<DataSettings>(text);
var candidateDataSettings = JsonConvert.DeserializeObject<DataSettings>(text);
candidateDataSettings.Decrypt();
Singleton<DataSettings>.Instance = candidateDataSettings;

//Handles resaving the file if the encryption setting has been changed by the user via editing the file
ResaveSettingsWhenEncryptionStateMismatch();

return Singleton<DataSettings>.Instance;
}

Expand All @@ -104,7 +81,65 @@ public virtual DataSettings LoadSettings(string filePath = null, bool reloadSett
public virtual void SaveSettings(DataSettings settings)
{
Singleton<DataSettings>.Instance = settings ?? throw new ArgumentNullException(nameof(settings));
var clonedSettings = settings.GetClone();
if (settings.EncryptStringSettings)
{
clonedSettings.Encrypt();
}
else
{
clonedSettings.Decrypt();
}
string text = JsonConvert.SerializeObject(clonedSettings, Formatting.Indented);
SaveFile(text); //encrypt settings file after saving.
}

/// <summary>
/// Attempts to load settings from old-style settings.txt file
/// </summary>
/// <param name="filePath">Path to settings.txt</param>
/// <returns>Returns a data settings object</returns>
protected DataSettings GetSettingsFromLegacyTextFile(string filePath)
{
var dataSettings = new DataSettings();
using (var reader = new StringReader(File.ReadAllText(filePath)))
{
var settingsLine = string.Empty;
while ((settingsLine = reader.ReadLine()) != null)
{
var separatorIndex = settingsLine.IndexOf(':');
if (separatorIndex == -1)
continue;

var key = settingsLine.Substring(0, separatorIndex).Trim();
var value = settingsLine.Substring(separatorIndex + 1).Trim();

switch (key)
{
case "DataProvider":
dataSettings.DataProvider = value;
continue;
case "DataConnectionString":
dataSettings.DataConnectionString = value;
continue;
case "EncryptStringSettings":
dataSettings.EncryptStringSettings = Convert.ToBoolean(value);
continue;
default:
dataSettings.RawDataSettings.Add(key, value);
continue;
}
}
}
return dataSettings;
}

/// <summary>
/// Saves the value of the text param to the settings file.
/// </summary>
/// <param name="text">value of all text to be saved to the settings file.</param>
protected void SaveFile(string text)
{
var filePath = CommonHelper.MapPath(DataSettingsFilePath);

//create file if not exists
Expand All @@ -113,12 +148,23 @@ public virtual void SaveSettings(DataSettings settings)
//we use 'using' to close the file after it's created
using (File.Create(filePath)) { }
}

//save data settings to the file
var text = JsonConvert.SerializeObject(Singleton<DataSettings>.Instance, Formatting.Indented);
File.WriteAllText(filePath, text);
}

#endregion
/// <summary>
/// Will call SaveSettings() as needed when the EncryptStringSettings method does not match the state of the current datasettings object. Used when the user manually edits the value of EncryptStringSettings in the settings file.
/// </summary>
private void ResaveSettingsWhenEncryptionStateMismatch()
{
if (Singleton<DataSettings>.Instance.EncryptStringSettings && Singleton<DataSettings>.Instance.HasAnyDecryptedDataSettings()) //If it's not encrypted and should be, then encrypt it.
{
SaveSettings(Singleton<DataSettings>.Instance);
}
else if (!Singleton<DataSettings>.Instance.EncryptStringSettings && !Singleton<DataSettings>.Instance.HasAnyEncryptedDataSettings()) //If it's encrypted and it should not be, then decrypt it.
{
SaveSettings(Singleton<DataSettings>.Instance);
}
}
#endregion Methods
}
}
}