- Pre-boot password gate (main.js) with language selector; backend starts only after unlock. - keylock.js: scrypt KEK + AES-256-GCM wrap/unwrap of the SSB secret and keyrings. - crypto.js: keyrings encrypted/decrypted with the KEK. - ssb_config.js: decrypted keys in memory (no plaintext secret on disk).
219 lines
18 KiB
JavaScript
219 lines
18 KiB
JavaScript
console.log('[Oasis Mobile] Node.js backend starting...');
|
|
|
|
process.env.HOME = process.env.HOME || require('path').resolve(__dirname, '..', '..');
|
|
process.env.ssb_appname = process.env.ssb_appname || 'ssb';
|
|
process.env.OASIS_MOBILE = '1';
|
|
process.env.CHLORIDE_JS = '1';
|
|
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
const tmpDir = path.join(process.env.HOME, 'tmp');
|
|
try { fs.mkdirSync(tmpDir, { recursive: true }); } catch (e) {}
|
|
process.env.TMPDIR = tmpDir;
|
|
process.env.TMP = tmpDir;
|
|
process.env.TEMP = tmpDir;
|
|
try { require('os').tmpdir = () => tmpDir; } catch (e) {}
|
|
|
|
try {
|
|
const dns = require('dns');
|
|
const net = require('net');
|
|
const servers = ['8.8.8.8', '1.1.1.1', '8.8.4.4'];
|
|
try { dns.setServers(servers); } catch (e) {}
|
|
try { if (dns.promises && dns.promises.setServers) dns.promises.setServers(servers); } catch (e) {}
|
|
|
|
const localResolve = (hostname, family) => {
|
|
if (hostname === 'localhost' || hostname === 'localhost.localdomain' || hostname === 'ip6-localhost') {
|
|
return family === 6 ? { address: '::1', family: 6 } : { address: '127.0.0.1', family: 4 };
|
|
}
|
|
const ipv = net.isIP(hostname);
|
|
if (ipv) return { address: hostname, family: ipv };
|
|
return null;
|
|
};
|
|
|
|
const safeLookup = function (hostname, options, callback) {
|
|
if (typeof options === 'function') { callback = options; options = {}; }
|
|
options = options || {};
|
|
const family = options.family === 6 ? 6 : 4;
|
|
const direct = localResolve(hostname, family);
|
|
if (direct) {
|
|
return process.nextTick(() => options.all
|
|
? callback(null, [direct])
|
|
: callback(null, direct.address, direct.family));
|
|
}
|
|
const resolver = family === 6 ? dns.resolve6 : dns.resolve4;
|
|
resolver.call(dns, hostname, (err, addrs) => {
|
|
if (err || !addrs || !addrs.length) return callback(err || new Error('ENOTFOUND ' + hostname));
|
|
if (options.all) return callback(null, addrs.map((a) => ({ address: a, family })));
|
|
return callback(null, addrs[0], family);
|
|
});
|
|
};
|
|
|
|
dns.lookup = safeLookup;
|
|
if (dns.promises) {
|
|
dns.promises.lookup = (hostname, options) => new Promise((resolve, reject) => {
|
|
safeLookup(hostname, options || {}, (err, address, family) => {
|
|
if (err) return reject(err);
|
|
if (options && options.all) return resolve(address);
|
|
resolve({ address, family });
|
|
});
|
|
});
|
|
}
|
|
} catch (e) {}
|
|
|
|
const backendPath = path.join(__dirname, 'src', 'backend', 'backend.js');
|
|
|
|
console.log('[Oasis] Launching Oasis Social Network Utopia...');
|
|
console.log('[Oasis] Backend path:', backendPath);
|
|
console.log('[Oasis] HOME:', process.env.HOME);
|
|
|
|
const http = require('http');
|
|
const keylock = require('./src/models/keylock');
|
|
const configPath = path.join(process.env.HOME, '.ssb');
|
|
|
|
let backendStarted = false;
|
|
function startBackend() {
|
|
if (backendStarted) return;
|
|
backendStarted = true;
|
|
try {
|
|
require(backendPath);
|
|
console.log('[Oasis] Backend started on port 3000');
|
|
} catch (error) {
|
|
console.error('[Oasis] Error starting backend:', error);
|
|
}
|
|
}
|
|
|
|
function unlockAndStart(keys) {
|
|
global.__OASIS_KEYS__ = keys;
|
|
for (const s of sockets) { try { s.destroy(); } catch (_) {} }
|
|
try { lockServer.close(() => startBackend()); } catch (_) { startBackend(); }
|
|
}
|
|
|
|
const MODE = keylock.isConfigured(configPath) ? 'unlock' : 'setup';
|
|
|
|
const LOCK_LANGS = ['en','es','fr','eu','de','it','pt','zh','ar','hi','ru'];
|
|
const LOCK_I18N = {
|
|
en: { name:"English", t_setup:"Create your password", t_unlock:"Unlock Oasis", s_setup:"It encrypts your identity and keys on this device. <b>If you forget it, you lose the account</b> (no recovery).", s_unlock:"Enter your password to decrypt your keys and start Oasis.", ph:"Password", ph2:"Repeat password", b_setup:"Create and enter", b_unlock:"Enter", e_min:"Minimum 6 characters", e_mism:"Passwords do not match", e_wrong:"Incorrect password", e_pre:"Error: ", m_unlock:"Unlocking…" },
|
|
es: { name:"Español", t_setup:"Crea tu contraseña", t_unlock:"Desbloquea Oasis", s_setup:"Cifrará tu identidad y tus claves en este dispositivo. <b>Si la olvidas, pierdes la cuenta</b> (no hay recuperación).", s_unlock:"Introduce tu contraseña para descifrar tus claves y arrancar Oasis.", ph:"Contraseña", ph2:"Repite la contraseña", b_setup:"Crear y entrar", b_unlock:"Entrar", e_min:"Mínimo 6 caracteres", e_mism:"Las contraseñas no coinciden", e_wrong:"Contraseña incorrecta", e_pre:"Error: ", m_unlock:"Desbloqueando…" },
|
|
fr: { name:"Français", t_setup:"Créez votre mot de passe", t_unlock:"Déverrouiller Oasis", s_setup:"Il chiffre votre identité et vos clés sur cet appareil. <b>Si vous l'oubliez, vous perdez le compte</b> (aucune récupération).", s_unlock:"Saisissez votre mot de passe pour déchiffrer vos clés et démarrer Oasis.", ph:"Mot de passe", ph2:"Répétez le mot de passe", b_setup:"Créer et entrer", b_unlock:"Entrer", e_min:"6 caractères minimum", e_mism:"Les mots de passe ne correspondent pas", e_wrong:"Mot de passe incorrect", e_pre:"Erreur : ", m_unlock:"Déverrouillage…" },
|
|
eu: { name:"Euskara", t_setup:"Sortu zure pasahitza", t_unlock:"Desblokeatu Oasis", s_setup:"Zure identitatea eta gakoak gailu honetan zifratzen ditu. <b>Ahazten baduzu, kontua galtzen duzu</b> (ez dago berreskuratzerik).", s_unlock:"Sartu zure pasahitza gakoak deszifratu eta Oasis abiarazteko.", ph:"Pasahitza", ph2:"Errepikatu pasahitza", b_setup:"Sortu eta sartu", b_unlock:"Sartu", e_min:"Gutxienez 6 karaktere", e_mism:"Pasahitzak ez datoz bat", e_wrong:"Pasahitz okerra", e_pre:"Errorea: ", m_unlock:"Desblokeatzen…" },
|
|
de: { name:"Deutsch", t_setup:"Erstelle dein Passwort", t_unlock:"Oasis entsperren", s_setup:"Es verschlüsselt deine Identität und Schlüssel auf diesem Gerät. <b>Wenn du es vergisst, verlierst du das Konto</b> (keine Wiederherstellung).", s_unlock:"Gib dein Passwort ein, um deine Schlüssel zu entschlüsseln und Oasis zu starten.", ph:"Passwort", ph2:"Passwort wiederholen", b_setup:"Erstellen und eintreten", b_unlock:"Eintreten", e_min:"Mindestens 6 Zeichen", e_mism:"Passwörter stimmen nicht überein", e_wrong:"Falsches Passwort", e_pre:"Fehler: ", m_unlock:"Entsperren…" },
|
|
it: { name:"Italiano", t_setup:"Crea la tua password", t_unlock:"Sblocca Oasis", s_setup:"Cifra la tua identità e le tue chiavi su questo dispositivo. <b>Se la dimentichi, perdi l'account</b> (nessun recupero).", s_unlock:"Inserisci la password per decifrare le chiavi e avviare Oasis.", ph:"Password", ph2:"Ripeti la password", b_setup:"Crea ed entra", b_unlock:"Entra", e_min:"Minimo 6 caratteri", e_mism:"Le password non coincidono", e_wrong:"Password errata", e_pre:"Errore: ", m_unlock:"Sblocco…" },
|
|
pt: { name:"Português", t_setup:"Crie a sua palavra-passe", t_unlock:"Desbloquear o Oasis", s_setup:"Cifra a sua identidade e chaves neste dispositivo. <b>Se a esquecer, perde a conta</b> (sem recuperação).", s_unlock:"Introduza a sua palavra-passe para decifrar as chaves e iniciar o Oasis.", ph:"Palavra-passe", ph2:"Repita a palavra-passe", b_setup:"Criar e entrar", b_unlock:"Entrar", e_min:"Mínimo 6 caracteres", e_mism:"As palavras-passe não coincidem", e_wrong:"Palavra-passe incorreta", e_pre:"Erro: ", m_unlock:"A desbloquear…" },
|
|
zh: { name:"中文", t_setup:"创建你的密码", t_unlock:"解锁 Oasis", s_setup:"它会在此设备上加密你的身份和密钥。<b>如果忘记,你将失去账户</b>(无法恢复)。", s_unlock:"输入密码以解密你的密钥并启动 Oasis。", ph:"密码", ph2:"重复密码", b_setup:"创建并进入", b_unlock:"进入", e_min:"至少 6 个字符", e_mism:"密码不一致", e_wrong:"密码错误", e_pre:"错误:", m_unlock:"正在解锁…" },
|
|
ar: { name:"العربية", t_setup:"أنشئ كلمة المرور", t_unlock:"فتح Oasis", s_setup:"يشفّر هويتك ومفاتيحك على هذا الجهاز. <b>إذا نسيتها، تفقد الحساب</b> (لا استعادة).", s_unlock:"أدخل كلمة المرور لفك تشفير مفاتيحك وتشغيل Oasis.", ph:"كلمة المرور", ph2:"أعد كلمة المرور", b_setup:"إنشاء ودخول", b_unlock:"دخول", e_min:"6 أحرف على الأقل", e_mism:"كلمتا المرور غير متطابقتين", e_wrong:"كلمة مرور غير صحيحة", e_pre:"خطأ: ", m_unlock:"جارٍ الفتح…" },
|
|
hi: { name:"हिन्दी", t_setup:"अपना पासवर्ड बनाएं", t_unlock:"Oasis अनलॉक करें", s_setup:"यह इस डिवाइस पर आपकी पहचान और कुंजियाँ एन्क्रिप्ट करता है। <b>भूल गए तो खाता खो देंगे</b> (कोई रिकवरी नहीं)।", s_unlock:"अपनी कुंजियाँ डिक्रिप्ट करने और Oasis शुरू करने के लिए पासवर्ड दर्ज करें।", ph:"पासवर्ड", ph2:"पासवर्ड दोहराएँ", b_setup:"बनाएँ और प्रवेश करें", b_unlock:"प्रवेश करें", e_min:"कम से कम 6 अक्षर", e_mism:"पासवर्ड मेल नहीं खाते", e_wrong:"गलत पासवर्ड", e_pre:"त्रुटि: ", m_unlock:"अनलॉक हो रहा है…" },
|
|
ru: { name:"Русский", t_setup:"Создайте пароль", t_unlock:"Разблокировать Oasis", s_setup:"Он шифрует вашу личность и ключи на этом устройстве. <b>Если забудете, потеряете аккаунт</b> (без восстановления).", s_unlock:"Введите пароль, чтобы расшифровать ключи и запустить Oasis.", ph:"Пароль", ph2:"Повторите пароль", b_setup:"Создать и войти", b_unlock:"Войти", e_min:"Минимум 6 символов", e_mism:"Пароли не совпадают", e_wrong:"Неверный пароль", e_pre:"Ошибка: ", m_unlock:"Разблокировка…" },
|
|
};
|
|
|
|
function lockPage(mode) {
|
|
const confirmField = mode === 'setup'
|
|
? '<input id="c" type="password" inputmode="text" autocomplete="new-password" />'
|
|
: '';
|
|
return '<!doctype html><!-- OASIS_LOCK_MARKER --><html lang="en"><head><meta charset="utf-8">'
|
|
+ '<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">'
|
|
+ '<title>Oasis</title><style>'
|
|
+ '*{box-sizing:border-box}body{margin:0;background:#121212;color:#eee;font-family:system-ui,sans-serif;'
|
|
+ 'min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}'
|
|
+ '.card{width:100%;max-width:360px;text-align:center}h1{color:#FFD700;font-size:22px;margin:0 0 8px}'
|
|
+ 'p{color:#aaa;font-size:14px;line-height:1.4;margin:0 0 20px}'
|
|
+ 'input{width:100%;padding:14px;margin:8px 0;border-radius:10px;border:1px solid #333;'
|
|
+ 'background:#1c1c1c;color:#eee;font-size:16px}'
|
|
+ 'button[type=submit]{width:100%;padding:14px;margin-top:12px;border:0;border-radius:10px;background:#FFD700;'
|
|
+ 'color:#111;font-size:16px;font-weight:600}button[type=submit]:disabled{opacity:.5}'
|
|
+ '#err{color:#ff6b6b;font-size:13px;min-height:18px;margin-top:8px}#msg{color:#FFD700;font-size:14px}'
|
|
+ '#langbtn{position:fixed;top:calc(env(safe-area-inset-top,0px) + 12px);right:12px;width:42px;height:42px;'
|
|
+ 'border-radius:50%;border:1px solid #333;background:#1c1c1c;color:#FFD700;font-size:13px;font-weight:700;'
|
|
+ 'padding:0;z-index:10;cursor:pointer;display:flex;align-items:center;justify-content:center}'
|
|
+ '.langmenu{position:fixed;top:calc(env(safe-area-inset-top,0px) + 60px);right:12px;background:#1c1c1c;'
|
|
+ 'border:1px solid #333;border-radius:12px;padding:6px;z-index:11;max-height:70vh;overflow:auto;min-width:150px;'
|
|
+ 'box-shadow:0 6px 20px rgba(0,0,0,.5)}'
|
|
+ '.langmenu button{display:block;width:100%;text-align:left;background:transparent;color:#eee;border:0;'
|
|
+ 'border-radius:8px;padding:11px 12px;font-size:15px;margin:0;font-weight:400;cursor:pointer}'
|
|
+ '.langmenu button.cur{color:#FFD700;font-weight:700;background:#262626}'
|
|
+ '[dir=rtl] #langbtn,[dir=rtl] .langmenu{right:auto;left:12px}[dir=rtl] .langmenu button{text-align:right}'
|
|
+ '</style></head><body>'
|
|
+ '<button id="langbtn" type="button" aria-label="Language">EN</button>'
|
|
+ '<div id="langmenu" class="langmenu" hidden></div>'
|
|
+ '<div class="card"><h1 id="title"></h1><p id="sub"></p>'
|
|
+ '<form id="f"><input id="p" type="password" inputmode="text" autocomplete="' + (mode === 'setup' ? 'new-password' : 'current-password') + '" autofocus />'
|
|
+ confirmField
|
|
+ '<button type="submit" id="btn"></button>'
|
|
+ '<div id="err"></div><div id="msg"></div></form></div>'
|
|
+ '<script>'
|
|
+ 'var MODE="' + mode + '";var I18N=' + JSON.stringify(LOCK_I18N) + ';var LANGS=' + JSON.stringify(LOCK_LANGS) + ';'
|
|
+ 'var $=function(id){return document.getElementById(id);};'
|
|
+ 'function getLang(){try{var s=localStorage.getItem("oasisLockLang");if(s&&I18N[s])return s;}catch(e){}return "en";}'
|
|
+ 'var LANG=getLang();'
|
|
+ 'function setCookie(l){try{document.cookie="language="+l+";path=/;max-age=31536000";}catch(e){}}'
|
|
+ 'try{if(localStorage.getItem("oasisLockLang"))setCookie(LANG);}catch(e){}'
|
|
+ 'function apply(){var t=I18N[LANG]||I18N.en;'
|
|
+ 'document.documentElement.lang=LANG;document.documentElement.dir=(LANG==="ar")?"rtl":"ltr";'
|
|
+ '$("title").innerHTML=(MODE==="setup")?t.t_setup:t.t_unlock;'
|
|
+ '$("sub").innerHTML=(MODE==="setup")?t.s_setup:t.s_unlock;'
|
|
+ '$("p").placeholder=t.ph;var c=$("c");if(c)c.placeholder=t.ph2;'
|
|
+ '$("btn").textContent=(MODE==="setup")?t.b_setup:t.b_unlock;'
|
|
+ '$("langbtn").textContent=LANG.toUpperCase();}'
|
|
+ 'function buildMenu(){var m=$("langmenu");m.innerHTML="";LANGS.forEach(function(l){'
|
|
+ 'var b=document.createElement("button");b.type="button";b.textContent=(I18N[l]&&I18N[l].name)||l;'
|
|
+ 'if(l===LANG)b.className="cur";b.onclick=function(){LANG=l;try{localStorage.setItem("oasisLockLang",l);}catch(e){}'
|
|
+ 'setCookie(l);apply();buildMenu();m.hidden=true;};m.appendChild(b);});}'
|
|
+ '$("langbtn").onclick=function(){var m=$("langmenu");m.hidden=!m.hidden;};'
|
|
+ 'document.addEventListener("click",function(e){var m=$("langmenu");if(!m.hidden&&e.target!==$("langbtn")&&!m.contains(e.target))m.hidden=true;});'
|
|
+ 'buildMenu();apply();'
|
|
+ 'var f=$("f"),err=$("err"),msg=$("msg");'
|
|
+ 'f.addEventListener("submit",async function(e){e.preventDefault();var t=I18N[LANG]||I18N.en;err.textContent="";'
|
|
+ 'var p=$("p").value,c=$("c");'
|
|
+ 'if(MODE==="setup"){if(p.length<6){err.textContent=t.e_min;return;}if(c&&c.value!==p){err.textContent=t.e_mism;return;}}'
|
|
+ 'var btn=$("btn");btn.disabled=true;'
|
|
+ 'try{var r=await fetch(MODE==="setup"?"/__keylock/setup":"/__keylock/unlock",{method:"POST",'
|
|
+ 'headers:{"Content-Type":"application/json"},body:JSON.stringify({password:p})});'
|
|
+ 'if(r.ok){msg.textContent=t.m_unlock;waitReload();}else{err.textContent=t.e_wrong;btn.disabled=false;}}'
|
|
+ 'catch(e2){err.textContent=t.e_pre+e2.message;btn.disabled=false;}});'
|
|
+ 'async function waitReload(){for(var i=0;i<120;i++){try{var r=await fetch("/?_k="+Date.now(),{cache:"no-store"});'
|
|
+ 'var t=await r.text();if(t.indexOf("OASIS_LOCK_MARKER")===-1){location.reload();return;}}catch(e){}'
|
|
+ 'await new Promise(function(s){setTimeout(s,500);});}location.reload();}'
|
|
+ '</script></body></html>';
|
|
}
|
|
|
|
function readBody(req) {
|
|
return new Promise((resolve) => {
|
|
let b = '';
|
|
req.on('data', (d) => { b += d; if (b.length > 1e5) req.destroy(); });
|
|
req.on('end', () => { try { resolve(JSON.parse(b || '{}')); } catch (_) { resolve({}); } });
|
|
req.on('error', () => resolve({}));
|
|
});
|
|
}
|
|
|
|
const sockets = new Set();
|
|
const lockServer = http.createServer(async (req, res) => {
|
|
try {
|
|
if (req.method === 'POST' && req.url === '/__keylock/setup') {
|
|
const { password } = await readBody(req);
|
|
if (!password || String(password).length < 6) { res.writeHead(400, { 'Connection': 'close' }); return res.end('{"ok":false}'); }
|
|
keylock.setPassword(configPath, password);
|
|
const keys = keylock.loadOrCreateSecret(configPath);
|
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Connection': 'close' });
|
|
res.end('{"ok":true}');
|
|
return unlockAndStart(keys);
|
|
}
|
|
if (req.method === 'POST' && req.url === '/__keylock/unlock') {
|
|
const { password } = await readBody(req);
|
|
const kek = password ? keylock.verifyPassword(configPath, password) : null;
|
|
if (!kek) { res.writeHead(401, { 'Connection': 'close' }); return res.end('{"ok":false}'); }
|
|
const keys = keylock.loadOrCreateSecret(configPath);
|
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Connection': 'close' });
|
|
res.end('{"ok":true}');
|
|
return unlockAndStart(keys);
|
|
}
|
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Connection': 'close' });
|
|
res.end(lockPage(MODE));
|
|
} catch (e) {
|
|
res.writeHead(500, { 'Connection': 'close' });
|
|
res.end('{"ok":false,"error":"' + String(e && e.message).replace(/"/g, '') + '"}');
|
|
}
|
|
});
|
|
lockServer.on('connection', (s) => { sockets.add(s); s.on('close', () => sockets.delete(s)); });
|
|
lockServer.listen(3000, '127.0.0.1', () => {
|
|
console.log('[Oasis] keylock gate escuchando en 127.0.0.1:3000 (modo: ' + MODE + ')');
|
|
});
|