aboutsummaryrefslogtreecommitdiffstats
path: root/Software/Visual_Studio/Web/Tango.Portal
diff options
context:
space:
mode:
authorRoy Ben Shabat <roy.mail.net@gmail.com>2025-09-04 15:42:21 +0300
committerRoy Ben Shabat <roy.mail.net@gmail.com>2025-09-04 15:42:21 +0300
commit404cff82fe27cc8e00b5eee4d41f825c4ba3a569 (patch)
treeaaa01e966981a9598ab760ef8ae2bf9ea6ec0576 /Software/Visual_Studio/Web/Tango.Portal
parentda102bf068b5a0734008cc576a20aef97ae0495b (diff)
downloadTango-404cff82fe27cc8e00b5eee4d41f825c4ba3a569.tar.gz
Tango-404cff82fe27cc8e00b5eee4d41f825c4ba3a569.zip
Twine portal <-> AI auth interaction.
Diffstat (limited to 'Software/Visual_Studio/Web/Tango.Portal')
-rw-r--r--Software/Visual_Studio/Web/Tango.Portal/Controllers/HomeController.cs8
-rw-r--r--Software/Visual_Studio/Web/Tango.Portal/Tango.Portal.csproj1
-rw-r--r--Software/Visual_Studio/Web/Tango.Portal/Utils/SimpleCryptoHelper.cs224
-rw-r--r--Software/Visual_Studio/Web/Tango.Portal/Views/Shared/NavigationPartial.cshtml2
4 files changed, 234 insertions, 1 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)
{