diff --git a/frontend/index.html b/frontend/index.html index 284fa5e..cedf839 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -73,7 +73,12 @@

CloudSSH

-
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1ca224c..df6dfcc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,7 +10,9 @@ "dependencies": { "@xterm/addon-fit": "^0.10.0", "@xterm/addon-web-links": "^0.11.0", - "@xterm/xterm": "^5.5.0" + "@xterm/addon-webgl": "^0.19.0", + "@xterm/xterm": "^5.5.0", + "zmodem.js": "^0.1.10" }, "devDependencies": { "typescript": "^5.7.0", @@ -848,12 +850,30 @@ "@xterm/xterm": "^5.0.0" } }, + "node_modules/@xterm/addon-webgl": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0.tgz", + "integrity": "sha512-b3fMOsyLVuCeNJWxolACEUED0vm7qC0cy4wRvf3oURSzDTYVQiGPhTnhWZwIHdvC48Y+oLhvYXnY4XDXPoJo6A==", + "license": "MIT" + }, "node_modules/@xterm/xterm": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", "license": "MIT" }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/esbuild": { "version": "0.25.12", "dev": true, @@ -1155,6 +1175,15 @@ "@rollup/rollup-win32-x64-msvc": "4.62.0", "fsevents": "~2.3.2" } + }, + "node_modules/zmodem.js": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/zmodem.js/-/zmodem.js-0.1.10.tgz", + "integrity": "sha512-Z1DWngunZ/j3BmIzSJpFZVNV73iHkj89rxXX4IciJdU9ga3nZ7rJ5LkfjV/QDsKhc7bazDWTTJCLJ+iRXD82dw==", + "license": "Apache-2.0", + "dependencies": { + "crc-32": "^1.1.1" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 42ef281..5f1c092 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,9 +8,11 @@ "preview": "vite preview" }, "dependencies": { - "@xterm/xterm": "^5.5.0", "@xterm/addon-fit": "^0.10.0", - "@xterm/addon-web-links": "^0.11.0" + "@xterm/addon-web-links": "^0.11.0", + "@xterm/addon-webgl": "^0.19.0", + "@xterm/xterm": "^5.5.0", + "zmodem.js": "^0.1.10" }, "devDependencies": { "typescript": "^5.7.0", diff --git a/frontend/src/auth-form.ts b/frontend/src/auth-form.ts index 15c3384..57cc6cd 100644 --- a/frontend/src/auth-form.ts +++ b/frontend/src/auth-form.ts @@ -1,11 +1,53 @@ import { SSHTerminal } from './terminal'; +// --- Credential encryption helpers --- +async function deriveKey(salt: Uint8Array): Promise { + const raw = new TextEncoder().encode(window.location.origin + ':cloudssh'); + const baseKey = await crypto.subtle.importKey('raw', raw, 'PBKDF2', false, ['deriveKey']); + return crypto.subtle.deriveKey( + { name: 'PBKDF2', salt: salt as any, iterations: 100000, hash: 'SHA-256' }, + baseKey, + { name: 'AES-GCM', length: 256 }, + false, + ['encrypt', 'decrypt'] + ); +} + +async function encryptCredentials(data: object): Promise { + const salt = crypto.getRandomValues(new Uint8Array(16)); + const iv = crypto.getRandomValues(new Uint8Array(12)); + const key = await deriveKey(salt); + const encoded = new TextEncoder().encode(JSON.stringify(data)); + const encrypted = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded)); + // Format: base64(salt + iv + ciphertext) + const combined = new Uint8Array(salt.length + iv.length + encrypted.length); + combined.set(salt, 0); + combined.set(iv, salt.length); + combined.set(encrypted, salt.length + iv.length); + return btoa(String.fromCharCode(...combined)); +} + +async function decryptCredentials(stored: string): Promise<{ host: string; port: string; username: string; password: string } | null> { + try { + const raw = Uint8Array.from(atob(stored), c => c.charCodeAt(0)); + const salt = raw.slice(0, 16); + const iv = raw.slice(16, 28); + const data = raw.slice(28); + const key = await deriveKey(salt); + const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, data); + return JSON.parse(new TextDecoder().decode(decrypted)); + } catch { + return null; + } +} + export class ConnectionForm { private terminal: SSHTerminal; constructor(terminal: SSHTerminal) { this.terminal = terminal; this.render(); + this.loadSavedCredentials(); } private render(): void { @@ -37,13 +79,26 @@ export class ConnectionForm {
- -
- key - + +
+ + +
+
+
+ key + +
+
+
-
+
+ + +
+