Adding IEncryptionServices

Implements symetric encoding/decoding services based on a per-tenant key generated randomly during setup in the ShellSettings.
Replaces MachineKey.Encode/Decode usages.
Adding ComputedField to wrap get/set calls from parts, making the Smtp password encrypted in the db automatically.

--HG--
branch : dev
This commit is contained in:
Sebastien Ros 2010-12-03 16:14:17 -08:00
parent 7b4025b8cb
commit fadcc4ef6e
20 changed files with 315 additions and 53 deletions

View File

@ -154,6 +154,7 @@
<Compile Include="Themes\Services\ThemeServiceTests.cs" />
<Compile Include="Users\Controllers\AccountControllerTests.cs" />
<Compile Include="Users\Services\UserServiceTests.cs" />
<Compile Include="Users\ShellSettingsUtility.cs" />
<Compile Include="Values.cs" />
<Compile Include="Users\Controllers\AdminControllerTests.cs" />
<Compile Include="Users\Services\MembershipServiceTests.cs" />

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
@ -26,20 +27,19 @@ using Orchard.Messaging.Events;
using Orchard.Messaging.Services;
using Orchard.Security;
using Orchard.Security.Permissions;
using Orchard.Security.Providers;
using Orchard.Tests.Stubs;
using Orchard.UI.Notify;
using Orchard.Users.Controllers;
using Orchard.Users.Handlers;
using Orchard.Users.Models;
using Orchard.Users.Services;
using Orchard.Users.ViewModels;
using Orchard.Settings;
using Orchard.Core.Settings.Services;
using Orchard.Tests.Messaging;
using Orchard.Environment.Configuration;
using Orchard.Core.Settings.Models;
using Orchard.Core.Settings.Handlers;
using Orchard.Messaging.Models;
using System.Collections.Specialized;
namespace Orchard.Tests.Modules.Users.Controllers {
@ -74,11 +74,14 @@ namespace Orchard.Tests.Modules.Users.Controllers {
builder.RegisterType<StubExtensionManager>().As<IExtensionManager>();
builder.RegisterType<SiteSettingsPartHandler>().As<IContentHandler>();
builder.RegisterType<RegistrationSettingsPartHandler>().As<IContentHandler>();
builder.RegisterInstance(new Mock<INotifier>().Object);
builder.RegisterInstance(new Mock<IContentDisplay>().Object);
builder.RegisterType<StubCacheManager>().As<ICacheManager>();
builder.RegisterType<Signals>().As<ISignals>();
builder.RegisterInstance(new ShellSettings { Name = "Alpha", RequestUrlHost = "wiki.example.com", RequestUrlPrefix = "~/foo" });
builder.RegisterType<DefaultEncryptionService>().As<IEncryptionService>();
builder.RegisterInstance(ShellSettingsUtility.CreateEncryptionEnabled());
_authorizer = new Mock<IAuthorizer>();
builder.RegisterInstance(_authorizer.Object);

View File

@ -26,6 +26,7 @@ using Orchard.Messaging.Events;
using Orchard.Messaging.Services;
using Orchard.Security;
using Orchard.Security.Permissions;
using Orchard.Security.Providers;
using Orchard.Tests.Stubs;
using Orchard.UI.Notify;
using Orchard.Users.Controllers;
@ -69,7 +70,8 @@ namespace Orchard.Tests.Modules.Users.Controllers {
builder.RegisterInstance(new Mock<IContentDisplay>().Object);
builder.RegisterType<StubCacheManager>().As<ICacheManager>();
builder.RegisterType<Signals>().As<ISignals>();
builder.RegisterInstance(new ShellSettings { Name = "Alpha", RequestUrlHost = "wiki.example.com", RequestUrlPrefix = "~/foo" });
builder.RegisterType<DefaultEncryptionService>().As<IEncryptionService>();
builder.RegisterInstance(ShellSettingsUtility.CreateEncryptionEnabled());
_authorizer = new Mock<IAuthorizer>();
builder.RegisterInstance(_authorizer.Object);

View File

@ -1,5 +1,4 @@
using System;
using System.Web.Security;
using System.Xml.Linq;
using Autofac;
using Moq;
@ -21,6 +20,7 @@ using Orchard.Environment.Extensions;
using Orchard.Messaging.Events;
using Orchard.Messaging.Services;
using Orchard.Security;
using Orchard.Security.Providers;
using Orchard.Tests.Stubs;
using Orchard.Tests.Utility;
using Orchard.Users.Handlers;
@ -96,7 +96,9 @@ namespace Orchard.Tests.Modules.Users.Services {
builder.RegisterType<DefaultShapeFactory>().As<IShapeFactory>();
builder.RegisterType<StubExtensionManager>().As<IExtensionManager>();
builder.RegisterType<DefaultContentDisplay>().As<IContentDisplay>();
builder.RegisterInstance(new ShellSettings { Name = "Alpha", RequestUrlHost = "wiki.example.com", RequestUrlPrefix = "~/foo" });
builder.RegisterType<DefaultEncryptionService>().As<IEncryptionService>();
builder.RegisterInstance(ShellSettingsUtility.CreateEncryptionEnabled());
_session = _sessionFactory.OpenSession();
builder.RegisterInstance(new TestSessionLocator(_session)).As<ISessionLocator>();
@ -121,25 +123,5 @@ namespace Orchard.Tests.Modules.Users.Services {
Assert.That(username, Is.EqualTo("foo"));
Assert.That(validateByUtc, Is.GreaterThan(_clock.UtcNow));
}
[Test]
public void NonceShouldNotBeUsedOnAnotherTenant() {
var user = _membershipService.CreateUser(new CreateUserParams("foo", "66554321", "foo@bar.com", "", "", true));
var nonce = _userService.CreateNonce(user, new TimeSpan(1, 0, 0));
Assert.That(nonce, Is.Not.Empty);
string username;
DateTime validateByUtc;
_container.Resolve<ShellSettings>().Name = "Beta";
var result = _userService.DecryptNonce(nonce, out username, out validateByUtc);
Assert.That(result, Is.False);
Assert.That(username, Is.EqualTo("foo"));
Assert.That(validateByUtc, Is.GreaterThan(_clock.UtcNow));
}
}
}

View File

@ -0,0 +1,27 @@
using System;
using System.Security.Cryptography;
using Orchard.Environment.Configuration;
using Orchard.Utility.Extensions;
namespace Orchard.Tests.Modules.Users {
public class ShellSettingsUtility {
public static ShellSettings CreateEncryptionEnabled() {
// generate random keys for encryption
var key = new byte[32];
var iv = new byte[16];
using ( var random = new RNGCryptoServiceProvider() ) {
random.GetBytes(key);
random.GetBytes(iv);
}
return new ShellSettings {
Name = "Alpha",
RequestUrlHost = "wiki.example.com",
RequestUrlPrefix = "~/foo",
EncryptionAlgorithm = "AES",
EncryptionKey = key.ToHexString(),
EncryptionIV = iv.ToHexString()
};
}
}
}

View File

@ -58,7 +58,7 @@ namespace Orchard.Tests.Environment.Configuration {
_appDataFolder.CreateFile("Sites\\Default\\Settings.txt", "Name: Default\r\nDataProvider: SqlCe\r\nDataConnectionString: something else");
IShellSettingsManager loader = new ShellSettingsManager(_appDataFolder, new Mock<IShellSettingsManagerEventHandler>().Object);
var foo = new ShellSettings { Name = "Foo", DataProvider = "Bar", DataConnectionString = "Quux" };
var foo = new ShellSettings {Name = "Foo", DataProvider = "Bar", DataConnectionString = "Quux"};
Assert.That(loader.LoadSettings().Count(), Is.EqualTo(1));
loader.SaveSettings(foo);
@ -69,5 +69,19 @@ namespace Orchard.Tests.Environment.Configuration {
Assert.That(text, Is.StringContaining("Bar"));
Assert.That(text, Is.StringContaining("Quux"));
}
[Test]
public void EncryptionSettingsAreStoredAndReadable() {
IShellSettingsManager loader = new ShellSettingsManager(_appDataFolder, new Mock<IShellSettingsManagerEventHandler>().Object);
var foo = new ShellSettings { Name = "Foo", DataProvider = "Bar", DataConnectionString = "Quux", EncryptionAlgorithm = "AES", EncryptionKey = "ABCDEFG", EncryptionIV= "HIJKL" };
loader.SaveSettings(foo);
Assert.That(loader.LoadSettings().Count(), Is.EqualTo(1));
var settings = loader.LoadSettings().First();
Assert.That(settings.EncryptionAlgorithm, Is.EqualTo("AES"));
Assert.That(settings.EncryptionKey, Is.EqualTo("ABCDEFG"));
Assert.That(settings.EncryptionIV, Is.EqualTo("HIJKL"));
}
}
}

View File

@ -245,6 +245,7 @@
<Compile Include="Mvc\Routes\ShellRouteTests.cs" />
<Compile Include="Mvc\Routes\UrlPrefixTests.cs" />
<Compile Include="Records\BigRecord.cs" />
<Compile Include="Security\DefaultEncryptionServiceTests.cs" />
<Compile Include="Stubs\InMemoryWebSiteFolder.cs" />
<Compile Include="Stubs\StubHttpContextAccessor.cs" />
<Compile Include="Stubs\StubWorkContextAccessor.cs" />

View File

@ -0,0 +1,53 @@
using System.Security.Cryptography;
using System.Text;
using Autofac;
using NUnit.Framework;
using Orchard.Environment.Configuration;
using Orchard.Security;
using Orchard.Security.Providers;
using Orchard.Utility.Extensions;
namespace Orchard.Tests.Security {
[TestFixture]
public class DefaultEncryptionServiceTests {
private IContainer container;
[SetUp]
public void Init() {
var key = new byte[32];
var iv = new byte[16];
using ( var random = new RNGCryptoServiceProvider() ) {
random.GetBytes(key);
random.GetBytes(iv);
}
var shellSettings = new ShellSettings {
Name = "Foo",
DataProvider = "Bar",
DataConnectionString = "Quux",
EncryptionAlgorithm = "AES",
EncryptionKey = key.ToHexString(),
EncryptionIV = iv.ToHexString()
};
var builder = new ContainerBuilder();
builder.RegisterInstance(shellSettings);
builder.RegisterType<DefaultEncryptionService>().As<IEncryptionService>();
container = builder.Build();
}
[Test]
public void CanEncodeAndDecodeData() {
var encryptionService = container.Resolve<IEncryptionService>();
var secretData = Encoding.Unicode.GetBytes("this is secret data");
var encrypted = encryptionService.Encode(secretData);
var decrypted = encryptionService.Decode(encrypted);
Assert.That(encrypted, Is.Not.EqualTo(decrypted));
Assert.That(decrypted, Is.EqualTo(secretData));
}
}
}

View File

@ -6,10 +6,8 @@ using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
using Orchard.ContentManagement.MetaData;
using Orchard.ContentManagement.MetaData.Models;
using Orchard.ContentTypes.Extensions;
using Orchard.ContentTypes.ViewModels;
using Orchard.Core.Contents.Extensions;
using Orchard.Core.Contents.Settings;
using Orchard.Localization;
namespace Orchard.ContentTypes.Services {

View File

@ -1,14 +1,27 @@
using JetBrains.Annotations;
using System;
using System.Text;
using JetBrains.Annotations;
using Orchard.Email.Models;
using Orchard.Data;
using Orchard.ContentManagement.Handlers;
using Orchard.Security;
namespace Orchard.Email.Handlers {
[UsedImplicitly]
public class SmtpSettingsPartHandler : ContentHandler {
public SmtpSettingsPartHandler(IRepository<SmtpSettingsPartRecord> repository) {
private readonly IEncryptionService _encryptionService;
public SmtpSettingsPartHandler(IRepository<SmtpSettingsPartRecord> repository, IEncryptionService encryptionService) {
_encryptionService = encryptionService;
Filters.Add(new ActivatingFilter<SmtpSettingsPart>("Site"));
Filters.Add(StorageFilter.For(repository));
OnLoaded<SmtpSettingsPart>(LazyLoadHandlers);
}
void LazyLoadHandlers(LoadContentContext context, SmtpSettingsPart part) {
part.PasswordField.Getter(() => String.IsNullOrWhiteSpace(part.Record.Password) ? String.Empty : Encoding.UTF8.GetString(_encryptionService.Decode(Convert.FromBase64String(part.Record.Password))));
part.PasswordField.Setter(value => part.Record.Password = String.IsNullOrWhiteSpace(value) ? String.Empty : Convert.ToBase64String(_encryptionService.Encode(Encoding.UTF8.GetBytes(value))));
}
}
}

View File

@ -1,14 +1,13 @@
using System.Text;
using System.Web.Security;
using Orchard.ContentManagement;
using Orchard.ContentManagement;
using System;
using Orchard.ContentManagement.Utilities;
namespace Orchard.Email.Models {
public class SmtpSettingsPart : ContentPart<SmtpSettingsPartRecord> {
public bool IsValid() {
return !String.IsNullOrWhiteSpace(Record.Host)
&& Record.Port > 0
&& !String.IsNullOrWhiteSpace(Record.Address);
private readonly ComputedField<string> _password = new ComputedField<string>();
public ComputedField<string> PasswordField {
get { return _password; }
}
public string Address {
@ -42,8 +41,14 @@ namespace Orchard.Email.Models {
}
public string Password {
get { return String.IsNullOrWhiteSpace(Record.Password) ? String.Empty : Encoding.UTF8.GetString(MachineKey.Decode(Record.Password, MachineKeyProtection.All)); ; }
set { Record.Password = String.IsNullOrWhiteSpace(value) ? String.Empty : MachineKey.Encode(Encoding.UTF8.GetBytes(value), MachineKeyProtection.All); }
get { return _password.Value; }
set { _password.Value = value; }
}
public bool IsValid() {
return !String.IsNullOrWhiteSpace(Record.Host)
&& Record.Port > 0
&& !String.IsNullOrWhiteSpace(Record.Address);
}
}
}

View File

@ -1,6 +1,7 @@
using System;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
using System.Web;
using Orchard.ContentManagement;
using Orchard.ContentManagement.MetaData;
@ -29,6 +30,7 @@ using Orchard.Settings;
using Orchard.Environment.State;
using Orchard.Data.Migration;
using Orchard.Themes.Services;
using Orchard.Utility.Extensions;
using Orchard.Widgets.Models;
using Orchard.Widgets;
@ -118,6 +120,21 @@ namespace Orchard.Setup.Services {
shellSettings.DataTablePrefix = context.DatabaseTablePrefix;
}
#region Encryption Settings
// generate random keys for encryption
var key = new byte[32];
var iv = new byte[16];
using ( var random = new RNGCryptoServiceProvider() ) {
random.GetBytes(key);
random.GetBytes(iv);
}
shellSettings.EncryptionAlgorithm = "AES";
shellSettings.EncryptionKey = key.ToHexString();
shellSettings.EncryptionIV = iv.ToHexString();
#endregion
var shellDescriptor = new ShellDescriptor {
Features = context.EnabledFeatures.Select(name => new ShellFeature { Name = name })
};

View File

@ -25,13 +25,15 @@ namespace Orchard.Users.Services {
private readonly IClock _clock;
private readonly IMessageManager _messageManager;
private readonly ShellSettings _shellSettings;
private readonly IEncryptionService _encryptionService;
public UserService(IContentManager contentManager, IMembershipService membershipService, IClock clock, IMessageManager messageManager, ShellSettings shellSettings) {
public UserService(IContentManager contentManager, IMembershipService membershipService, IClock clock, IMessageManager messageManager, ShellSettings shellSettings, IEncryptionService encryptionService) {
_contentManager = contentManager;
_membershipService = membershipService;
_clock = clock;
_messageManager = messageManager;
_shellSettings = shellSettings;
_encryptionService = encryptionService;
Logger = NullLogger.Instance;
}
@ -66,24 +68,22 @@ namespace Orchard.Users.Services {
}
public string CreateNonce(IUser user, TimeSpan delay) {
// the tenant's name is added to the token to prevent cross-tenant requests
var challengeToken = new XElement("n", new XAttribute("s", _shellSettings.Name), new XAttribute("un", user.UserName), new XAttribute("utc", _clock.UtcNow.ToUniversalTime().Add(delay).ToString(CultureInfo.InvariantCulture))).ToString();
var data = Encoding.Unicode.GetBytes(challengeToken);
return MachineKey.Encode(data, MachineKeyProtection.All);
var challengeToken = new XElement("n", new XAttribute("un", user.UserName), new XAttribute("utc", _clock.UtcNow.ToUniversalTime().Add(delay).ToString(CultureInfo.InvariantCulture))).ToString();
var data = Encoding.UTF8.GetBytes(challengeToken);
return Convert.ToBase64String(_encryptionService.Encode(data));
}
public bool DecryptNonce(string challengeToken, out string username, out DateTime validateByUtc) {
public bool DecryptNonce(string nonce, out string username, out DateTime validateByUtc) {
username = null;
validateByUtc = _clock.UtcNow;
try {
var data = MachineKey.Decode(challengeToken, MachineKeyProtection.All);
var xml = Encoding.Unicode.GetString(data);
var data = _encryptionService.Decode(Convert.FromBase64String(nonce));
var xml = Encoding.UTF8.GetString(data);
var element = XElement.Parse(xml);
var tenant = element.Attribute("s").Value;
username = element.Attribute("un").Value;
validateByUtc = DateTime.Parse(element.Attribute("utc").Value, CultureInfo.InvariantCulture);
return String.Equals(_shellSettings.Name, tenant, StringComparison.Ordinal) && _clock.UtcNow <= validateByUtc;
return _clock.UtcNow <= validateByUtc;
}
catch {
return false;

View File

@ -0,0 +1,29 @@
using System;
namespace Orchard.ContentManagement.Utilities {
public class ComputedField<T> {
private Func<T> _getter;
private Action<T> _setter;
public T Value {
get { return GetValue(); }
set { SetValue(value); }
}
public void Getter(Func<T> loader) {
_getter = loader;
}
public void Setter(Action<T> setter) {
_setter = setter;
}
private T GetValue() {
return _getter();
}
private void SetValue(T value) {
_setter(value);
}
}
}

View File

@ -16,6 +16,9 @@
DataTablePrefix = settings.DataTablePrefix;
RequestUrlHost = settings.RequestUrlHost;
RequestUrlPrefix = settings.RequestUrlPrefix;
EncryptionAlgorithm = settings.EncryptionAlgorithm;
EncryptionKey = settings.EncryptionKey;
EncryptionIV = settings.EncryptionIV;
State = settings.State;
}
@ -28,6 +31,10 @@
public string RequestUrlHost { get; set; }
public string RequestUrlPrefix { get; set; }
public string EncryptionAlgorithm { get; set; }
public string EncryptionKey { get; set; }
public string EncryptionIV { get; set; }
public TenantState State { get; set; }
}
}

View File

@ -84,6 +84,15 @@ namespace Orchard.Environment.Configuration {
case "RequestUrlPrefix":
shellSettings.RequestUrlPrefix = settingFields[1];
break;
case "EncryptionAlgorithm":
shellSettings.EncryptionAlgorithm = settingFields[1];
break;
case "EncryptionKey":
shellSettings.EncryptionKey = settingFields[1];
break;
case "EncryptionIV":
shellSettings.EncryptionIV = settingFields[1];
break;
}
}
}
@ -94,14 +103,18 @@ namespace Orchard.Environment.Configuration {
if (settings == null)
return "";
return string.Format("Name: {0}\r\nDataProvider: {1}\r\nDataConnectionString: {2}\r\nDataPrefix: {3}\r\nRequestUrlHost: {4}\r\nRequestUrlPrefix: {5}\r\nState: {6}\r\n",
return string.Format("Name: {0}\r\nDataProvider: {1}\r\nDataConnectionString: {2}\r\nDataPrefix: {3}\r\nRequestUrlHost: {4}\r\nRequestUrlPrefix: {5}\r\nState: {6}\r\nEncryptionAlgorithm: {7}\r\nEncryptionKey: {8}\r\nEncryptionIV: {9}\r\n",
settings.Name,
settings.DataProvider,
settings.DataConnectionString ?? "null",
settings.DataTablePrefix ?? "null",
settings.RequestUrlHost ?? "null",
settings.RequestUrlPrefix ?? "null",
settings.State != null ? settings.State.ToString() : String.Empty);
settings.State != null ? settings.State.ToString() : String.Empty,
settings.EncryptionAlgorithm ?? "null",
settings.EncryptionKey ?? "null",
settings.EncryptionIV ?? "null"
);
}
}
}

View File

@ -142,6 +142,7 @@
<Compile Include="ContentManagement\DefaultContentDisplay.cs" />
<Compile Include="ContentManagement\Drivers\ContentShapeResult.cs" />
<Compile Include="ContentManagement\Handlers\BuildShapeContext.cs" />
<Compile Include="ContentManagement\Utilities\ComputedField.cs" />
<Compile Include="DisplayManagement\Descriptors\ResourceBindingStrategy\StylesheetBindingStrategy.cs" />
<Compile Include="DisplayManagement\Descriptors\ShapeDescriptor.cs" />
<Compile Include="DisplayManagement\Descriptors\ShapeAlteration.cs" />
@ -171,7 +172,9 @@
<Compile Include="Mvc\IOrchardViewPage.cs" />
<Compile Include="Mvc\Spooling\HtmlStringWriter.cs" />
<Compile Include="Mvc\ViewEngines\Razor\IRazorCompilationEvents.cs" />
<Compile Include="Security\IEncryptionService.cs" />
<Compile Include="Security\CurrentUserWorkContext.cs" />
<Compile Include="Security\Providers\DefaultEncryptionService.cs" />
<Compile Include="Settings\CurrentSiteWorkContext.cs" />
<Compile Include="Settings\ResourceDebugMode.cs" />
<Compile Include="Themes\ThemeManager.cs" />

View File

@ -0,0 +1,20 @@
namespace Orchard.Security {
/// <summary>
/// Provides encryption services adapted to securing tenant level information
/// </summary>
public interface IEncryptionService : ISingletonDependency {
/// <summary>
/// Decodes data that has been encrypted.
/// </summary>
/// <param name="encodedData">The encrypted data to decrypt.</param>
/// <returns>A Byte[] array that represents the decrypted data.</returns>
byte[] Decode(byte[] encodedData);
/// <summary>
/// Encrypts data.
/// </summary>
/// <param name="data">The data to encrypt.</param>
/// <returns>The encrypted value.</returns>
byte[] Encode(byte[] data);
}
}

View File

@ -0,0 +1,62 @@
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using Orchard.Environment.Configuration;
using Orchard.Utility.Extensions;
namespace Orchard.Security.Providers {
public class DefaultEncryptionService : IEncryptionService {
private readonly ShellSettings _shellSettings;
private const int SaltSize = 16;
public DefaultEncryptionService(ShellSettings shellSettings ) {
_shellSettings = shellSettings;
}
public byte[] Decode(byte[] encodedData) {
using ( var ms = new MemoryStream() ) {
using (var algorithm = CreateAlgorithm()) {
using ( var cs = new CryptoStream(ms, algorithm.CreateDecryptor(), CryptoStreamMode.Write) ) {
cs.Write(encodedData, 0, encodedData.Length);
cs.FlushFinalBlock();
}
// remove the salt part
return ms.ToArray().Skip(SaltSize).ToArray();
}
}
}
public byte[] Encode(byte[] data) {
var salt = new byte[SaltSize];
// generate a random salt to happend to encoded data
using ( var random = new RNGCryptoServiceProvider() ) {
random.GetBytes(salt);
}
using ( var ms = new MemoryStream() ) {
using (var algorithm = CreateAlgorithm()) {
using ( var cs = new CryptoStream(ms, algorithm.CreateEncryptor(), CryptoStreamMode.Write) ) {
// append the salt to the data and encrypt
var salted = salt.Concat(data).ToArray();
cs.Write(salted, 0, salted.Length);
cs.FlushFinalBlock();
}
return ms.ToArray();
}
}
}
private SymmetricAlgorithm CreateAlgorithm() {
var encryptionAlgorithm = SymmetricAlgorithm.Create(_shellSettings.EncryptionAlgorithm);
encryptionAlgorithm.Key = _shellSettings.EncryptionKey.ToByteArray();
encryptionAlgorithm.IV = _shellSettings.EncryptionIV.ToByteArray();
return encryptionAlgorithm;
}
}
}

View File

@ -1,4 +1,5 @@
using System.Linq;
using System;
using System.Linq;
using System.Text.RegularExpressions;
using Orchard.Localization;
@ -55,5 +56,16 @@ namespace Orchard.Utility.Extensions {
? ""
: Regex.Replace(text, @"(\r?\n)", replacement, RegexOptions.Singleline);
}
public static string ToHexString(this byte[] bytes) {
return BitConverter.ToString(bytes).Replace("-", "");
}
public static byte[] ToByteArray(this string hex) {
return Enumerable.Range(0, hex.Length).
Where(x => 0 == x % 2).
Select(x => Convert.ToByte(hex.Substring(x, 2), 16)).
ToArray();
}
}
}