#include <M5StickCPlus2.h>
#include <BleKeyboard.h>
#include "mbedtls/md.h"
#include <vector>
#include <WiFi.h>
#include <WebServer.h>
#include <DNSServer.h>
#include <Preferences.h>
#include <WiFiManager.h>
#include <WiFiClientSecure.h>
#include <HTTPUpdate.h>
BleKeyboard bleKeyboard("M5 Vault", "Espressif", 100);
Preferences preferences;
WebServer server(80);
DNSServer dnsServer;
const byte DNS_PORT = 53;
// ==========================================
// 🛑 CONFIGURATION 🛑
// ==========================================
String CONFIG_PIN = "0000"; // Enter this at the lock screen to launch the Wi-Fi portal
const unsigned long AUTO_LOCK_MS = 60000; // 60 seconds of inactivity
// ==========================================
std::vector<String> services;
int currentIndex = 0;
// Device States
enum DeviceMode { LOCKED, VAULT_MODE, CONFIG_MODE };
DeviceMode currentMode = LOCKED;
int pinDigits[4] = {0, 0, 0, 0};
int currentDigitIndex = 0;
String derivedMasterKey = "";
unsigned long lastActivityTime = 0;
bool lastBleState = false;
// ==========================================
// AUDIO FEEDBACK HELPERS
// ==========================================
void playClick() {
StickCP2.Speaker.tone(4000, 30);
}
void playError() {
StickCP2.Speaker.tone(1000, 300);
}
void playSuccess() {
StickCP2.Speaker.tone(5000, 100);
delay(100);
StickCP2.Speaker.tone(7000, 150);
}
// ==========================================
// NVRAM & BATTERY HELPERS
// ==========================================
void loadServices() {
preferences.begin("m5vault", false);
String savedApps = preferences.getString("apps", "Gmail,SAP_Portal,HDFC_Bank,BGMI_Account");
preferences.end();
services.clear();
int start = 0;
int end = savedApps.indexOf(',');
while (end != -1) {
services.push_back(savedApps.substring(start, end));
start = end + 1;
end = savedApps.indexOf(',', start);
}
services.push_back(savedApps.substring(start));
}
void drawBattery() {
int batLevel = StickCP2.Power.getBatteryLevel();
int x = 185; int y = 6; int w = 42; int h = 14;
StickCP2.Display.drawRect(x, y, w, h, TFT_DARKCYAN);
StickCP2.Display.fillRect(x + w, y + 4, 3, 6, TFT_DARKCYAN);
uint16_t fillColor = TFT_DARKGREEN;
if (batLevel <= 20) fillColor = TFT_MAROON;
else if (batLevel <= 50) fillColor = TFT_DARKGREY;
int fillWidth = (w - 4) * batLevel / 100;
if (fillWidth > 0) {
StickCP2.Display.fillRect(x + 2, y + 2, fillWidth, h - 4, fillColor);
}
StickCP2.Display.setTextSize(1);
StickCP2.Display.setTextColor(TFT_WHITE);
if (batLevel == 100) StickCP2.Display.setCursor(x + 4, y + 4);
else if (batLevel < 10) StickCP2.Display.setCursor(x + 13, y + 4);
else StickCP2.Display.setCursor(x + 7, y + 4);
StickCP2.Display.printf("%d%%", batLevel);
}
// ==========================================
// MAIN SETUP & LOOP
// ==========================================
void setup() {
auto cfg = M5.config();
StickCP2.begin(cfg);
StickCP2.Display.setBrightness(128);
StickCP2.Display.setRotation(1);
StickCP2.Speaker.setVolume(100);
loadServices();
lastActivityTime = millis();
}
void loop() {
StickCP2.update();
if (StickCP2.BtnA.wasPressed() || StickCP2.BtnB.wasPressed()) {
lastActivityTime = millis();
}
if (currentMode == LOCKED) {
handleLockScreen();
} else if (currentMode == VAULT_MODE) {
handleVaultMode();
} else if (currentMode == CONFIG_MODE) {
dnsServer.processNextRequest();
server.handleClient();
}
}
// ==========================================
// MODE HANDLERS
// ==========================================
void handleLockScreen() {
if (StickCP2.BtnB.wasPressed()) {
playClick();
pinDigits[currentDigitIndex] = (pinDigits[currentDigitIndex] + 1) % 10;
drawLockScreen();
}
if (StickCP2.BtnA.wasPressed()) {
currentDigitIndex++;
if (currentDigitIndex >= 4) {
derivedMasterKey = String(pinDigits[0]) + String(pinDigits[1]) + String(pinDigits[2]) + String(pinDigits[3]);
if (derivedMasterKey == CONFIG_PIN) {
playSuccess();
startConfigPortal();
} else {
playSuccess();
currentMode = VAULT_MODE;
bleKeyboard.begin();
drawUI();
}
} else {
playClick();
drawLockScreen();
}
}
}
void handleVaultMode() {
if (millis() - lastActivityTime > AUTO_LOCK_MS) {
currentMode = LOCKED;
currentDigitIndex = 0;
pinDigits[0] = 0; pinDigits[1] = 0; pinDigits[2] = 0; pinDigits[3] = 0;
derivedMasterKey = "";
if (bleKeyboard.isConnected()) bleKeyboard.end();
drawLockScreen();
return;
}
bool currentBleState = bleKeyboard.isConnected();
if (currentBleState != lastBleState) {
lastBleState = currentBleState;
drawUI();
}
if (StickCP2.BtnB.wasPressed()) {
playClick();
currentIndex = (currentIndex + 1) % services.size();
drawUI();
}
if (StickCP2.BtnA.wasPressed()) {
if (bleKeyboard.isConnected()) {
playSuccess();
StickCP2.Display.fillScreen(TFT_BLACK);
StickCP2.Display.drawRect(10, 10, 220, 115, TFT_GREEN);
StickCP2.Display.setTextColor(TFT_GREEN);
StickCP2.Display.setTextSize(2);
StickCP2.Display.setCursor(30, 45);
StickCP2.Display.print("EXECUTING...");
StickCP2.Display.setTextSize(1);
StickCP2.Display.setCursor(30, 75);
StickCP2.Display.print("INJECTING PAYLOAD");
String password = generateStatelessPassword(derivedMasterKey, services[currentIndex], 16);
// SLOW-TYPE FIX: Type one character at a time to prevent BLE buffer overflow
for (int i = 0; i < password.length(); i++) {
bleKeyboard.print(password[i]);
delay(25); // 25ms gap gives your PC time to register the key release
}
// SAFETY CATCH: Force the OS to lift all virtual keys just in case
bleKeyboard.releaseAll();
password = "";
delay(600);
lastActivityTime = millis();
drawUI();
} else {
playError();
StickCP2.Display.fillScreen(TFT_BLACK);
StickCP2.Display.drawRect(10, 10, 220, 115, TFT_RED);
StickCP2.Display.setTextColor(TFT_RED);
StickCP2.Display.setTextSize(2);
StickCP2.Display.setCursor(30, 55);
StickCP2.Display.print("[NO SIGNAL]");
delay(1000);
drawUI();
}
}
}
// ==========================================
// CAPTIVE PORTAL & WEB SERVER
// ==========================================
void startConfigPortal() {
currentMode = CONFIG_MODE;
WiFi.softAP("M5_Vault", "password123");
dnsServer.start(DNS_PORT, "*", WiFi.softAPIP());
server.onNotFound([]() {
server.sendHeader("Location", String("http://") + WiFi.softAPIP().toString(), true);
server.send(302, "text/plain", "");
});
server.on("/", HTTP_GET, []() {
preferences.begin("m5vault", true);
String currentList = preferences.getString("apps", "Gmail,SAP_Portal,HDFC_Bank,BGMI_Account");
preferences.end();
String html = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>M5 Vault Terminal</title>
<style>
:root { --bg: #050505; --panel: #0a0a0a; --text: #00ff00; --accent: #00ffcc; --border: #222; }
body { background: var(--bg); color: var(--text); font-family: "Courier New", Courier, monospace; display: flex; justify-content: center; padding: 20px; margin: 0; }
.vault-card { background: var(--panel); border: 1px solid var(--border); box-shadow: 0 0 20px rgba(0, 255, 204, 0.05); padding: 30px; width: 100%; max-width: 450px; position: relative; overflow: hidden; }
.vault-card::before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 2px; background: rgba(0, 255, 204, 0.3); animation: scan 4s linear infinite; pointer-events: none; }
@keyframes scan { 0% { top: -10%; } 100% { top: 110%; } }
h2 { margin: 0 0 5px; color: var(--accent); text-transform: uppercase; font-size: 22px; letter-spacing: 2px; }
.status { font-size: 11px; color: #555; margin-bottom: 25px; text-transform: uppercase; letter-spacing: 1px; }
textarea { width: 100%; background: #000; color: var(--text); border: 1px solid var(--border); padding: 15px; box-sizing: border-box; font-family: inherit; font-size: 15px; min-height: 180px; outline: none; transition: 0.3s; line-height: 1.5; }
textarea:focus { border-color: var(--accent); box-shadow: inset 0 0 15px rgba(0, 255, 204, 0.1); }
button { background: transparent; color: var(--accent); border: 1px solid var(--accent); padding: 15px; width: 100%; margin-top: 25px; font-family: inherit; font-size: 16px; font-weight: bold; text-transform: uppercase; letter-spacing: 2px; cursor: pointer; transition: 0.3s; }
button:hover { background: var(--accent); color: #000; box-shadow: 0 0 20px var(--accent); }
</style>
</head>
<body>
<div class="vault-card">
<h2>SYS.VAULT_CONFIG</h2>
<div class="status">CONNECTION: SECURE | ENCRYPTION: SHA-256</div>
<form action="/save" method="POST">
<textarea name="apps" spellcheck="false" placeholder="Enter_Services_Here...">)rawliteral" + currentList + R"rawliteral(</textarea>
<button type="submit">Initialize Flash Write</button>
</form>
</div>
</body>
</html>
)rawliteral";
server.send(200, "text/html", html);
});
server.on("/save", HTTP_POST, []() {
String newApps = server.arg("apps");
newApps.replace(" ", "");
newApps.replace("\n", "");
newApps.replace("\r", "");
preferences.begin("m5vault", false);
preferences.putString("apps", newApps);
preferences.end();
String successHtml = R"rawliteral(
<!DOCTYPE html><html><head><meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>body{background:#050505;color:#00ffcc;font-family:"Courier New",monospace;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;text-align:center;} .box{border:1px solid #222;padding:40px;box-shadow:0 0 20px rgba(0,255,204,0.1);} h1{margin:0 0 10px 0;font-size:24px;text-transform:uppercase;letter-spacing:2px;} p{color:#00ff00;font-size:14px;}</style></head>
<body><div class="box"><h1>[ NVRAM WRITE OK ]</h1><p>SYSTEM REBOOTING...<br>CONNECTION TERMINATED.</p></div></body></html>
)rawliteral";
server.send(200, "text/html", successHtml);
delay(1500);
ESP.restart();
});
server.begin();
StickCP2.Display.fillScreen(TFT_BLACK);
StickCP2.Display.fillRect(2, 2, 236, 25, TFT_CYAN);
StickCP2.Display.setTextColor(TFT_BLACK);
StickCP2.Display.setTextSize(1);
StickCP2.Display.setCursor(10, 8);
StickCP2.Display.print("SYS.HOSTING_PORTAL");
StickCP2.Display.drawRect(2, 2, 236, 131, TFT_CYAN);
StickCP2.Display.setTextSize(1);
StickCP2.Display.setTextColor(TFT_GREEN);
StickCP2.Display.setCursor(10, 45);
StickCP2.Display.print("SSID: M5_Vault");
StickCP2.Display.setCursor(10, 65);
StickCP2.Display.print("PASS: password123");
StickCP2.Display.setTextColor(TFT_YELLOW);
StickCP2.Display.setCursor(10, 105);
StickCP2.Display.print("AWAITING DNS CLIENT...");
}
// ==========================================
// HARDWARE UI RENDERING
// ==========================================
void drawLockScreen() {
StickCP2.Display.fillScreen(TFT_BLACK);
StickCP2.Display.drawRect(2, 2, 236, 131, TFT_DARKCYAN);
StickCP2.Display.drawLine(2, 25, 238, 25, TFT_DARKCYAN);
StickCP2.Display.setTextSize(1);
StickCP2.Display.setTextColor(TFT_GREEN);
StickCP2.Display.setCursor(10, 8);
StickCP2.Display.print("SYS.AUTH_MODULE");
StickCP2.Display.setTextSize(3);
for (int i = 0; i < 4; i++) {
int xPos = 40 + (i * 40);
StickCP2.Display.setCursor(xPos, 55);
if (i == currentDigitIndex) {
// Fixed: Green text, black background (No green box!)
StickCP2.Display.setTextColor(TFT_GREEN, TFT_BLACK);
StickCP2.Display.print(pinDigits[i]);
} else if (i < currentDigitIndex) {
StickCP2.Display.setTextColor(TFT_DARKGREY, TFT_BLACK);
StickCP2.Display.print("*");
} else {
StickCP2.Display.setTextColor(TFT_DARKCYAN, TFT_BLACK);
StickCP2.Display.print("_");
}
}
StickCP2.Display.drawLine(2, 110, 238, 110, TFT_DARKCYAN);
StickCP2.Display.setTextSize(1);
StickCP2.Display.setTextColor(TFT_DARKCYAN, TFT_BLACK);
StickCP2.Display.setCursor(10, 116);
StickCP2.Display.print("BTN.S: SHIFT");
StickCP2.Display.setCursor(140, 116);
StickCP2.Display.print("BTN.M: LOCK");
drawBattery();
}
void drawUI() {
StickCP2.Display.fillScreen(TFT_BLACK);
StickCP2.Display.drawRect(2, 2, 236, 131, TFT_DARKCYAN);
StickCP2.Display.drawLine(2, 25, 238, 25, TFT_DARKCYAN);
StickCP2.Display.setTextSize(1);
StickCP2.Display.setTextColor(TFT_DARKGREY);
StickCP2.Display.setCursor(10, 8);
StickCP2.Display.print("TARGET:");
StickCP2.Display.setCursor(120, 8);
if (bleKeyboard.isConnected()) {
StickCP2.Display.setTextColor(TFT_GREEN);
StickCP2.Display.print("[UPLINK]");
} else {
StickCP2.Display.setTextColor(TFT_RED);
StickCP2.Display.print("[NO_SIG]");
}
StickCP2.Display.fillRect(10, 45, 216, 40, TFT_DARKGREEN);
StickCP2.Display.drawRect(10, 45, 216, 40, TFT_GREEN);
StickCP2.Display.setTextSize(2);
StickCP2.Display.setTextColor(TFT_BLACK);
StickCP2.Display.setCursor(20, 56);
StickCP2.Display.print("> " + services[currentIndex]);
StickCP2.Display.drawLine(2, 110, 238, 110, TFT_DARKCYAN);
StickCP2.Display.setTextSize(1);
StickCP2.Display.setTextColor(TFT_DARKCYAN, TFT_BLACK);
StickCP2.Display.setCursor(10, 116);
StickCP2.Display.print("BTN.S: CYCLE");
StickCP2.Display.setCursor(130, 116);
StickCP2.Display.print("BTN.M: EXECUTE");
drawBattery();
}
// ==========================================
// CRYPTOGRAPHIC ENGINE
// ==========================================
String generateStatelessPassword(String masterKey, String service, int length) {
String combined = masterKey + "|" + service;
byte shaResult[32];
mbedtls_md_context_t ctx;
mbedtls_md_init(&ctx);
mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 0);
mbedtls_md_starts(&ctx);
mbedtls_md_update(&ctx, (const unsigned char*)combined.c_str(), combined.length());
mbedtls_md_finish(&ctx, shaResult);
mbedtls_md_free(&ctx);
String charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*";
String finalPassword = "";
for (int i = 0; i < length; i++) {
finalPassword += charset.charAt(shaResult[i % 32] % charset.length());
}
return finalPassword;
}
Comments