Compare commits

...
1 Commits
5 changed files with 76 additions and 5 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "mrterm", "name": "mrterm",
"version": "0.13.2", "version": "0.13.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mrterm", "name": "mrterm",
"version": "0.13.2", "version": "0.13.3",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@xterm/addon-fit": "^0.11.0", "@xterm/addon-fit": "^0.11.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "mrterm", "name": "mrterm",
"productName": "MrTerm", "productName": "MrTerm",
"version": "0.13.2", "version": "0.13.3",
"description": "Moderner SSH-, SFTP- und RDP-Client", "description": "Moderner SSH-, SFTP- und RDP-Client",
"main": "src/main/main.js", "main": "src/main/main.js",
"author": "MrBlake", "author": "MrBlake",
+45 -1
View File
@@ -210,4 +210,48 @@ async function makeCredential(ui, { timeoutMs = 60000 } = {}) {
} }
} }
module.exports = { makeCredential, CtapError, CANCEL_CODES, PIN }; // Entspricht WebAuthn-PRF (evalByCredential) über hmac-secret, ohne PIN (wie Chromium mit
// userVerification "discouraged"). creds: [{ credId: Buffer, salt: Buffer }] – salt ist der rohe PRF-Eingabewert.
// Liefert { credId: Buffer, secret: Buffer(32) }, oder null, wenn kein FIDO2-Gerät per hidraw erreichbar ist.
async function getHmacSecret(ui, creds, { timeoutMs = 60000 } = {}) {
let dev;
try { dev = Device.open(); } catch { return null; }
if (!dev) return null;
let timer;
try {
await dev.init();
const info = await dev.cbor(0x04);
if (!(info.get(1) || []).some((v) => String(v).startsWith('FIDO_2'))) return null;
const rpId = 'localhost';
const desc = (c) => new Map([['id', c.credId], ['type', 'public-key']]);
// Welches Credential liegt auf diesem Schlüssel? (Vorabprüfung ohne Berühren, up=false)
let cred = null;
for (const c of creds) {
try {
await dev.cbor(0x02, new Map([[1, rpId], [2, crypto.randomBytes(32)], [3, [desc(c)]], [5, new Map([['up', false]])]]));
cred = c; break;
} catch (e) { if (e.code !== 0x2e) throw e; }
}
if (!cred) throw new CtapError(0x2e);
const { key, platformKey } = await sharedSecret(dev);
const salt = crypto.createHash('sha256').update(Buffer.concat([Buffer.from('WebAuthn PRF\0', 'latin1'), cred.salt])).digest();
const saltEnc = aes('enc', key, salt);
const saltAuth = crypto.createHmac('sha256', key).update(saltEnc).digest().subarray(0, 16);
ui.touch(() => dev.cancel());
timer = setTimeout(() => dev.cancel(), timeoutMs);
const r = await dev.cbor(0x02, new Map([
[1, rpId], [2, crypto.randomBytes(32)], [3, [desc(cred)]],
[4, new Map([['hmac-secret', new Map([[1, platformKey], [2, saltEnc], [3, saltAuth]])]])],
]));
const authData = r.get(2);
if (!(authData[32] & 0x80)) throw new CtapError(-1);
const ext = dec(authData, 37)[0].get('hmac-secret');
if (!ext) throw new CtapError(-1);
return { credId: Buffer.from(cred.credId), secret: aes('dec', key, ext).subarray(0, 32) };
} finally {
clearTimeout(timer);
dev.close();
}
}
module.exports = { makeCredential, getHmacSecret, CtapError, CANCEL_CODES, PIN };
+24
View File
@@ -99,6 +99,26 @@ async function registerNative(parent) {
} }
} }
// PRF-Abfrage direkt per CTAP2 (Linux); null, wenn kein FIDO2-Gerät per hidraw erreichbar ist
async function deriveNative(parent, creds, text) {
const w = await openWindow(parent, text || i18n.t('Touch your security key to unlock MrTerm.'));
let abort = () => {};
let cancelled = false;
w.once('closed', () => { cancelled = true; abort(); });
try {
const r = await ctap2.getHmacSecret({ touch(a) { abort = a; if (cancelled) a(); } },
creds.map((c) => ({ credId: Buffer.from(c.credId, 'base64url'), salt: Buffer.from(c.prfSalt, 'base64url') })));
return r && { credId: b64url(r.credId), secret: r.secret };
} catch (e) {
if (cancelled || ctap2.CANCEL_CODES.has(e.code)) throw new Error(i18n.t('Security key prompt was cancelled or timed out.'));
if (e.code === 0x2e) throw new Error(i18n.t('Unknown security key.'));
if (e.code === -1) throw new Error(i18n.t('This security key does not support the hmac-secret/PRF extension.'));
throw e;
} finally {
if (!w.isDestroyed()) w.destroy();
}
}
const b64url = (buf) => Buffer.from(buf).toString('base64url'); const b64url = (buf) => Buffer.from(buf).toString('base64url');
// Neuen Schlüssel registrieren; liefert die Credential-ID (base64url) // Neuen Schlüssel registrieren; liefert die Credential-ID (base64url)
@@ -124,6 +144,10 @@ async function register(parent) {
// PRF-Wert für einen der Schlüssel abfragen. creds: [{ credId, prfSalt }] (base64url) // PRF-Wert für einen der Schlüssel abfragen. creds: [{ credId, prfSalt }] (base64url)
// Liefert { credId, secret: Buffer(32) } // Liefert { credId, secret: Buffer(32) }
async function derive(parent, creds, text) { async function derive(parent, creds, text) {
if (process.platform === 'linux') {
const r = await deriveNative(parent, creds, text);
if (r) return r;
}
const r = await ceremony(parent, text || i18n.t('Touch your security key to unlock MrTerm.'), ` const r = await ceremony(parent, text || i18n.t('Touch your security key to unlock MrTerm.'), `
const creds = ${JSON.stringify(creds)}; const creds = ${JSON.stringify(creds)};
const evalByCredential = Object.fromEntries(creds.map((c) => [c.credId, { first: unb64(c.prfSalt) }])); const evalByCredential = Object.fromEntries(creds.map((c) => [c.credId, { first: unb64(c.prfSalt) }]));
+4 -1
View File
@@ -1338,11 +1338,14 @@ async function importRdm() {
// ============================================================ Updates // ============================================================ Updates
let updateInfo = null; let updateInfo = null;
let updateDialogOpen = false; // Auto-Prüfung beim Start und manuelle Prüfung sollen keine zwei Dialoge stapeln
async function showUpdate(info) { async function showUpdate(info) {
updateInfo = info; updateInfo = info;
const b = $('#updateBadge'); const b = $('#updateBadge');
b.style.display = ''; b.style.display = '';
b.querySelector('span').textContent = `Update ${info.version}`; b.querySelector('span').textContent = `Update ${info.version}`;
if (updateDialogOpen) return;
updateDialogOpen = true;
const r = await modal({ const r = await modal({
title: T('MrTerm {v} is available', { v: info.version }), title: T('MrTerm {v} is available', { v: info.version }),
text: T('Installed: {v}', { v: info.current }) + (info.asset ? ' · ' + T('Package: {name} ({size})', { name: info.asset.name, size: fmtSize(info.asset.size) }) : ''), text: T('Installed: {v}', { v: info.current }) + (info.asset ? ' · ' + T('Package: {name} ({size})', { name: info.asset.name, size: fmtSize(info.asset.size) }) : ''),
@@ -1350,7 +1353,7 @@ async function showUpdate(info) {
${info.kind === 'dev' ? `<p>${T('Development mode: please update via <code>git pull</code>.')}</p>` : !info.asset ? `<p>${T('The release contains no package for this system.')}</p>` : ''} ${info.kind === 'dev' ? `<p>${T('Development mode: please update via <code>git pull</code>.')}</p>` : !info.asset ? `<p>${T('The release contains no package for this system.')}</p>` : ''}
<div class="upd-prog" style="display:none"><div class="transfer" style="padding:6px 0"><span class="nm">${T('Downloading …')}</span><span class="pct"></span><div class="bar"><i></i></div></div></div>`, <div class="upd-prog" style="display:none"><div class="transfer" style="padding:6px 0"><span class="nm">${T('Downloading …')}</span><span class="pct"></span><div class="bar"><i></i></div></div></div>`,
buttons: [{ label: T('Later'), value: false, cls: 'ghost' }, { label: T('Release page'), value: 'web', cls: '' }, ...(info.asset && info.kind !== 'dev' ? [{ label: T('Install now'), value: true, cls: 'primary' }] : [])], buttons: [{ label: T('Later'), value: false, cls: 'ghost' }, { label: T('Release page'), value: 'web', cls: '' }, ...(info.asset && info.kind !== 'dev' ? [{ label: T('Install now'), value: true, cls: 'primary' }] : [])],
}); }).finally(() => { updateDialogOpen = false; });
if (r === 'web') return api.call('shell:open', info.url); if (r === 'web') return api.call('shell:open', info.url);
if (r !== true) return; if (r !== true) return;
toast(T('Downloading update …')); toast(T('Downloading update …'));