diff options
6 files changed, 479 insertions, 3 deletions
diff --git a/Software/Visual_Studio/Web/Tango.Portal/Controllers/HomeController.cs b/Software/Visual_Studio/Web/Tango.Portal/Controllers/HomeController.cs index b913ec6d8..dff8a140e 100644 --- a/Software/Visual_Studio/Web/Tango.Portal/Controllers/HomeController.cs +++ b/Software/Visual_Studio/Web/Tango.Portal/Controllers/HomeController.cs @@ -15,6 +15,7 @@ using Tango.Portal.Models; using Tango.Portal.Utils; using Tango.Portal.ViewModels; using System.Data.Entity; +using Newtonsoft.Json; namespace Tango.Portal.Controllers { @@ -167,6 +168,13 @@ namespace Tango.Portal.Controllers } } + public ActionResult AI() + { + if (!SessionUser.IsTwineUser) return RedirectToAction(nameof(Login)); + String session = SimpleCryptoHelper.Encrypt(JsonConvert.SerializeObject(new { UserName = SessionUser.Name, Expires = DateTime.UtcNow.AddHours(1) })); + return new RedirectResult($"https://ai.twine-srv.com?session={session}"); + } + public ActionResult Docs() { DocsVM vm = new DocsVM(SessionUser); diff --git a/Software/Visual_Studio/Web/Tango.Portal/Tango.Portal.csproj b/Software/Visual_Studio/Web/Tango.Portal/Tango.Portal.csproj index 75cc549e6..071be6fe1 100644 --- a/Software/Visual_Studio/Web/Tango.Portal/Tango.Portal.csproj +++ b/Software/Visual_Studio/Web/Tango.Portal/Tango.Portal.csproj @@ -189,6 +189,7 @@ <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Startup.cs" /> <Compile Include="Utils\DbUtils.cs" /> + <Compile Include="Utils\SimpleCryptoHelper.cs" /> <Compile Include="Utils\StatisticsUtils.cs" /> <Compile Include="Utils\StorageUtils.cs" /> <Compile Include="ViewModel.cs" /> diff --git a/Software/Visual_Studio/Web/Tango.Portal/Utils/SimpleCryptoHelper.cs b/Software/Visual_Studio/Web/Tango.Portal/Utils/SimpleCryptoHelper.cs new file mode 100644 index 000000000..bcd0281f4 --- /dev/null +++ b/Software/Visual_Studio/Web/Tango.Portal/Utils/SimpleCryptoHelper.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Web; + +namespace Tango.Portal.Utils +{ + public static class SimpleCryptoHelper + { + // ======= CONFIG ======= + // Provide a 32-byte (256-bit) master key in Base64 (standard Base64, not URL form). + // You can reuse the one we generated earlier or generate your own. + private const string MasterKeyBase64 = "1SuLF9QlXTNIAKAzRb4ILuQnfZ40BERQtXiMQKHLbQg="; + + // ======= PUBLIC API ======= + public static string Encrypt(string plainText) + { + if (plainText == null) throw new ArgumentNullException(nameof(plainText)); + + var masterKey = Convert.FromBase64String(MasterKeyBase64); + if (masterKey.Length != 32) throw new InvalidOperationException("Master key must be 32 bytes (Base64 of 32 bytes)."); + + // Derive subkeys + byte[] encKey = HkdfSha256(masterKey, null, Encoding.UTF8.GetBytes("enc"), 32); + byte[] macKey = HkdfSha256(masterKey, null, Encoding.UTF8.GetBytes("mac"), 32); + + // Generate IV (16 bytes for AES-CBC) + byte[] iv = new byte[16]; + using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(iv); + + byte[] ciphertext; + using (var aes = CreateAes(encKey, iv)) + using (var encryptor = aes.CreateEncryptor()) + { + var pt = Encoding.UTF8.GetBytes(plainText); + ciphertext = encryptor.TransformFinalBlock(pt, 0, pt.Length); + } + + // Build payload: version(1) || iv(16) || ciphertext || tag(32) + byte version = 1; + byte[] versionArr = new[] { version }; + byte[] macInput = Concat(versionArr, iv, ciphertext); + byte[] tag = HmacSha256(macKey, macInput); + + byte[] combined = Concat(versionArr, iv, ciphertext, tag); + return Base64UrlEncode(combined); // URL-safe & no padding + } + + public static string Decrypt(string encrypted) + { + if (encrypted == null) throw new ArgumentNullException(nameof(encrypted)); + + byte[] combined; + try + { + combined = Base64UrlDecode(encrypted); + } + catch + { + throw new InvalidOperationException("Invalid encrypted string. Decryption failed."); + } + + if (combined.Length < 1 + 16 + 32) + throw new InvalidOperationException("Invalid encrypted string. Decryption failed."); + + // Parse + int offset = 0; + byte version = combined[offset++]; + + if (version != 1) + throw new InvalidOperationException("Invalid encrypted string. Decryption failed."); + + byte[] iv = new byte[16]; + Buffer.BlockCopy(combined, offset, iv, 0, iv.Length); + offset += iv.Length; + + int tagLen = 32; + int ciphertextLen = combined.Length - offset - tagLen; + if (ciphertextLen <= 0) throw new InvalidOperationException("Invalid encrypted string. Decryption failed."); + + byte[] ciphertext = new byte[ciphertextLen]; + Buffer.BlockCopy(combined, offset, ciphertext, 0, ciphertextLen); + offset += ciphertextLen; + + byte[] tag = new byte[tagLen]; + Buffer.BlockCopy(combined, offset, tag, 0, tagLen); + + // Re-derive keys + var masterKey = Convert.FromBase64String(MasterKeyBase64); + if (masterKey.Length != 32) throw new InvalidOperationException("Master key must be 32 bytes (Base64 of 32 bytes)."); + + byte[] encKey = HkdfSha256(masterKey, null, Encoding.UTF8.GetBytes("enc"), 32); + byte[] macKey = HkdfSha256(masterKey, null, Encoding.UTF8.GetBytes("mac"), 32); + + // Verify HMAC (constant-time) + byte[] macInput = Concat(new[] { version }, iv, ciphertext); + byte[] expectedTag = HmacSha256(macKey, macInput); + if (!FixedTimeEquals(tag, expectedTag)) + throw new InvalidOperationException("Invalid encrypted string. Decryption failed."); + + // Decrypt + using (var aes = CreateAes(encKey, iv)) + using (var decryptor = aes.CreateDecryptor()) + { + try + { + byte[] pt = decryptor.TransformFinalBlock(ciphertext, 0, ciphertext.Length); + return Encoding.UTF8.GetString(pt); + } + catch + { + // Covers padding/format errors + throw new InvalidOperationException("Invalid encrypted string. Decryption failed."); + } + } + } + + // ======= CRYPTO PRIMITIVES ======= + private static Aes CreateAes(byte[] key, byte[] iv) + { + // AesManaged/AesCryptoServiceProvider both exist on .NET 4.6.1; .NET 8 maps to platform crypto. + var aes = Aes.Create(); + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; + aes.KeySize = 256; + aes.BlockSize = 128; + aes.Key = key; + aes.IV = iv; + return aes; + } + + private static byte[] HmacSha256(byte[] key, byte[] data) + { + using (var h = new HMACSHA256(key)) + { + return h.ComputeHash(data); + } + } + + // Minimal HKDF-SHA256 (RFC 5869): we use only Expand with Extract(salt, IKM) + private static byte[] HkdfSha256(byte[] ikm, byte[] salt, byte[] info, int length) + { + // Extract + byte[] prk; + using (var h = new HMACSHA256(salt ?? new byte[0])) + { + prk = h.ComputeHash(ikm); + } + + // Expand + int hashLen = 32; + int n = (int)Math.Ceiling(length / (double)hashLen); + byte[] okm = new byte[length]; + byte[] t = new byte[0]; + int offset = 0; + + using (var h = new HMACSHA256(prk)) + { + for (int i = 1; i <= n; i++) + { + // T(i) = HMAC-PRK(T(i-1) | info | i) + h.Initialize(); + h.TransformBlock(t, 0, t.Length, null, 0); + if (info != null && info.Length > 0) h.TransformBlock(info, 0, info.Length, null, 0); + var ctr = new[] { (byte)i }; + h.TransformFinalBlock(ctr, 0, 1); + t = h.Hash; + h.Initialize(); + + int toCopy = Math.Min(hashLen, length - offset); + Buffer.BlockCopy(t, 0, okm, offset, toCopy); + offset += toCopy; + } + } + return okm; + } + + private static bool FixedTimeEquals(byte[] a, byte[] b) + { + if (a == null || b == null || a.Length != b.Length) return false; + int diff = 0; + for (int i = 0; i < a.Length; i++) diff |= a[i] ^ b[i]; + return diff == 0; + } + + // ======= BASE64URL HELPERS (no padding) ======= + private static string Base64UrlEncode(byte[] data) + { + var s = Convert.ToBase64String(data) + .TrimEnd('=') + .Replace('+', '-') + .Replace('/', '_'); + return s; + } + + private static byte[] Base64UrlDecode(string s) + { + s = s.Replace('-', '+').Replace('_', '/'); + switch (s.Length % 4) + { + case 2: s += "=="; break; + case 3: s += "="; break; + } + return Convert.FromBase64String(s); + } + + // ======= UTIL ======= + private static byte[] Concat(params byte[][] arrays) + { + int len = 0; + for (int i = 0; i < arrays.Length; i++) len += arrays[i].Length; + var result = new byte[len]; + int pos = 0; + for (int i = 0; i < arrays.Length; i++) + { + Buffer.BlockCopy(arrays[i], 0, result, pos, arrays[i].Length); + pos += arrays[i].Length; + } + return result; + } + } +}
\ No newline at end of file diff --git a/Software/Visual_Studio/Web/Tango.Portal/Views/Shared/NavigationPartial.cshtml b/Software/Visual_Studio/Web/Tango.Portal/Views/Shared/NavigationPartial.cshtml index 0e45b2ead..063018130 100644 --- a/Software/Visual_Studio/Web/Tango.Portal/Views/Shared/NavigationPartial.cshtml +++ b/Software/Visual_Studio/Web/Tango.Portal/Views/Shared/NavigationPartial.cshtml @@ -9,7 +9,7 @@ <li class="nav-item"><a class="nav-link" href="/firmware.html">Firmware Upgrades</a></li> <li class="nav-item"><a class="nav-link" href="/utilities.html">Utilities</a></li> <li class="nav-item"><a class="nav-link" href="/docs.html">Docs</a></li> - <li class="nav-item"><a class="nav-link" href="https://ai.twine-srv.com">AI</a></li> + <li class="nav-item"><a class="nav-link" href="/ai">AI</a></li> </ul> @if (!Model.User.IsAuthenticated) { diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Controllers/HomeController.cs b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Controllers/HomeController.cs index 4f7305600..f83749f46 100644 --- a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Controllers/HomeController.cs +++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Controllers/HomeController.cs @@ -1,14 +1,38 @@ using Microsoft.AspNetCore.Mvc; +using System.Text.Json; using Tango.Portal.Chat.Web.ViewModels; namespace Tango.Portal.Chat.Web.Controllers { public sealed class HomeController : Controller { - public IActionResult Index() + public IActionResult Index(String session) { + String loginUrl = "https://twine-srv.com/login"; + + if (String.IsNullOrWhiteSpace(session)) return new RedirectResult(loginUrl); + + String decryptedSession = String.Empty; + + try + { + decryptedSession = SimpleCryptoHelper.Decrypt(session); + } + catch + { + return new RedirectResult(loginUrl); + } + + var template = new { UserName = "", Expires = DateTime.MinValue }; + + var sessionUser = JsonSerializer.Deserialize(decryptedSession, template.GetType()); + if (sessionUser == null || (DateTime)(sessionUser as dynamic).Expires < DateTime.UtcNow) + { + return new RedirectResult(loginUrl); + } + HomeViewVM vm = new HomeViewVM(); - vm.UserName = "Roy"; + vm.UserName = (sessionUser as dynamic).UserName; return View(vm); } } diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Utils/SimpleCryptoHelper.cs b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Utils/SimpleCryptoHelper.cs new file mode 100644 index 000000000..f0c797b54 --- /dev/null +++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Utils/SimpleCryptoHelper.cs @@ -0,0 +1,219 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +public static class SimpleCryptoHelper +{ + // ======= CONFIG ======= + // Provide a 32-byte (256-bit) master key in Base64 (standard Base64, not URL form). + // You can reuse the one we generated earlier or generate your own. + private const string MasterKeyBase64 = "1SuLF9QlXTNIAKAzRb4ILuQnfZ40BERQtXiMQKHLbQg="; + + // ======= PUBLIC API ======= + public static string Encrypt(string plainText) + { + if (plainText == null) throw new ArgumentNullException(nameof(plainText)); + + var masterKey = Convert.FromBase64String(MasterKeyBase64); + if (masterKey.Length != 32) throw new InvalidOperationException("Master key must be 32 bytes (Base64 of 32 bytes)."); + + // Derive subkeys + byte[] encKey = HkdfSha256(masterKey, null, Encoding.UTF8.GetBytes("enc"), 32); + byte[] macKey = HkdfSha256(masterKey, null, Encoding.UTF8.GetBytes("mac"), 32); + + // Generate IV (16 bytes for AES-CBC) + byte[] iv = new byte[16]; + using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(iv); + + byte[] ciphertext; + using (var aes = CreateAes(encKey, iv)) + using (var encryptor = aes.CreateEncryptor()) + { + var pt = Encoding.UTF8.GetBytes(plainText); + ciphertext = encryptor.TransformFinalBlock(pt, 0, pt.Length); + } + + // Build payload: version(1) || iv(16) || ciphertext || tag(32) + byte version = 1; + byte[] versionArr = new[] { version }; + byte[] macInput = Concat(versionArr, iv, ciphertext); + byte[] tag = HmacSha256(macKey, macInput); + + byte[] combined = Concat(versionArr, iv, ciphertext, tag); + return Base64UrlEncode(combined); // URL-safe & no padding + } + + public static string Decrypt(string encrypted) + { + if (encrypted == null) throw new ArgumentNullException(nameof(encrypted)); + + byte[] combined; + try + { + combined = Base64UrlDecode(encrypted); + } + catch + { + throw new InvalidOperationException("Invalid encrypted string. Decryption failed."); + } + + if (combined.Length < 1 + 16 + 32) + throw new InvalidOperationException("Invalid encrypted string. Decryption failed."); + + // Parse + int offset = 0; + byte version = combined[offset++]; + + if (version != 1) + throw new InvalidOperationException("Invalid encrypted string. Decryption failed."); + + byte[] iv = new byte[16]; + Buffer.BlockCopy(combined, offset, iv, 0, iv.Length); + offset += iv.Length; + + int tagLen = 32; + int ciphertextLen = combined.Length - offset - tagLen; + if (ciphertextLen <= 0) throw new InvalidOperationException("Invalid encrypted string. Decryption failed."); + + byte[] ciphertext = new byte[ciphertextLen]; + Buffer.BlockCopy(combined, offset, ciphertext, 0, ciphertextLen); + offset += ciphertextLen; + + byte[] tag = new byte[tagLen]; + Buffer.BlockCopy(combined, offset, tag, 0, tagLen); + + // Re-derive keys + var masterKey = Convert.FromBase64String(MasterKeyBase64); + if (masterKey.Length != 32) throw new InvalidOperationException("Master key must be 32 bytes (Base64 of 32 bytes)."); + + byte[] encKey = HkdfSha256(masterKey, null, Encoding.UTF8.GetBytes("enc"), 32); + byte[] macKey = HkdfSha256(masterKey, null, Encoding.UTF8.GetBytes("mac"), 32); + + // Verify HMAC (constant-time) + byte[] macInput = Concat(new[] { version }, iv, ciphertext); + byte[] expectedTag = HmacSha256(macKey, macInput); + if (!FixedTimeEquals(tag, expectedTag)) + throw new InvalidOperationException("Invalid encrypted string. Decryption failed."); + + // Decrypt + using (var aes = CreateAes(encKey, iv)) + using (var decryptor = aes.CreateDecryptor()) + { + try + { + byte[] pt = decryptor.TransformFinalBlock(ciphertext, 0, ciphertext.Length); + return Encoding.UTF8.GetString(pt); + } + catch + { + // Covers padding/format errors + throw new InvalidOperationException("Invalid encrypted string. Decryption failed."); + } + } + } + + // ======= CRYPTO PRIMITIVES ======= + private static Aes CreateAes(byte[] key, byte[] iv) + { + // AesManaged/AesCryptoServiceProvider both exist on .NET 4.6.1; .NET 8 maps to platform crypto. + var aes = Aes.Create(); + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; + aes.KeySize = 256; + aes.BlockSize = 128; + aes.Key = key; + aes.IV = iv; + return aes; + } + + private static byte[] HmacSha256(byte[] key, byte[] data) + { + using (var h = new HMACSHA256(key)) + { + return h.ComputeHash(data); + } + } + + // Minimal HKDF-SHA256 (RFC 5869): we use only Expand with Extract(salt, IKM) + private static byte[] HkdfSha256(byte[] ikm, byte[] salt, byte[] info, int length) + { + // Extract + byte[] prk; + using (var h = new HMACSHA256(salt ?? new byte[0])) + { + prk = h.ComputeHash(ikm); + } + + // Expand + int hashLen = 32; + int n = (int)Math.Ceiling(length / (double)hashLen); + byte[] okm = new byte[length]; + byte[] t = new byte[0]; + int offset = 0; + + using (var h = new HMACSHA256(prk)) + { + for (int i = 1; i <= n; i++) + { + // T(i) = HMAC-PRK(T(i-1) | info | i) + h.Initialize(); + h.TransformBlock(t, 0, t.Length, null, 0); + if (info != null && info.Length > 0) h.TransformBlock(info, 0, info.Length, null, 0); + var ctr = new[] { (byte)i }; + h.TransformFinalBlock(ctr, 0, 1); + t = h.Hash; + h.Initialize(); + + int toCopy = Math.Min(hashLen, length - offset); + Buffer.BlockCopy(t, 0, okm, offset, toCopy); + offset += toCopy; + } + } + return okm; + } + + private static bool FixedTimeEquals(byte[] a, byte[] b) + { + if (a == null || b == null || a.Length != b.Length) return false; + int diff = 0; + for (int i = 0; i < a.Length; i++) diff |= a[i] ^ b[i]; + return diff == 0; + } + + // ======= BASE64URL HELPERS (no padding) ======= + private static string Base64UrlEncode(byte[] data) + { + var s = Convert.ToBase64String(data) + .TrimEnd('=') + .Replace('+', '-') + .Replace('/', '_'); + return s; + } + + private static byte[] Base64UrlDecode(string s) + { + s = s.Replace('-', '+').Replace('_', '/'); + switch (s.Length % 4) + { + case 2: s += "=="; break; + case 3: s += "="; break; + } + return Convert.FromBase64String(s); + } + + // ======= UTIL ======= + private static byte[] Concat(params byte[][] arrays) + { + int len = 0; + for (int i = 0; i < arrays.Length; i++) len += arrays[i].Length; + var result = new byte[len]; + int pos = 0; + for (int i = 0; i < arrays.Length; i++) + { + Buffer.BlockCopy(arrays[i], 0, result, pos, arrays[i].Length); + pos += arrays[i].Length; + } + return result; + } +} |
