diff --git a/Teknik/App_Start/SubdomainRoute.cs b/Teknik/App_Start/SubdomainRoute.cs index 75bc00c..7950c68 100644 --- a/Teknik/App_Start/SubdomainRoute.cs +++ b/Teknik/App_Start/SubdomainRoute.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; +using Teknik.Helpers; namespace Teknik { diff --git a/Teknik/Areas/Shortener/Shortener.cs b/Teknik/Areas/Shortener/Shortener.cs index 1fe5a89..f8f3cfe 100644 --- a/Teknik/Areas/Shortener/Shortener.cs +++ b/Teknik/Areas/Shortener/Shortener.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using Teknik.Areas.Shortener.Models; +using Teknik.Helpers; using Teknik.Models; namespace Teknik.Areas.Shortener diff --git a/Teknik/Areas/Upload/Uploader.cs b/Teknik/Areas/Upload/Uploader.cs index 4cfd55f..60312b3 100644 --- a/Teknik/Areas/Upload/Uploader.cs +++ b/Teknik/Areas/Upload/Uploader.cs @@ -5,6 +5,7 @@ using System.Web; using System.IO; using Teknik.Configuration; using Teknik.Models; +using Teknik.Helpers; namespace Teknik.Areas.Upload { diff --git a/Teknik/Areas/User/Controllers/UserController.cs b/Teknik/Areas/User/Controllers/UserController.cs index 5351654..5387441 100644 --- a/Teknik/Areas/User/Controllers/UserController.cs +++ b/Teknik/Areas/User/Controllers/UserController.cs @@ -23,6 +23,7 @@ using Teknik.Filters; using QRCoder; using System.Text; using TwoStepsAuthenticator; +using System.Drawing; namespace Teknik.Areas.Users.Controllers { @@ -167,10 +168,30 @@ namespace Teknik.Areas.Users.Controllers bool userValid = UserHelper.UserPasswordCorrect(db, Config, user, model.Password); if (userValid) { + bool twoFactor = false; string returnUrl = model.ReturnUrl; if (user.SecuritySettings.TwoFactorEnabled) { + twoFactor = true; // We need to check their device, and two factor them + if (user.SecuritySettings.AllowTrustedDevices) + { + // Check for the trusted device cookie + HttpCookie cookie = Request.Cookies[Constants.TRUSTEDDEVICECOOKIE + "_" + username]; + if (cookie != null) + { + string token = cookie.Value; + if (user.TrustedDevices.Where(d => d.Token == token).FirstOrDefault() != null) + { + // The device token is attached to the user, let's let it slide + twoFactor = false; + } + } + } + } + + if (twoFactor) + { Session["AuthenticatedUser"] = user; if (string.IsNullOrEmpty(model.ReturnUrl)) returnUrl = Request.UrlReferrer.AbsoluteUri.ToString(); @@ -295,7 +316,7 @@ namespace Teknik.Areas.Users.Controllers } [HttpPost] - public ActionResult Edit(string curPass, string newPass, string newPassConfirm, string pgpPublicKey, string recoveryEmail, bool twoFactorEnabled, string website, string quote, string about, string blogTitle, string blogDesc, bool saveKey, bool serverSideEncrypt) + public ActionResult Edit(string curPass, string newPass, string newPassConfirm, string pgpPublicKey, string recoveryEmail, bool allowTrustedDevices, bool twoFactorEnabled, string website, string quote, string about, string blogTitle, string blogDesc, bool saveKey, bool serverSideEncrypt) { if (ModelState.IsValid) { @@ -329,6 +350,7 @@ namespace Teknik.Areas.Users.Controllers } user.SecuritySettings.PGPSignature = pgpPublicKey; + // Recovery Email bool newRecovery = false; if (recoveryEmail != user.SecuritySettings.RecoveryEmail) { @@ -337,24 +359,56 @@ namespace Teknik.Areas.Users.Controllers user.SecuritySettings.RecoveryVerified = false; } + // Trusted Devices + user.SecuritySettings.AllowTrustedDevices = allowTrustedDevices; + if (!allowTrustedDevices) + { + // They turned it off, let's clear the trusted devices + user.TrustedDevices.Clear(); + List foundDevices = db.TrustedDevices.Where(d => d.UserId == user.UserId).ToList(); + if (foundDevices != null) + { + foreach (TrustedDevice device in foundDevices) + { + db.TrustedDevices.Remove(device); + } + } + } + + // Two Factor Authentication bool oldTwoFactor = user.SecuritySettings.TwoFactorEnabled; user.SecuritySettings.TwoFactorEnabled = twoFactorEnabled; string newKey = string.Empty; - if (twoFactorEnabled) + if (!oldTwoFactor && twoFactorEnabled) { + // They just enabled it, let's regen the key newKey = Authenticator.GenerateKey(); } + else if (!twoFactorEnabled) + { + // remove the key when it's disabled + newKey = string.Empty; + } + else + { + // No change, let's use the old value + newKey = user.SecuritySettings.TwoFactorKey; + } user.SecuritySettings.TwoFactorKey = newKey; + // Profile Info user.UserSettings.Website = website; user.UserSettings.Quote = quote; user.UserSettings.About = about; + // Blogs user.BlogSettings.Title = blogTitle; user.BlogSettings.Description = blogDesc; + // Uploads user.UploadSettings.SaveKey = saveKey; user.UploadSettings.ServerSideEncrypt = serverSideEncrypt; + UserHelper.EditAccount(db, Config, user, changePass, newPass); // If they have a recovery email, let's send a verification @@ -557,18 +611,24 @@ namespace Teknik.Areas.Users.Controllers [AllowAnonymous] public ActionResult ConfirmTwoFactorAuth(string returnUrl, bool rememberMe) { - ViewBag.Title = "Unknown Device - " + Config.Title; - ViewBag.Description = "We do not recognize this device."; - LoginViewModel model = new LoginViewModel(); - model.ReturnUrl = returnUrl; - model.RememberMe = rememberMe; + User user = (User)Session["AuthenticatedUser"]; + if (user != null) + { + ViewBag.Title = "Unknown Device - " + Config.Title; + ViewBag.Description = "We do not recognize this device."; + TwoFactorViewModel model = new TwoFactorViewModel(); + model.ReturnUrl = returnUrl; + model.RememberMe = rememberMe; + model.AllowTrustedDevice = user.SecuritySettings.AllowTrustedDevices; - return View("/Areas/User/Views/User/TwoFactorCheck.cshtml", model); + return View("/Areas/User/Views/User/TwoFactorCheck.cshtml", model); + } + return Redirect(Url.SubRouteUrl("error", "Error.Http403")); } [HttpPost] [AllowAnonymous] - public ActionResult ConfirmAuthenticatorCode(string code, string returnUrl, bool rememberMe) + public ActionResult ConfirmAuthenticatorCode(string code, string returnUrl, bool rememberMe, bool rememberDevice, string deviceName) { User user = (User)Session["AuthenticatedUser"]; if (user != null) @@ -586,6 +646,23 @@ namespace Teknik.Areas.Users.Controllers HttpCookie authcookie = UserHelper.CreateAuthCookie(user.Username, rememberMe, Request.Url.Host.GetDomain(), Request.IsLocal); Response.Cookies.Add(authcookie); + if (user.SecuritySettings.AllowTrustedDevices && rememberDevice) + { + // They want to remember the device, and have allow trusted devices on + HttpCookie trustedDeviceCookie = UserHelper.CreateTrustedDeviceCookie(user.Username, Request.Url.Host.GetDomain(), Request.IsLocal); + Response.Cookies.Add(trustedDeviceCookie); + + TrustedDevice device = new TrustedDevice(); + device.UserId = user.UserId; + device.Name = (string.IsNullOrEmpty(deviceName)) ? "Unknown" : deviceName; + device.DateSeen = DateTime.Now; + device.Token = trustedDeviceCookie.Value; + + // Add the token + db.TrustedDevices.Add(device); + db.SaveChanges(); + } + if (string.IsNullOrEmpty(returnUrl)) returnUrl = Request.UrlReferrer.AbsoluteUri.ToString(); return Json(new { result = returnUrl }); @@ -628,9 +705,9 @@ namespace Teknik.Areas.Users.Controllers QRCodeGenerator qrGenerator = new QRCodeGenerator(); QRCodeData qrCodeData = qrGenerator.CreateQrCode(ProvisionUrl, QRCodeGenerator.ECCLevel.Q); - SvgQRCode qrCode = new SvgQRCode(qrCodeData); - string qrCodeImage = qrCode.GetGraphic(20); - return File(Encoding.UTF8.GetBytes(qrCodeImage), "image/svg+xml"); + QRCode qrCode = new QRCode(qrCodeData); + Bitmap qrCodeImage = qrCode.GetGraphic(20); + return File(Helpers.Utility.ImageToByte(qrCodeImage), "image/png"); } } } \ No newline at end of file diff --git a/Teknik/Areas/User/Models/SecuritySettings.cs b/Teknik/Areas/User/Models/SecuritySettings.cs index d59b118..182692b 100644 --- a/Teknik/Areas/User/Models/SecuritySettings.cs +++ b/Teknik/Areas/User/Models/SecuritySettings.cs @@ -24,6 +24,8 @@ namespace Teknik.Areas.Users.Models public bool RecoveryVerified { get; set; } + public bool AllowTrustedDevices { get; set; } + public bool TwoFactorEnabled { get; set; } [CaseSensitive] @@ -35,6 +37,7 @@ namespace Teknik.Areas.Users.Models { RecoveryEmail = string.Empty; RecoveryVerified = false; + AllowTrustedDevices = false; TwoFactorEnabled = false; TwoFactorKey = string.Empty; PGPSignature = string.Empty; diff --git a/Teknik/Areas/User/Models/UserDevice.cs b/Teknik/Areas/User/Models/TrustedDevice.cs similarity index 83% rename from Teknik/Areas/User/Models/UserDevice.cs rename to Teknik/Areas/User/Models/TrustedDevice.cs index 85cd17e..250f65d 100644 --- a/Teknik/Areas/User/Models/UserDevice.cs +++ b/Teknik/Areas/User/Models/TrustedDevice.cs @@ -6,9 +6,9 @@ using Teknik.Attributes; namespace Teknik.Areas.Users.Models { - public class UserDevice + public class TrustedDevice { - public int UserDeviceId { get; set; } + public int TrustedDeviceId { get; set; } public int UserId { get; set; } diff --git a/Teknik/Areas/User/Models/User.cs b/Teknik/Areas/User/Models/User.cs index 4ac8434..bd8ffd2 100644 --- a/Teknik/Areas/User/Models/User.cs +++ b/Teknik/Areas/User/Models/User.cs @@ -33,7 +33,7 @@ namespace Teknik.Areas.Users.Models public virtual UploadSettings UploadSettings { get; set; } - public virtual ICollection Devices { get; set; } + public virtual ICollection TrustedDevices { get; set; } public virtual ICollection Uploads { get; set; } @@ -47,7 +47,7 @@ namespace Teknik.Areas.Users.Models JoinDate = DateTime.Now; LastSeen = DateTime.Now; Groups = new List(); - Devices = new List(); + TrustedDevices = new List(); } } } \ No newline at end of file diff --git a/Teknik/Areas/User/Scripts/CheckAuthCode.js b/Teknik/Areas/User/Scripts/CheckAuthCode.js index 8ab3083..ce59a5b 100644 --- a/Teknik/Areas/User/Scripts/CheckAuthCode.js +++ b/Teknik/Areas/User/Scripts/CheckAuthCode.js @@ -2,16 +2,18 @@ $("#authCheckStatus").css('display', 'none', 'important'); $("#verifyCodeSubmit").click(function () { - setCode = $("#code").val(); - returnUrl = $("#returnUrl").val(); - rememberMe = ($("#rememberMe").val() == 'True'); + setCode = $("#Code").val(); + returnUrl = $("#ReturnUrl").val(); + rememberMe = ($("#RememberMe").val() == 'True'); + rememberDevice = $("#RememberDevice").is(":checked"); $.ajax({ type: "POST", url: confirmAuthCodeURL, data: { code: setCode, returnUrl: returnUrl, - rememberMe: rememberMe + rememberMe: rememberMe, + rememberDevice: rememberDevice }, xhrFields: { withCredentials: true diff --git a/Teknik/Areas/User/Scripts/User.js b/Teknik/Areas/User/Scripts/User.js index cf6af8b..56b37f5 100644 --- a/Teknik/Areas/User/Scripts/User.js +++ b/Teknik/Areas/User/Scripts/User.js @@ -2,6 +2,7 @@ $("[name='update_upload_saveKey']").bootstrapSwitch(); $("[name='update_upload_serverSideEncrypt']").bootstrapSwitch(); $("[name='update_security_two_factor']").bootstrapSwitch(); + $("[name='update_security_allow_trusted']").bootstrapSwitch(); $('#ResendVerification').click(function () { $.ajax({ @@ -98,6 +99,7 @@ password = $("#update_password").val(); password_confirm = $("#update_password_confirm").val(); update_pgp_public_key = $("#update_pgp_public_key").val(); + update_security_allow_trusted = $("#update_security_allow_trusted").is(":checked"); update_security_two_factor = $("#update_security_two_factor").is(":checked"); recovery = $("#update_recovery_email").val(); website = $("#update_website").val(); @@ -115,6 +117,7 @@ newPass: password, newPassConfirm: password_confirm, pgpPublicKey: update_pgp_public_key, + allowTrustedDevices: update_security_allow_trusted, twoFactorEnabled: update_security_two_factor, recoveryEmail: recovery, website: website, diff --git a/Teknik/Areas/User/Utility/UserHelper.cs b/Teknik/Areas/User/Utility/UserHelper.cs index 2e4d8a1..892326e 100644 --- a/Teknik/Areas/User/Utility/UserHelper.cs +++ b/Teknik/Areas/User/Utility/UserHelper.cs @@ -437,7 +437,7 @@ namespace Teknik.Areas.Users.Utility } // Create a new verification code and add it - string verifyCode = Teknik.Utility.RandomString(24); + string verifyCode = Helpers.Utility.RandomString(24); RecoveryEmailVerification ver = new RecoveryEmailVerification(); ver.UserId = user.UserId; ver.Code = verifyCode; @@ -518,7 +518,7 @@ Thank you and enjoy! } // Create a new verification code and add it - string verifyCode = Teknik.Utility.RandomString(24); + string verifyCode = Helpers.Utility.RandomString(24); ResetPasswordVerification ver = new ResetPasswordVerification(); ver.UserId = user.UserId; ver.Code = verifyCode; @@ -858,7 +858,7 @@ If you recieved this email and you did not reset your password, you can ignore t { Config config = Config.Load(); HttpCookie authcookie = FormsAuthentication.GetAuthCookie(username, remember); - authcookie.Name = "TeknikAuth"; + authcookie.Name = Constants.AUTHCOOKIE; authcookie.HttpOnly = true; authcookie.Secure = true; @@ -878,5 +878,35 @@ If you recieved this email and you did not reset your password, you can ignore t return authcookie; } + + public static HttpCookie CreateTrustedDeviceCookie(string username, string domain, bool local) + { + Config config = Config.Load(); + + byte[] time = BitConverter.GetBytes(DateTime.UtcNow.ToBinary()); + byte[] key = Guid.NewGuid().ToByteArray(); + string token = Convert.ToBase64String(time.Concat(key).ToArray()); + HttpCookie trustCookie = new HttpCookie(Constants.TRUSTEDDEVICECOOKIE + "_" + username); + trustCookie.Value = token; + trustCookie.HttpOnly = true; + trustCookie.Secure = true; + trustCookie.Expires = DateTime.Now.AddYears(1); + + // Set domain dependent on where it's being ran from + if (local) // localhost + { + trustCookie.Domain = null; + } + else if (config.DevEnvironment) // dev.example.com + { + trustCookie.Domain = string.Format("dev.{0}", domain); + } + else // A production instance + { + trustCookie.Domain = string.Format(".{0}", domain); + } + + return trustCookie; + } } } diff --git a/Teknik/Areas/User/ViewModels/TwoFactorViewModel.cs b/Teknik/Areas/User/ViewModels/TwoFactorViewModel.cs new file mode 100644 index 0000000..4752afa --- /dev/null +++ b/Teknik/Areas/User/ViewModels/TwoFactorViewModel.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using Teknik.ViewModels; + +namespace Teknik.Areas.Users.ViewModels +{ + public class TwoFactorViewModel : ViewModelBase + { + public bool RememberMe { get; set; } + + public string ReturnUrl { get; set; } + + public bool AllowTrustedDevice { get; set; } + } +} \ No newline at end of file diff --git a/Teknik/Areas/User/Views/User/Settings.cshtml b/Teknik/Areas/User/Views/User/Settings.cshtml index 80fbbcd..da80c4f 100644 --- a/Teknik/Areas/User/Views/User/Settings.cshtml +++ b/Teknik/Areas/User/Views/User/Settings.cshtml @@ -125,19 +125,6 @@ -
-
- -
- -
- -
-
@@ -161,6 +148,27 @@
+
+
+ +
+ +
+ +
+
+ +
+ +
+
+
diff --git a/Teknik/Areas/User/Views/User/TwoFactorCheck.cshtml b/Teknik/Areas/User/Views/User/TwoFactorCheck.cshtml index b30df5e..ed9b91c 100644 --- a/Teknik/Areas/User/Views/User/TwoFactorCheck.cshtml +++ b/Teknik/Areas/User/Views/User/TwoFactorCheck.cshtml @@ -1,4 +1,4 @@ -@model Teknik.Areas.Users.ViewModels.LoginViewModel +@model Teknik.Areas.Users.ViewModels.TwoFactorViewModel @using Teknik.Helpers @@ -22,12 +22,23 @@
- - + +
- +
+ @if (Model.AllowTrustedDevice) + { +
+ +
+ Set this device as a trusted device. It is not advised to trust a public computer. +
+
+ }
diff --git a/Teknik/Helpers/Constants.cs b/Teknik/Helpers/Constants.cs index 88b4106..bd97b33 100644 --- a/Teknik/Helpers/Constants.cs +++ b/Teknik/Helpers/Constants.cs @@ -9,6 +9,9 @@ namespace Teknik.Helpers public static class Constants { public const string SERVERUSER = "Server Admin"; + public const string AUTHCOOKIE = "TeknikAuth"; + public const string TRUSTEDDEVICECOOKIE = "TeknikTrustedDevice"; + // Paste Constants public static Dictionary HIGHLIGHTFORMATS = new Dictionary() { diff --git a/Teknik/Helpers/Utility.cs b/Teknik/Helpers/Utility.cs index 7309721..5babd35 100644 --- a/Teknik/Helpers/Utility.cs +++ b/Teknik/Helpers/Utility.cs @@ -8,7 +8,7 @@ using System.Linq; using System.Text; using System.Web; -namespace Teknik +namespace Teknik.Helpers { public static class Utility { diff --git a/Teknik/Models/TeknikEntities.cs b/Teknik/Models/TeknikEntities.cs index ef7bd78..1f72a06 100644 --- a/Teknik/Models/TeknikEntities.cs +++ b/Teknik/Models/TeknikEntities.cs @@ -22,7 +22,7 @@ namespace Teknik.Models public DbSet Users { get; set; } public DbSet Groups { get; set; } public DbSet Roles { get; set; } - public DbSet UserDevices { get; set; } + public DbSet TrustedDevices { get; set; } public DbSet TransferTypes { get; set; } // User Settings public DbSet UserSettings { get; set; } @@ -112,7 +112,7 @@ namespace Teknik.Models modelBuilder.Entity().ToTable("Users"); modelBuilder.Entity().ToTable("Groups"); modelBuilder.Entity().ToTable("Roles"); - modelBuilder.Entity().ToTable("UserDevices"); + modelBuilder.Entity().ToTable("TrustedDevices"); modelBuilder.Entity().ToTable("TransferTypes"); modelBuilder.Entity().ToTable("RecoveryEmailVerifications"); modelBuilder.Entity().ToTable("ResetPasswordVerifications"); diff --git a/Teknik/Teknik.csproj b/Teknik/Teknik.csproj index dbc716a..fb60ab4 100644 --- a/Teknik/Teknik.csproj +++ b/Teknik/Teknik.csproj @@ -224,7 +224,8 @@ - + +