تشخیص اصالت ردیفهای یک بانک اطلاعاتی در EF Core
نویسنده: وحید نصیری
تاریخ: ۱۳۹۸/۰۶/۱۶ ۱۴:۳۰
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
SELECT
[Id],
(SELECT top 1 * FROM [AppUsers] FOR XML auto),
HASHBYTES ('SHA2_256', (SELECT top 1 * FROM [AppUsers] FOR XML auto)) AS [hash] -- varbinary(n), since 2012
FROM
[AppUsers]
using System.Collections.Generic;
namespace EFCoreRowIntegrity
{
public interface IAuditableEntity
{
string Hash { set; get; }
}
public static class AuditableShadowProperties
{
public static readonly string CreatedDateTime = nameof(CreatedDateTime);
public static readonly string ModifiedDateTime = nameof(ModifiedDateTime);
}
public class Blog : IAuditableEntity
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
public string Hash { get; set; }
}
public class Post : IAuditableEntity
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
public string Hash { get; set; }
}
}
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.ChangeTracking;
namespace EFCoreRowIntegrity
{
public class AuditEntry
{
public EntityEntry EntityEntry { set; get; }
public IList<AuditProperty> AuditProperties { set; get; } = new List<AuditProperty>();
public AuditEntry() { }
public AuditEntry(EntityEntry entry)
{
EntityEntry = entry;
}
}
public class AuditProperty
{
public string Name { set; get; }
public object Value { set; get; }
public bool IsTemporary { set; get; }
public PropertyEntry PropertyEntry { set; get; }
public AuditProperty() { }
public AuditProperty(string name, object value, bool isTemporary, PropertyEntry property)
{
Name = name;
Value = value;
IsTemporary = isTemporary;
PropertyEntry = property;
}
}
} using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Newtonsoft.Json;
namespace EFCoreRowIntegrity
{
public static class HashingExtensions
{
public static string GenerateObjectHash(this object @object)
{
if (@object == null)
{
return string.Empty;
}
var jsonData = JsonConvert.SerializeObject(@object, Formatting.Indented);
using (var hashAlgorithm = new SHA256CryptoServiceProvider())
{
var byteValue = Encoding.UTF8.GetBytes(jsonData);
var byteHash = hashAlgorithm.ComputeHash(byteValue);
return Convert.ToBase64String(byteHash);
}
}
public static string GenerateEntityEntryHash(this EntityEntry entry, string propertyToIgnore)
{
var auditEntry = new Dictionary<string, object>();
foreach (var property in entry.Properties)
{
var propertyName = property.Metadata.Name;
if (propertyName == propertyToIgnore)
{
continue;
}
auditEntry[propertyName] = property.CurrentValue;
}
return auditEntry.GenerateObjectHash();
}
public static string GenerateEntityHash<TEntity>(this DbContext context, TEntity entity, string propertyToIgnore)
{
return context.Entry(entity).GenerateEntityEntryHash(propertyToIgnore);
}
}
} using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.Extensions.Logging;
namespace EFCoreRowIntegrity
{
public class BloggingContext : DbContext
{
public BloggingContext()
{ }
public BloggingContext(DbContextOptions options)
: base(options)
{ }
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.EnableSensitiveDataLogging();
var path = Path.Combine(Directory.GetCurrentDirectory(), "app_data", "EFCore.RowIntegrity.mdf");
optionsBuilder.UseSqlServer($"Server=(localdb)\\mssqllocaldb;Database=EFCore.RowIntegrity;AttachDbFilename={path};Trusted_Connection=True;");
optionsBuilder.UseLoggerFactory(new LoggerFactory().AddConsole((message, logLevel) =>
logLevel == LogLevel.Debug &&
message.StartsWith("Microsoft.EntityFrameworkCore.Database.Command")));
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
foreach (var entityType in modelBuilder.Model
.GetEntityTypes()
.Where(e => typeof(IAuditableEntity)
.IsAssignableFrom(e.ClrType)))
{
modelBuilder.Entity(entityType.ClrType)
.Property<DateTimeOffset?>(AuditableShadowProperties.CreatedDateTime);
modelBuilder.Entity(entityType.ClrType)
.Property<DateTimeOffset?>(AuditableShadowProperties.ModifiedDateTime);
}
}
public override int SaveChanges()
{
var auditEntries = OnBeforeSaveChanges();
var result = base.SaveChanges();
OnAfterSaveChanges(auditEntries);
return result;
}
private IList<AuditEntry> OnBeforeSaveChanges()
{
var auditEntries = new List<AuditEntry>();
foreach (var entry in ChangeTracker.Entries<IAuditableEntity>())
{
if (entry.State == EntityState.Detached || entry.State == EntityState.Unchanged)
{
continue;
}
var auditEntry = new AuditEntry(entry);
auditEntries.Add(auditEntry);
var now = DateTimeOffset.UtcNow;
foreach (var property in entry.Properties)
{
var propertyName = property.Metadata.Name;
if (propertyName == nameof(IAuditableEntity.Hash))
{
continue;
}
if (property.IsTemporary)
{
// It's an auto-generated value and should be retrieved from the DB after calling the base.SaveChanges().
auditEntry.AuditProperties.Add(new AuditProperty(propertyName, null, true, property));
continue;
}
switch (entry.State)
{
case EntityState.Added:
entry.Property(AuditableShadowProperties.CreatedDateTime).CurrentValue = now;
auditEntry.AuditProperties.Add(new AuditProperty(propertyName, property.CurrentValue, false, property));
break;
case EntityState.Modified:
auditEntry.AuditProperties.Add(new AuditProperty(propertyName, property.CurrentValue, false, property));
entry.Property(AuditableShadowProperties.ModifiedDateTime).CurrentValue = now;
break;
}
}
}
return auditEntries;
}
private void OnAfterSaveChanges(IList<AuditEntry> auditEntries)
{
foreach (var auditEntry in auditEntries)
{
foreach (var auditProperty in auditEntry.AuditProperties.Where(x => x.IsTemporary))
{
// Now we have the auto-generated value from the DB.
auditProperty.Value = auditProperty.PropertyEntry.CurrentValue;
auditProperty.IsTemporary = false;
}
auditEntry.EntityEntry.Property(nameof(IAuditableEntity.Hash)).CurrentValue =
auditEntry.AuditProperties.ToDictionary(x => x.Name, x => x.Value).GenerateObjectHash();
}
base.SaveChanges();
}
}
} public override int SaveChanges()
{
var auditEntries = OnBeforeSaveChanges();
var result = base.SaveChanges();
OnAfterSaveChanges(auditEntries);
return result;
} if (property.IsTemporary)
{
// It's an auto-generated value and should be retrieved from the DB after calling the base.SaveChanges().
auditEntry.AuditProperties.Add(new AuditProperty(propertyName, null, true, property));
continue;
} private static void CheckRow1IsAuthentic()
{
using (var context = new BloggingContext())
{
var blog1 = context.Blogs.Single(x => x.BlogId == 1);
var entityHash = context.GenerateEntityHash(blog1, propertyToIgnore: nameof(IAuditableEntity.Hash));
var dbRowHash = blog1.Hash;
Console.WriteLine($"entityHash: {entityHash}\ndbRowHash: {dbRowHash}");
if (entityHash == dbRowHash)
{
Console.WriteLine("This row is authentic!");
}
else
{
Console.WriteLine("This row is tampered outside of the application!");
}
}
} entityHash: P110cYquWpoaZuTpCWaqBn6HPSGdoQdmaAN05s1zYqo= dbRowHash: P110cYquWpoaZuTpCWaqBn6HPSGdoQdmaAN05s1zYqo= This row is authentic!
entityHash: tdiZhKMJRnROGLLam1WpldA0fy/CbjJaR2Y2jNU9izk= dbRowHash: P110cYquWpoaZuTpCWaqBn6HPSGdoQdmaAN05s1zYqo= This row is tampered outside of the application!
var blogs = (from blog in context.Blogs.ToList() // Note: this `ToList()` is necessary here for having Shadow properties values, otherwise they will considered `null`.
let computedHash = context.GenerateEntityHash(blog, nameof(IAuditableEntity.Hash))
select new
{
blog.BlogId,
blog.Url,
RowHash = blog.Hash,
ComputedHash = computedHash,
IsAuthentic = blog.Hash == computedHash
}).ToList(); public interface IHasRowIntegrity
{
string Hash { get; set; }
} internal sealed class RowIntegrityHook : PostActionHook<IHasRowIntegrity>
{
public override string Name => HookNames.RowIntegrity;
public override int Order => int.MaxValue;
public override EntityState HookState => EntityState.Unchanged;
protected override void Hook(IHasRowIntegrity entity, HookEntityMetadata metadata, IUnitOfWork uow)
{
metadata.Entry.Property(EFCore.Hash).CurrentValue = uow.EntityHash(entity);
}
} //DbContextCore : IUnitOfWork
public string EntityHash<TEntity>(TEntity entity) where TEntity : class
{
var row = Entry(entity).ToDictionary(p => p.Metadata.Name != EFCore.Hash &&
!p.Metadata.ValueGenerated.HasFlag(ValueGenerated.OnUpdate) &&
!p.Metadata.IsShadowProperty());
return EntityHash<TEntity>(row);
}
protected virtual string EntityHash<TEntity>(Dictionary<string, object> row) where TEntity : class
{
var json = JsonConvert.SerializeObject(row, Formatting.Indented);
using (var hashAlgorithm = SHA256.Create())
{
var byteValue = Encoding.UTF8.GetBytes(json);
var byteHash = hashAlgorithm.ComputeHash(byteValue);
return Convert.ToBase64String(byteHash);
}
} services.AddEFCore<ProjectDbContext>()
.WithTrackingHook<long>()
.WithDeletedEntityHook()
.WithRowLevelSecurityHook<long>()
.WithRowIntegrityHook()
.WithNumberingHook(options =>
{
options.NumberedEntityMap[typeof(Task)] = new NumberedEntityOption
{
Prefix = "Task",
FieldNames = new[] {nameof(Task.BranchId)}
};
});