diff --git a/public/novnc/app/locale/el.json b/public/novnc/app/locale/el.json index f801251c..4df3e03c 100644 --- a/public/novnc/app/locale/el.json +++ b/public/novnc/app/locale/el.json @@ -1,4 +1,5 @@ { + "HTTPS is required for full functionality": "Το HTTPS είναι απαιτούμενο για πλήρη λειτουργικότητα", "Connecting...": "Συνδέεται...", "Disconnecting...": "Aποσυνδέεται...", "Reconnecting...": "Επανασυνδέεται...", @@ -7,19 +8,15 @@ "Connected (encrypted) to ": "Συνδέθηκε (κρυπτογραφημένα) με το ", "Connected (unencrypted) to ": "Συνδέθηκε (μη κρυπτογραφημένα) με το ", "Something went wrong, connection is closed": "Κάτι πήγε στραβά, η σύνδεση διακόπηκε", + "Failed to connect to server": "Αποτυχία στη σύνδεση με το διακομιστή", "Disconnected": "Αποσυνδέθηκε", "New connection has been rejected with reason: ": "Η νέα σύνδεση απορρίφθηκε διότι: ", "New connection has been rejected": "Η νέα σύνδεση απορρίφθηκε ", - "Password is required": "Απαιτείται ο κωδικός πρόσβασης", + "Credentials are required": "Απαιτούνται διαπιστευτήρια", "noVNC encountered an error:": "το noVNC αντιμετώπισε ένα σφάλμα:", "Hide/Show the control bar": "Απόκρυψη/Εμφάνιση γραμμής ελέγχου", + "Drag": "Σύρσιμο", "Move/Drag Viewport": "Μετακίνηση/Σύρσιμο Θεατού πεδίου", - "viewport drag": "σύρσιμο θεατού πεδίου", - "Active Mouse Button": "Ενεργό Πλήκτρο Ποντικιού", - "No mousebutton": "Χωρίς Πλήκτρο Ποντικιού", - "Left mousebutton": "Αριστερό Πλήκτρο Ποντικιού", - "Middle mousebutton": "Μεσαίο Πλήκτρο Ποντικιού", - "Right mousebutton": "Δεξί Πλήκτρο Ποντικιού", "Keyboard": "Πληκτρολόγιο", "Show Keyboard": "Εμφάνιση Πληκτρολογίου", "Extra keys": "Επιπλέον πλήκτρα", @@ -28,6 +25,8 @@ "Toggle Ctrl": "Εναλλαγή Ctrl", "Alt": "Alt", "Toggle Alt": "Εναλλαγή Alt", + "Toggle Windows": "Εναλλαγή Παράθυρων", + "Windows": "Παράθυρα", "Send Tab": "Αποστολή Tab", "Tab": "Tab", "Esc": "Esc", @@ -41,8 +40,7 @@ "Reboot": "Επανεκκίνηση", "Reset": "Επαναφορά", "Clipboard": "Πρόχειρο", - "Clear": "Καθάρισμα", - "Fullscreen": "Πλήρης Οθόνη", + "Edit clipboard content in the textarea below.": "Επεξεργαστείτε το περιεχόμενο του πρόχειρου στην περιοχή κειμένου παρακάτω.", "Settings": "Ρυθμίσεις", "Shared Mode": "Κοινόχρηστη Λειτουργία", "View Only": "Μόνο Θέαση", @@ -52,6 +50,8 @@ "Local Scaling": "Τοπική Κλιμάκωση", "Remote Resizing": "Απομακρυσμένη Αλλαγή μεγέθους", "Advanced": "Για προχωρημένους", + "Quality:": "Ποιότητα:", + "Compression level:": "Επίπεδο συμπίεσης:", "Repeater ID:": "Repeater ID:", "WebSocket": "WebSocket", "Encrypt": "Κρυπτογράφηση", @@ -60,10 +60,20 @@ "Path:": "Διαδρομή:", "Automatic Reconnect": "Αυτόματη επανασύνδεση", "Reconnect Delay (ms):": "Καθυστέρηση επανασύνδεσης (ms):", + "Show Dot when No Cursor": "Εμφάνιση Τελείας όταν δεν υπάρχει Δρομέας", "Logging:": "Καταγραφή:", + "Version:": "Έκδοση:", "Disconnect": "Αποσύνδεση", "Connect": "Σύνδεση", + "Server identity": "Ταυτότητα Διακομιστή", + "The server has provided the following identifying information:": "Ο διακομιστής παρείχε την ακόλουθη πληροφορία ταυτοποίησης:", + "Fingerprint:": "Δακτυλικό αποτύπωμα:", + "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Παρακαλώ επαληθεύσετε ότι η πληροφορία είναι σωστή και πιέστε \"Αποδοχή\". Αλλιώς πιέστε \"Απόρριψη\".", + "Approve": "Αποδοχή", + "Reject": "Απόρριψη", + "Credentials": "Διαπιστευτήρια", + "Username:": "Κωδικός Χρήστη:", "Password:": "Κωδικός Πρόσβασης:", - "Cancel": "Ακύρωση", - "Canvas not supported.": "Δεν υποστηρίζεται το στοιχείο Canvas" + "Send Credentials": "Αποστολή Διαπιστευτηρίων", + "Cancel": "Ακύρωση" } \ No newline at end of file diff --git a/public/novnc/app/locale/fr.json b/public/novnc/app/locale/fr.json index 22531f73..c0eeec7d 100644 --- a/public/novnc/app/locale/fr.json +++ b/public/novnc/app/locale/fr.json @@ -1,5 +1,4 @@ { - "HTTPS is required for full functionality": "", "Connecting...": "En cours de connexion...", "Disconnecting...": "Déconnexion en cours...", "Reconnecting...": "Reconnexion en cours...", @@ -40,7 +39,8 @@ "Reboot": "Redémarrer", "Reset": "Réinitialiser", "Clipboard": "Presse-papiers", - "Edit clipboard content in the textarea below.": "", + "Clear": "Effacer", + "Fullscreen": "Plein écran", "Settings": "Paramètres", "Shared Mode": "Mode partagé", "View Only": "Afficher uniquement", @@ -65,12 +65,6 @@ "Version:": "Version :", "Disconnect": "Déconnecter", "Connect": "Connecter", - "Server identity": "", - "The server has provided the following identifying information:": "", - "Fingerprint:": "", - "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "", - "Approve": "", - "Reject": "", "Username:": "Nom d'utilisateur :", "Password:": "Mot de passe :", "Send Credentials": "Envoyer les identifiants", diff --git a/public/novnc/app/locale/it.json b/public/novnc/app/locale/it.json index 6fd25702..18a7f744 100644 --- a/public/novnc/app/locale/it.json +++ b/public/novnc/app/locale/it.json @@ -14,8 +14,6 @@ "Credentials are required": "Le credenziali sono obbligatorie", "noVNC encountered an error:": "noVNC ha riscontrato un errore:", "Hide/Show the control bar": "Nascondi/Mostra la barra di controllo", - "Drag": "", - "Move/Drag Viewport": "", "Keyboard": "Tastiera", "Show Keyboard": "Mostra tastiera", "Extra keys": "Tasti Aggiuntivi", @@ -44,7 +42,6 @@ "Settings": "Impostazioni", "Shared Mode": "Modalità condivisa", "View Only": "Sola Visualizzazione", - "Clip to Window": "", "Scaling Mode:": "Modalità di ridimensionamento:", "None": "Nessuna", "Local Scaling": "Ridimensionamento Locale", @@ -61,7 +58,6 @@ "Automatic Reconnect": "Riconnessione Automatica", "Reconnect Delay (ms):": "Ritardo Riconnessione (ms):", "Show Dot when No Cursor": "Mostra Punto quando Nessun Cursore", - "Logging:": "", "Version:": "Versione:", "Disconnect": "Disconnetti", "Connect": "Connetti", diff --git a/public/novnc/app/locale/ja.json b/public/novnc/app/locale/ja.json index 43fc5bf3..70fd7a5d 100644 --- a/public/novnc/app/locale/ja.json +++ b/public/novnc/app/locale/ja.json @@ -1,4 +1,5 @@ { + "HTTPS is required for full functionality": "すべての機能を使用するにはHTTPS接続が必要です", "Connecting...": "接続しています...", "Disconnecting...": "切断しています...", "Reconnecting...": "再接続しています...", @@ -21,10 +22,10 @@ "Extra keys": "追加キー", "Show Extra Keys": "追加キーを表示", "Ctrl": "Ctrl", - "Toggle Ctrl": "Ctrl キーを切り替え", + "Toggle Ctrl": "Ctrl キーをトグル", "Alt": "Alt", - "Toggle Alt": "Alt キーを切り替え", - "Toggle Windows": "Windows キーを切り替え", + "Toggle Alt": "Alt キーをトグル", + "Toggle Windows": "Windows キーをトグル", "Windows": "Windows", "Send Tab": "Tab キーを送信", "Tab": "Tab", @@ -39,11 +40,11 @@ "Reboot": "再起動", "Reset": "リセット", "Clipboard": "クリップボード", - "Clear": "クリア", - "Fullscreen": "全画面表示", + "Edit clipboard content in the textarea below.": "以下の入力欄からクリップボードの内容を編集できます。", + "Full Screen": "全画面表示", "Settings": "設定", "Shared Mode": "共有モード", - "View Only": "表示のみ", + "View Only": "表示専用", "Clip to Window": "ウィンドウにクリップ", "Scaling Mode:": "スケーリングモード:", "None": "なし", @@ -60,11 +61,18 @@ "Path:": "パス:", "Automatic Reconnect": "自動再接続", "Reconnect Delay (ms):": "再接続する遅延 (ミリ秒):", - "Show Dot when No Cursor": "カーソルがないときにドットを表示", + "Show Dot when No Cursor": "カーソルがないときにドットを表示する", "Logging:": "ロギング:", "Version:": "バージョン:", "Disconnect": "切断", "Connect": "接続", + "Server identity": "サーバーの識別情報", + "The server has provided the following identifying information:": "サーバーは以下の識別情報を提供しています:", + "Fingerprint:": "フィンガープリント:", + "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "この情報が正しい場合は「承認」を、そうでない場合は「拒否」を押してください。", + "Approve": "承認", + "Reject": "拒否", + "Credentials": "資格情報", "Username:": "ユーザー名:", "Password:": "パスワード:", "Send Credentials": "資格情報を送信", diff --git a/public/novnc/app/locale/sv.json b/public/novnc/app/locale/sv.json index 077ef42c..80a400bf 100644 --- a/public/novnc/app/locale/sv.json +++ b/public/novnc/app/locale/sv.json @@ -1,10 +1,11 @@ { - "HTTPS is required for full functionality": "HTTPS krävs för full funktionalitet", + "Running without HTTPS is not recommended, crashes or other issues are likely.": "Det är ej rekommenderat att köra utan HTTPS, krascher och andra problem är troliga.", "Connecting...": "Ansluter...", "Disconnecting...": "Kopplar ner...", "Reconnecting...": "Återansluter...", "Internal error": "Internt fel", "Must set host": "Du måste specifiera en värd", + "Failed to connect to server: ": "Misslyckades att ansluta till servern: ", "Connected (encrypted) to ": "Ansluten (krypterat) till ", "Connected (unencrypted) to ": "Ansluten (okrypterat) till ", "Something went wrong, connection is closed": "Något gick fel, anslutningen avslutades", diff --git a/public/novnc/app/locale/zh_CN.json b/public/novnc/app/locale/zh_CN.json index f0aea9af..3679eadd 100644 --- a/public/novnc/app/locale/zh_CN.json +++ b/public/novnc/app/locale/zh_CN.json @@ -1,69 +1,69 @@ { "Connecting...": "连接中...", + "Connected (encrypted) to ": "已连接(已加密)到", + "Connected (unencrypted) to ": "已连接(未加密)到", "Disconnecting...": "正在断开连接...", - "Reconnecting...": "重新连接中...", - "Internal error": "内部错误", - "Must set host": "请提供主机名", - "Connected (encrypted) to ": "已连接到(加密)", - "Connected (unencrypted) to ": "已连接到(未加密)", - "Something went wrong, connection is closed": "发生错误,连接已关闭", - "Failed to connect to server": "无法连接到服务器", "Disconnected": "已断开连接", - "New connection has been rejected with reason: ": "连接被拒绝,原因:", - "New connection has been rejected": "连接被拒绝", + "Must set host": "必须设置主机", + "Reconnecting...": "重新连接中...", "Password is required": "请提供密码", + "Disconnect timeout": "超时断开", "noVNC encountered an error:": "noVNC 遇到一个错误:", "Hide/Show the control bar": "显示/隐藏控制栏", - "Move/Drag Viewport": "拖放显示范围", - "viewport drag": "显示范围拖放", - "Active Mouse Button": "启动鼠标按鍵", - "No mousebutton": "禁用鼠标按鍵", - "Left mousebutton": "鼠标左鍵", - "Middle mousebutton": "鼠标中鍵", - "Right mousebutton": "鼠标右鍵", + "Move/Drag Viewport": "移动/拖动窗口", + "viewport drag": "窗口拖动", + "Active Mouse Button": "启动鼠标按键", + "No mousebutton": "禁用鼠标按键", + "Left mousebutton": "鼠标左键", + "Middle mousebutton": "鼠标中键", + "Right mousebutton": "鼠标右键", "Keyboard": "键盘", "Show Keyboard": "显示键盘", "Extra keys": "额外按键", "Show Extra Keys": "显示额外按键", "Ctrl": "Ctrl", "Toggle Ctrl": "切换 Ctrl", + "Edit clipboard content in the textarea below.": "在下面的文本区域中编辑剪贴板内容。", "Alt": "Alt", "Toggle Alt": "切换 Alt", "Send Tab": "发送 Tab 键", "Tab": "Tab", "Esc": "Esc", "Send Escape": "发送 Escape 键", - "Ctrl+Alt+Del": "Ctrl-Alt-Del", - "Send Ctrl-Alt-Del": "发送 Ctrl-Alt-Del 键", - "Shutdown/Reboot": "关机/重新启动", - "Shutdown/Reboot...": "关机/重新启动...", + "Ctrl+Alt+Del": "Ctrl+Alt+Del", + "Send Ctrl-Alt-Del": "发送 Ctrl+Alt+Del 键", + "Shutdown/Reboot": "关机/重启", + "Shutdown/Reboot...": "关机/重启...", "Power": "电源", "Shutdown": "关机", - "Reboot": "重新启动", + "Reboot": "重启", "Reset": "重置", "Clipboard": "剪贴板", "Clear": "清除", "Fullscreen": "全屏", "Settings": "设置", + "Encrypt": "加密", "Shared Mode": "分享模式", "View Only": "仅查看", "Clip to Window": "限制/裁切窗口大小", "Scaling Mode:": "缩放模式:", "None": "无", "Local Scaling": "本地缩放", + "Local Downscaling": "降低本地尺寸", "Remote Resizing": "远程调整大小", "Advanced": "高级", + "Local Cursor": "本地光标", "Repeater ID:": "中继站 ID", "WebSocket": "WebSocket", - "Encrypt": "加密", "Host:": "主机:", "Port:": "端口:", "Path:": "路径:", "Automatic Reconnect": "自动重新连接", "Reconnect Delay (ms):": "重新连接间隔 (ms):", "Logging:": "日志级别:", - "Disconnect": "中断连接", + "Disconnect": "断开连接", "Connect": "连接", "Password:": "密码:", - "Cancel": "取消" + "Cancel": "取消", + "Canvas not supported.": "不支持 Canvas。" } \ No newline at end of file diff --git a/public/novnc/app/localization.js b/public/novnc/app/localization.js index 84341da6..7d7e6e6a 100644 --- a/public/novnc/app/localization.js +++ b/public/novnc/app/localization.js @@ -16,13 +16,19 @@ export class Localizer { this.language = 'en'; // Current dictionary of translations - this.dictionary = undefined; + this._dictionary = undefined; } // Configure suitable language based on user preferences - setup(supportedLanguages) { + async setup(supportedLanguages, baseURL) { this.language = 'en'; // Default: US English + this._dictionary = undefined; + this._setupLanguage(supportedLanguages); + await this._setupDictionary(baseURL); + } + + _setupLanguage(supportedLanguages) { /* * Navigator.languages only available in Chrome (32+) and FireFox (32+) * Fall back to navigator.language for other browsers @@ -40,12 +46,6 @@ export class Localizer { .replace("_", "-") .split("-"); - // Built-in default? - if ((userLang[0] === 'en') && - ((userLang[1] === undefined) || (userLang[1] === 'us'))) { - return; - } - // First pass: perfect match for (let j = 0; j < supportedLanguages.length; j++) { const supLang = supportedLanguages[j] @@ -64,7 +64,12 @@ export class Localizer { return; } - // Second pass: fallback + // Second pass: English fallback + if (userLang[0] === 'en') { + return; + } + + // Third pass pass: other fallback for (let j = 0;j < supportedLanguages.length;j++) { const supLang = supportedLanguages[j] .toLowerCase() @@ -84,10 +89,32 @@ export class Localizer { } } + async _setupDictionary(baseURL) { + if (baseURL) { + if (!baseURL.endsWith("/")) { + baseURL = baseURL + "/"; + } + } else { + baseURL = ""; + } + + if (this.language === "en") { + return; + } + + let response = await fetch(baseURL + this.language + ".json"); + if (!response.ok) { + throw Error("" + response.status + " " + response.statusText); + } + + this._dictionary = await response.json(); + } + // Retrieve localised text get(id) { - if (typeof this.dictionary !== 'undefined' && this.dictionary[id]) { - return this.dictionary[id]; + if (typeof this._dictionary !== 'undefined' && + this._dictionary[id]) { + return this._dictionary[id]; } else { return id; } diff --git a/public/novnc/app/styles/base.css b/public/novnc/app/styles/base.css index 06e736a9..f83ad4b9 100644 --- a/public/novnc/app/styles/base.css +++ b/public/novnc/app/styles/base.css @@ -661,7 +661,7 @@ html { justify-content: center; align-content: center; - line-height: 25px; + line-height: 1.6; word-wrap: break-word; color: #fff; @@ -887,7 +887,7 @@ html { .noVNC_logo { color:yellow; font-family: 'Orbitron', 'OrbitronTTF', sans-serif; - line-height:90%; + line-height: 0.9; text-shadow: 0.1em 0.1em 0 black; } .noVNC_logo span{ diff --git a/public/novnc/app/styles/input.css b/public/novnc/app/styles/input.css index eaf083c7..dc345aab 100644 --- a/public/novnc/app/styles/input.css +++ b/public/novnc/app/styles/input.css @@ -86,6 +86,9 @@ option { * Checkboxes */ input[type=checkbox] { + display: inline-flex; + justify-content: center; + align-items: center; background-color: white; background-image: unset; border: 1px solid dimgrey; @@ -104,14 +107,11 @@ input[type=checkbox]:checked { input[type=checkbox]:checked::after { content: ""; display: block; /* width & height doesn't work on inline elements */ - position: relative; - top: 0; - left: 3px; width: 3px; height: 7px; border: 1px solid white; border-width: 0 2px 2px 0; - transform: rotate(40deg); + transform: rotate(40deg) translateY(-1px); } /* diff --git a/public/novnc/app/ui.js b/public/novnc/app/ui.js index fc0c4396..906f4c3f 100644 --- a/public/novnc/app/ui.js +++ b/public/novnc/app/ui.js @@ -91,7 +91,7 @@ const UI = { // insecure context if (!window.isSecureContext) { // FIXME: This gets hidden when connecting - UI.showStatus(_("HTTPS is required for full functionality"), 'error'); + UI.showStatus(_("Running without HTTPS is not recommended, crashes or other issues are likely."), 'error'); } // Try to fetch version number @@ -1058,11 +1058,18 @@ const UI = { + try { + UI.rfb = new RFB(document.getElementById('noVNC_container'), urlargs.ws, + { shared: UI.getSetting('shared'), + repeaterID: UI.getSetting('repeaterID'), + credentials: { password: password } }); + } catch (exc) { + Log.Error("Failed to connect to server: " + exc); + UI.updateVisualState('disconnected'); + UI.showStatus(_("Failed to connect to server: ") + exc, 'error'); + return; + } - UI.rfb = new RFB(document.getElementById('noVNC_container'), urlargs.ws, - { shared: UI.getSetting('shared'), - repeaterID: UI.getSetting('repeaterID'), - credentials: { password: password } }); UI.rfb.addEventListener("connect", UI.connectFinished); UI.rfb.addEventListener("disconnect", UI.disconnectFinished); UI.rfb.addEventListener("serververification", UI.serverVerify); @@ -1168,6 +1175,7 @@ const UI = { UI.showStatus(_("Disconnected"), 'normal'); } + document.title = PAGE_TITLE; UI.openControlbar(); UI.openConnectPanel(); @@ -1780,20 +1788,8 @@ const UI = { // Set up translations const LINGUAS = ["cs", "de", "el", "es", "fr", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"]; -l10n.setup(LINGUAS); -if (l10n.language === "en" || l10n.dictionary !== undefined) { - UI.prime(); -} else { - fetch('app/locale/' + l10n.language + '.json') - .then((response) => { - if (!response.ok) { - throw Error("" + response.status + " " + response.statusText); - } - return response.json(); - }) - .then((translations) => { l10n.dictionary = translations; }) - .catch(err => Log.Error("Failed to load translations: " + err)) - .then(UI.prime); -} +l10n.setup(LINGUAS, "app/locale/") + .catch(err => Log.Error("Failed to load translations: " + err)) + .then(UI.prime); export default UI; diff --git a/public/novnc/app/webutil.js b/public/novnc/app/webutil.js index 084c69f6..6011442c 100644 --- a/public/novnc/app/webutil.js +++ b/public/novnc/app/webutil.js @@ -6,16 +6,16 @@ * See README.md for usage and integration instructions. */ -import { initLogging as mainInitLogging } from '../core/util/logging.js'; +import * as Log from '../core/util/logging.js'; // init log level reading the logging HTTP param export function initLogging(level) { "use strict"; if (typeof level !== "undefined") { - mainInitLogging(level); + Log.initLogging(level); } else { const param = document.location.href.match(/logging=([A-Za-z0-9._-]*)/); - mainInitLogging(param || undefined); + Log.initLogging(param || undefined); } } @@ -25,14 +25,14 @@ export function initLogging(level) { // // For privacy (Using a hastag #, the parameters will not be sent to the server) // the url can be requested in the following way: -// https://www.example.com#myqueryparam=myvalue&password=secreatvalue +// https://www.example.com#myqueryparam=myvalue&password=secretvalue // // Even Mixing public and non public parameters will work: -// https://www.example.com?nonsecretparam=example.com#password=secreatvalue +// https://www.example.com?nonsecretparam=example.com#password=secretvalue export function getQueryVar(name, defVal) { "use strict"; const re = new RegExp('.*[?&]' + name + '=([^&#]*)'), - match = ''.concat(document.location.href, window.location.hash).match(re); + match = document.location.href.match(re); if (typeof defVal === 'undefined') { defVal = null; } if (match) { @@ -146,7 +146,7 @@ export function writeSetting(name, value) { if (window.chrome && window.chrome.storage) { window.chrome.storage.sync.set(settings); } else { - localStorage.setItem(name, value); + localStorageSet(name, value); } } @@ -156,7 +156,7 @@ export function readSetting(name, defaultValue) { if ((name in settings) || (window.chrome && window.chrome.storage)) { value = settings[name]; } else { - value = localStorage.getItem(name); + value = localStorageGet(name); settings[name] = value; } if (typeof value === "undefined") { @@ -181,6 +181,70 @@ export function eraseSetting(name) { if (window.chrome && window.chrome.storage) { window.chrome.storage.sync.remove(name); } else { - localStorage.removeItem(name); + localStorageRemove(name); + } +} + +let loggedMsgs = []; +function logOnce(msg, level = "warn") { + if (!loggedMsgs.includes(msg)) { + switch (level) { + case "error": + Log.Error(msg); + break; + case "warn": + Log.Warn(msg); + break; + case "debug": + Log.Debug(msg); + break; + default: + Log.Info(msg); + } + loggedMsgs.push(msg); + } +} + +let cookiesMsg = "Couldn't access noVNC settings, are cookies disabled?"; + +function localStorageGet(name) { + let r; + try { + r = localStorage.getItem(name); + } catch (e) { + if (e instanceof DOMException) { + logOnce(cookiesMsg); + logOnce("'localStorage.getItem(" + name + ")' failed: " + e, + "debug"); + } else { + throw e; + } + } + return r; +} +function localStorageSet(name, value) { + try { + localStorage.setItem(name, value); + } catch (e) { + if (e instanceof DOMException) { + logOnce(cookiesMsg); + logOnce("'localStorage.setItem(" + name + "," + value + + ")' failed: " + e, "debug"); + } else { + throw e; + } + } +} +function localStorageRemove(name) { + try { + localStorage.removeItem(name); + } catch (e) { + if (e instanceof DOMException) { + logOnce(cookiesMsg); + logOnce("'localStorage.removeItem(" + name + ")' failed: " + e, + "debug"); + } else { + throw e; + } } } diff --git a/public/novnc/core/crypto/aes.js b/public/novnc/core/crypto/aes.js new file mode 100644 index 00000000..e6aaea7c --- /dev/null +++ b/public/novnc/core/crypto/aes.js @@ -0,0 +1,178 @@ +export class AESECBCipher { + constructor() { + this._key = null; + } + + get algorithm() { + return { name: "AES-ECB" }; + } + + static async importKey(key, _algorithm, extractable, keyUsages) { + const cipher = new AESECBCipher; + await cipher._importKey(key, extractable, keyUsages); + return cipher; + } + + async _importKey(key, extractable, keyUsages) { + this._key = await window.crypto.subtle.importKey( + "raw", key, {name: "AES-CBC"}, extractable, keyUsages); + } + + async encrypt(_algorithm, plaintext) { + const x = new Uint8Array(plaintext); + if (x.length % 16 !== 0 || this._key === null) { + return null; + } + const n = x.length / 16; + for (let i = 0; i < n; i++) { + const y = new Uint8Array(await window.crypto.subtle.encrypt({ + name: "AES-CBC", + iv: new Uint8Array(16), + }, this._key, x.slice(i * 16, i * 16 + 16))).slice(0, 16); + x.set(y, i * 16); + } + return x; + } +} + +export class AESEAXCipher { + constructor() { + this._rawKey = null; + this._ctrKey = null; + this._cbcKey = null; + this._zeroBlock = new Uint8Array(16); + this._prefixBlock0 = this._zeroBlock; + this._prefixBlock1 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + this._prefixBlock2 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + } + + get algorithm() { + return { name: "AES-EAX" }; + } + + async _encryptBlock(block) { + const encrypted = await window.crypto.subtle.encrypt({ + name: "AES-CBC", + iv: this._zeroBlock, + }, this._cbcKey, block); + return new Uint8Array(encrypted).slice(0, 16); + } + + async _initCMAC() { + const k1 = await this._encryptBlock(this._zeroBlock); + const k2 = new Uint8Array(16); + const v = k1[0] >>> 6; + for (let i = 0; i < 15; i++) { + k2[i] = (k1[i + 1] >> 6) | (k1[i] << 2); + k1[i] = (k1[i + 1] >> 7) | (k1[i] << 1); + } + const lut = [0x0, 0x87, 0x0e, 0x89]; + k2[14] ^= v >>> 1; + k2[15] = (k1[15] << 2) ^ lut[v]; + k1[15] = (k1[15] << 1) ^ lut[v >> 1]; + this._k1 = k1; + this._k2 = k2; + } + + async _encryptCTR(data, counter) { + const encrypted = await window.crypto.subtle.encrypt({ + name: "AES-CTR", + counter: counter, + length: 128 + }, this._ctrKey, data); + return new Uint8Array(encrypted); + } + + async _decryptCTR(data, counter) { + const decrypted = await window.crypto.subtle.decrypt({ + name: "AES-CTR", + counter: counter, + length: 128 + }, this._ctrKey, data); + return new Uint8Array(decrypted); + } + + async _computeCMAC(data, prefixBlock) { + if (prefixBlock.length !== 16) { + return null; + } + const n = Math.floor(data.length / 16); + const m = Math.ceil(data.length / 16); + const r = data.length - n * 16; + const cbcData = new Uint8Array((m + 1) * 16); + cbcData.set(prefixBlock); + cbcData.set(data, 16); + if (r === 0) { + for (let i = 0; i < 16; i++) { + cbcData[n * 16 + i] ^= this._k1[i]; + } + } else { + cbcData[(n + 1) * 16 + r] = 0x80; + for (let i = 0; i < 16; i++) { + cbcData[(n + 1) * 16 + i] ^= this._k2[i]; + } + } + let cbcEncrypted = await window.crypto.subtle.encrypt({ + name: "AES-CBC", + iv: this._zeroBlock, + }, this._cbcKey, cbcData); + + cbcEncrypted = new Uint8Array(cbcEncrypted); + const mac = cbcEncrypted.slice(cbcEncrypted.length - 32, cbcEncrypted.length - 16); + return mac; + } + + static async importKey(key, _algorithm, _extractable, _keyUsages) { + const cipher = new AESEAXCipher; + await cipher._importKey(key); + return cipher; + } + + async _importKey(key) { + this._rawKey = key; + this._ctrKey = await window.crypto.subtle.importKey( + "raw", key, {name: "AES-CTR"}, false, ["encrypt", "decrypt"]); + this._cbcKey = await window.crypto.subtle.importKey( + "raw", key, {name: "AES-CBC"}, false, ["encrypt"]); + await this._initCMAC(); + } + + async encrypt(algorithm, message) { + const ad = algorithm.additionalData; + const nonce = algorithm.iv; + const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0); + const encrypted = await this._encryptCTR(message, nCMAC); + const adCMAC = await this._computeCMAC(ad, this._prefixBlock1); + const mac = await this._computeCMAC(encrypted, this._prefixBlock2); + for (let i = 0; i < 16; i++) { + mac[i] ^= nCMAC[i] ^ adCMAC[i]; + } + const res = new Uint8Array(16 + encrypted.length); + res.set(encrypted); + res.set(mac, encrypted.length); + return res; + } + + async decrypt(algorithm, data) { + const encrypted = data.slice(0, data.length - 16); + const ad = algorithm.additionalData; + const nonce = algorithm.iv; + const mac = data.slice(data.length - 16); + const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0); + const adCMAC = await this._computeCMAC(ad, this._prefixBlock1); + const computedMac = await this._computeCMAC(encrypted, this._prefixBlock2); + for (let i = 0; i < 16; i++) { + computedMac[i] ^= nCMAC[i] ^ adCMAC[i]; + } + if (computedMac.length !== mac.length) { + return null; + } + for (let i = 0; i < mac.length; i++) { + if (computedMac[i] !== mac[i]) { + return null; + } + } + const res = await this._decryptCTR(encrypted, nCMAC); + return res; + } +} diff --git a/public/novnc/core/crypto/bigint.js b/public/novnc/core/crypto/bigint.js new file mode 100644 index 00000000..d3443265 --- /dev/null +++ b/public/novnc/core/crypto/bigint.js @@ -0,0 +1,34 @@ +export function modPow(b, e, m) { + let r = 1n; + b = b % m; + while (e > 0n) { + if ((e & 1n) === 1n) { + r = (r * b) % m; + } + e = e >> 1n; + b = (b * b) % m; + } + return r; +} + +export function bigIntToU8Array(bigint, padLength=0) { + let hex = bigint.toString(16); + if (padLength === 0) { + padLength = Math.ceil(hex.length / 2); + } + hex = hex.padStart(padLength * 2, '0'); + const length = hex.length / 2; + const arr = new Uint8Array(length); + for (let i = 0; i < length; i++) { + arr[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); + } + return arr; +} + +export function u8ArrayToBigInt(arr) { + let hex = '0x'; + for (let i = 0; i < arr.length; i++) { + hex += arr[i].toString(16).padStart(2, '0'); + } + return BigInt(hex); +} diff --git a/public/novnc/core/crypto/crypto.js b/public/novnc/core/crypto/crypto.js new file mode 100644 index 00000000..cc17da22 --- /dev/null +++ b/public/novnc/core/crypto/crypto.js @@ -0,0 +1,90 @@ +import { AESECBCipher, AESEAXCipher } from "./aes.js"; +import { DESCBCCipher, DESECBCipher } from "./des.js"; +import { RSACipher } from "./rsa.js"; +import { DHCipher } from "./dh.js"; +import { MD5 } from "./md5.js"; + +// A single interface for the cryptographic algorithms not supported by SubtleCrypto. +// Both synchronous and asynchronous implmentations are allowed. +class LegacyCrypto { + constructor() { + this._algorithms = { + "AES-ECB": AESECBCipher, + "AES-EAX": AESEAXCipher, + "DES-ECB": DESECBCipher, + "DES-CBC": DESCBCCipher, + "RSA-PKCS1-v1_5": RSACipher, + "DH": DHCipher, + "MD5": MD5, + }; + } + + encrypt(algorithm, key, data) { + if (key.algorithm.name !== algorithm.name) { + throw new Error("algorithm does not match"); + } + if (typeof key.encrypt !== "function") { + throw new Error("key does not support encryption"); + } + return key.encrypt(algorithm, data); + } + + decrypt(algorithm, key, data) { + if (key.algorithm.name !== algorithm.name) { + throw new Error("algorithm does not match"); + } + if (typeof key.decrypt !== "function") { + throw new Error("key does not support encryption"); + } + return key.decrypt(algorithm, data); + } + + importKey(format, keyData, algorithm, extractable, keyUsages) { + if (format !== "raw") { + throw new Error("key format is not supported"); + } + const alg = this._algorithms[algorithm.name]; + if (typeof alg === "undefined" || typeof alg.importKey !== "function") { + throw new Error("algorithm is not supported"); + } + return alg.importKey(keyData, algorithm, extractable, keyUsages); + } + + generateKey(algorithm, extractable, keyUsages) { + const alg = this._algorithms[algorithm.name]; + if (typeof alg === "undefined" || typeof alg.generateKey !== "function") { + throw new Error("algorithm is not supported"); + } + return alg.generateKey(algorithm, extractable, keyUsages); + } + + exportKey(format, key) { + if (format !== "raw") { + throw new Error("key format is not supported"); + } + if (typeof key.exportKey !== "function") { + throw new Error("key does not support exportKey"); + } + return key.exportKey(); + } + + digest(algorithm, data) { + const alg = this._algorithms[algorithm]; + if (typeof alg !== "function") { + throw new Error("algorithm is not supported"); + } + return alg(data); + } + + deriveBits(algorithm, key, length) { + if (key.algorithm.name !== algorithm.name) { + throw new Error("algorithm does not match"); + } + if (typeof key.deriveBits !== "function") { + throw new Error("key does not support deriveBits"); + } + return key.deriveBits(algorithm, length); + } +} + +export default new LegacyCrypto; diff --git a/public/novnc/core/crypto/des.js b/public/novnc/core/crypto/des.js new file mode 100644 index 00000000..8dab31fb --- /dev/null +++ b/public/novnc/core/crypto/des.js @@ -0,0 +1,330 @@ +/* + * Ported from Flashlight VNC ActionScript implementation: + * http://www.wizhelp.com/flashlight-vnc/ + * + * Full attribution follows: + * + * ------------------------------------------------------------------------- + * + * This DES class has been extracted from package Acme.Crypto for use in VNC. + * The unnecessary odd parity code has been removed. + * + * These changes are: + * Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + + * DesCipher - the DES encryption method + * + * The meat of this code is by Dave Zimmerman , and is: + * + * Copyright (c) 1996 Widget Workshop, Inc. All Rights Reserved. + * + * Permission to use, copy, modify, and distribute this software + * and its documentation for NON-COMMERCIAL or COMMERCIAL purposes and + * without fee is hereby granted, provided that this copyright notice is kept + * intact. + * + * WIDGET WORKSHOP MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY + * OF THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + * TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE, OR NON-INFRINGEMENT. WIDGET WORKSHOP SHALL NOT BE LIABLE + * FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR + * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. + * + * THIS SOFTWARE IS NOT DESIGNED OR INTENDED FOR USE OR RESALE AS ON-LINE + * CONTROL EQUIPMENT IN HAZARDOUS ENVIRONMENTS REQUIRING FAIL-SAFE + * PERFORMANCE, SUCH AS IN THE OPERATION OF NUCLEAR FACILITIES, AIRCRAFT + * NAVIGATION OR COMMUNICATION SYSTEMS, AIR TRAFFIC CONTROL, DIRECT LIFE + * SUPPORT MACHINES, OR WEAPONS SYSTEMS, IN WHICH THE FAILURE OF THE + * SOFTWARE COULD LEAD DIRECTLY TO DEATH, PERSONAL INJURY, OR SEVERE + * PHYSICAL OR ENVIRONMENTAL DAMAGE ("HIGH RISK ACTIVITIES"). WIDGET WORKSHOP + * SPECIFICALLY DISCLAIMS ANY EXPRESS OR IMPLIED WARRANTY OF FITNESS FOR + * HIGH RISK ACTIVITIES. + * + * + * The rest is: + * + * Copyright (C) 1996 by Jef Poskanzer . All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * Visit the ACME Labs Java page for up-to-date versions of this and other + * fine Java utilities: http://www.acme.com/java/ + */ + +/* eslint-disable comma-spacing */ + +// Tables, permutations, S-boxes, etc. +const PC2 = [13,16,10,23, 0, 4, 2,27,14, 5,20, 9,22,18,11, 3, + 25, 7,15, 6,26,19,12, 1,40,51,30,36,46,54,29,39, + 50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31 ], + totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28]; + +const z = 0x0; +let a,b,c,d,e,f; +a=1<<16; b=1<<24; c=a|b; d=1<<2; e=1<<10; f=d|e; +const SP1 = [c|e,z|z,a|z,c|f,c|d,a|f,z|d,a|z,z|e,c|e,c|f,z|e,b|f,c|d,b|z,z|d, + z|f,b|e,b|e,a|e,a|e,c|z,c|z,b|f,a|d,b|d,b|d,a|d,z|z,z|f,a|f,b|z, + a|z,c|f,z|d,c|z,c|e,b|z,b|z,z|e,c|d,a|z,a|e,b|d,z|e,z|d,b|f,a|f, + c|f,a|d,c|z,b|f,b|d,z|f,a|f,c|e,z|f,b|e,b|e,z|z,a|d,a|e,z|z,c|d]; +a=1<<20; b=1<<31; c=a|b; d=1<<5; e=1<<15; f=d|e; +const SP2 = [c|f,b|e,z|e,a|f,a|z,z|d,c|d,b|f,b|d,c|f,c|e,b|z,b|e,a|z,z|d,c|d, + a|e,a|d,b|f,z|z,b|z,z|e,a|f,c|z,a|d,b|d,z|z,a|e,z|f,c|e,c|z,z|f, + z|z,a|f,c|d,a|z,b|f,c|z,c|e,z|e,c|z,b|e,z|d,c|f,a|f,z|d,z|e,b|z, + z|f,c|e,a|z,b|d,a|d,b|f,b|d,a|d,a|e,z|z,b|e,z|f,b|z,c|d,c|f,a|e]; +a=1<<17; b=1<<27; c=a|b; d=1<<3; e=1<<9; f=d|e; +const SP3 = [z|f,c|e,z|z,c|d,b|e,z|z,a|f,b|e,a|d,b|d,b|d,a|z,c|f,a|d,c|z,z|f, + b|z,z|d,c|e,z|e,a|e,c|z,c|d,a|f,b|f,a|e,a|z,b|f,z|d,c|f,z|e,b|z, + c|e,b|z,a|d,z|f,a|z,c|e,b|e,z|z,z|e,a|d,c|f,b|e,b|d,z|e,z|z,c|d, + b|f,a|z,b|z,c|f,z|d,a|f,a|e,b|d,c|z,b|f,z|f,c|z,a|f,z|d,c|d,a|e]; +a=1<<13; b=1<<23; c=a|b; d=1<<0; e=1<<7; f=d|e; +const SP4 = [c|d,a|f,a|f,z|e,c|e,b|f,b|d,a|d,z|z,c|z,c|z,c|f,z|f,z|z,b|e,b|d, + z|d,a|z,b|z,c|d,z|e,b|z,a|d,a|e,b|f,z|d,a|e,b|e,a|z,c|e,c|f,z|f, + b|e,b|d,c|z,c|f,z|f,z|z,z|z,c|z,a|e,b|e,b|f,z|d,c|d,a|f,a|f,z|e, + c|f,z|f,z|d,a|z,b|d,a|d,c|e,b|f,a|d,a|e,b|z,c|d,z|e,b|z,a|z,c|e]; +a=1<<25; b=1<<30; c=a|b; d=1<<8; e=1<<19; f=d|e; +const SP5 = [z|d,a|f,a|e,c|d,z|e,z|d,b|z,a|e,b|f,z|e,a|d,b|f,c|d,c|e,z|f,b|z, + a|z,b|e,b|e,z|z,b|d,c|f,c|f,a|d,c|e,b|d,z|z,c|z,a|f,a|z,c|z,z|f, + z|e,c|d,z|d,a|z,b|z,a|e,c|d,b|f,a|d,b|z,c|e,a|f,b|f,z|d,a|z,c|e, + c|f,z|f,c|z,c|f,a|e,z|z,b|e,c|z,z|f,a|d,b|d,z|e,z|z,b|e,a|f,b|d]; +a=1<<22; b=1<<29; c=a|b; d=1<<4; e=1<<14; f=d|e; +const SP6 = [b|d,c|z,z|e,c|f,c|z,z|d,c|f,a|z,b|e,a|f,a|z,b|d,a|d,b|e,b|z,z|f, + z|z,a|d,b|f,z|e,a|e,b|f,z|d,c|d,c|d,z|z,a|f,c|e,z|f,a|e,c|e,b|z, + b|e,z|d,c|d,a|e,c|f,a|z,z|f,b|d,a|z,b|e,b|z,z|f,b|d,c|f,a|e,c|z, + a|f,c|e,z|z,c|d,z|d,z|e,c|z,a|f,z|e,a|d,b|f,z|z,c|e,b|z,a|d,b|f]; +a=1<<21; b=1<<26; c=a|b; d=1<<1; e=1<<11; f=d|e; +const SP7 = [a|z,c|d,b|f,z|z,z|e,b|f,a|f,c|e,c|f,a|z,z|z,b|d,z|d,b|z,c|d,z|f, + b|e,a|f,a|d,b|e,b|d,c|z,c|e,a|d,c|z,z|e,z|f,c|f,a|e,z|d,b|z,a|e, + b|z,a|e,a|z,b|f,b|f,c|d,c|d,z|d,a|d,b|z,b|e,a|z,c|e,z|f,a|f,c|e, + z|f,b|d,c|f,c|z,a|e,z|z,z|d,c|f,z|z,a|f,c|z,z|e,b|d,b|e,z|e,a|d]; +a=1<<18; b=1<<28; c=a|b; d=1<<6; e=1<<12; f=d|e; +const SP8 = [b|f,z|e,a|z,c|f,b|z,b|f,z|d,b|z,a|d,c|z,c|f,a|e,c|e,a|f,z|e,z|d, + c|z,b|d,b|e,z|f,a|e,a|d,c|d,c|e,z|f,z|z,z|z,c|d,b|d,b|e,a|f,a|z, + a|f,a|z,c|e,z|e,z|d,c|d,z|e,a|f,b|e,z|d,b|d,c|z,c|d,b|z,a|z,b|f, + z|z,c|f,a|d,b|d,c|z,b|e,b|f,z|z,c|f,a|e,a|e,z|f,z|f,a|d,b|z,c|e]; + +/* eslint-enable comma-spacing */ + +class DES { + constructor(password) { + this.keys = []; + + // Set the key. + const pc1m = [], pcr = [], kn = []; + + for (let j = 0, l = 56; j < 56; ++j, l -= 8) { + l += l < -5 ? 65 : l < -3 ? 31 : l < -1 ? 63 : l === 27 ? 35 : 0; // PC1 + const m = l & 0x7; + pc1m[j] = ((password[l >>> 3] & (1<>> 10; + this.keys[KnLi] |= (raw1 & 0x00000fc0) >>> 6; + ++KnLi; + this.keys[KnLi] = (raw0 & 0x0003f000) << 12; + this.keys[KnLi] |= (raw0 & 0x0000003f) << 16; + this.keys[KnLi] |= (raw1 & 0x0003f000) >>> 4; + this.keys[KnLi] |= (raw1 & 0x0000003f); + ++KnLi; + } + } + + // Encrypt 8 bytes of text + enc8(text) { + const b = text.slice(); + let i = 0, l, r, x; // left, right, accumulator + + // Squash 8 bytes to 2 ints + l = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++]; + r = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++]; + + x = ((l >>> 4) ^ r) & 0x0f0f0f0f; + r ^= x; + l ^= (x << 4); + x = ((l >>> 16) ^ r) & 0x0000ffff; + r ^= x; + l ^= (x << 16); + x = ((r >>> 2) ^ l) & 0x33333333; + l ^= x; + r ^= (x << 2); + x = ((r >>> 8) ^ l) & 0x00ff00ff; + l ^= x; + r ^= (x << 8); + r = (r << 1) | ((r >>> 31) & 1); + x = (l ^ r) & 0xaaaaaaaa; + l ^= x; + r ^= x; + l = (l << 1) | ((l >>> 31) & 1); + + for (let i = 0, keysi = 0; i < 8; ++i) { + x = (r << 28) | (r >>> 4); + x ^= this.keys[keysi++]; + let fval = SP7[x & 0x3f]; + fval |= SP5[(x >>> 8) & 0x3f]; + fval |= SP3[(x >>> 16) & 0x3f]; + fval |= SP1[(x >>> 24) & 0x3f]; + x = r ^ this.keys[keysi++]; + fval |= SP8[x & 0x3f]; + fval |= SP6[(x >>> 8) & 0x3f]; + fval |= SP4[(x >>> 16) & 0x3f]; + fval |= SP2[(x >>> 24) & 0x3f]; + l ^= fval; + x = (l << 28) | (l >>> 4); + x ^= this.keys[keysi++]; + fval = SP7[x & 0x3f]; + fval |= SP5[(x >>> 8) & 0x3f]; + fval |= SP3[(x >>> 16) & 0x3f]; + fval |= SP1[(x >>> 24) & 0x3f]; + x = l ^ this.keys[keysi++]; + fval |= SP8[x & 0x0000003f]; + fval |= SP6[(x >>> 8) & 0x3f]; + fval |= SP4[(x >>> 16) & 0x3f]; + fval |= SP2[(x >>> 24) & 0x3f]; + r ^= fval; + } + + r = (r << 31) | (r >>> 1); + x = (l ^ r) & 0xaaaaaaaa; + l ^= x; + r ^= x; + l = (l << 31) | (l >>> 1); + x = ((l >>> 8) ^ r) & 0x00ff00ff; + r ^= x; + l ^= (x << 8); + x = ((l >>> 2) ^ r) & 0x33333333; + r ^= x; + l ^= (x << 2); + x = ((r >>> 16) ^ l) & 0x0000ffff; + l ^= x; + r ^= (x << 16); + x = ((r >>> 4) ^ l) & 0x0f0f0f0f; + l ^= x; + r ^= (x << 4); + + // Spread ints to bytes + x = [r, l]; + for (i = 0; i < 8; i++) { + b[i] = (x[i>>>2] >>> (8 * (3 - (i % 4)))) % 256; + if (b[i] < 0) { b[i] += 256; } // unsigned + } + return b; + } +} + +export class DESECBCipher { + constructor() { + this._cipher = null; + } + + get algorithm() { + return { name: "DES-ECB" }; + } + + static importKey(key, _algorithm, _extractable, _keyUsages) { + const cipher = new DESECBCipher; + cipher._importKey(key); + return cipher; + } + + _importKey(key, _extractable, _keyUsages) { + this._cipher = new DES(key); + } + + encrypt(_algorithm, plaintext) { + const x = new Uint8Array(plaintext); + if (x.length % 8 !== 0 || this._cipher === null) { + return null; + } + const n = x.length / 8; + for (let i = 0; i < n; i++) { + x.set(this._cipher.enc8(x.slice(i * 8, i * 8 + 8)), i * 8); + } + return x; + } +} + +export class DESCBCCipher { + constructor() { + this._cipher = null; + } + + get algorithm() { + return { name: "DES-CBC" }; + } + + static importKey(key, _algorithm, _extractable, _keyUsages) { + const cipher = new DESCBCCipher; + cipher._importKey(key); + return cipher; + } + + _importKey(key) { + this._cipher = new DES(key); + } + + encrypt(algorithm, plaintext) { + const x = new Uint8Array(plaintext); + let y = new Uint8Array(algorithm.iv); + if (x.length % 8 !== 0 || this._cipher === null) { + return null; + } + const n = x.length / 8; + for (let i = 0; i < n; i++) { + for (let j = 0; j < 8; j++) { + y[j] ^= plaintext[i * 8 + j]; + } + y = this._cipher.enc8(y); + x.set(y, i * 8); + } + return x; + } +} diff --git a/public/novnc/core/crypto/dh.js b/public/novnc/core/crypto/dh.js new file mode 100644 index 00000000..bd705d9b --- /dev/null +++ b/public/novnc/core/crypto/dh.js @@ -0,0 +1,55 @@ +import { modPow, bigIntToU8Array, u8ArrayToBigInt } from "./bigint.js"; + +class DHPublicKey { + constructor(key) { + this._key = key; + } + + get algorithm() { + return { name: "DH" }; + } + + exportKey() { + return this._key; + } +} + +export class DHCipher { + constructor() { + this._g = null; + this._p = null; + this._gBigInt = null; + this._pBigInt = null; + this._privateKey = null; + } + + get algorithm() { + return { name: "DH" }; + } + + static generateKey(algorithm, _extractable) { + const cipher = new DHCipher; + cipher._generateKey(algorithm); + return { privateKey: cipher, publicKey: new DHPublicKey(cipher._publicKey) }; + } + + _generateKey(algorithm) { + const g = algorithm.g; + const p = algorithm.p; + this._keyBytes = p.length; + this._gBigInt = u8ArrayToBigInt(g); + this._pBigInt = u8ArrayToBigInt(p); + this._privateKey = window.crypto.getRandomValues(new Uint8Array(this._keyBytes)); + this._privateKeyBigInt = u8ArrayToBigInt(this._privateKey); + this._publicKey = bigIntToU8Array(modPow( + this._gBigInt, this._privateKeyBigInt, this._pBigInt), this._keyBytes); + } + + deriveBits(algorithm, length) { + const bytes = Math.ceil(length / 8); + const pkey = new Uint8Array(algorithm.public); + const len = bytes > this._keyBytes ? bytes : this._keyBytes; + const secret = modPow(u8ArrayToBigInt(pkey), this._privateKeyBigInt, this._pBigInt); + return bigIntToU8Array(secret, len).slice(0, len); + } +} diff --git a/public/novnc/core/crypto/md5.js b/public/novnc/core/crypto/md5.js new file mode 100644 index 00000000..fcfefff0 --- /dev/null +++ b/public/novnc/core/crypto/md5.js @@ -0,0 +1,82 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2021 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +/* + * Performs MD5 hashing on an array of bytes, returns an array of bytes + */ + +export async function MD5(d) { + let s = ""; + for (let i = 0; i < d.length; i++) { + s += String.fromCharCode(d[i]); + } + return M(V(Y(X(s), 8 * s.length))); +} + +function M(d) { + let f = new Uint8Array(d.length); + for (let i=0;i> 2); + for (let m = 0; m < r.length; m++) r[m] = 0; + for (let m = 0; m < 8 * d.length; m += 8) r[m >> 5] |= (255 & d.charCodeAt(m / 8)) << m % 32; + return r; +} + +function V(d) { + let r = ""; + for (let m = 0; m < 32 * d.length; m += 8) r += String.fromCharCode(d[m >> 5] >>> m % 32 & 255); + return r; +} + +function Y(d, g) { + d[g >> 5] |= 128 << g % 32, d[14 + (g + 64 >>> 9 << 4)] = g; + let m = 1732584193, f = -271733879, r = -1732584194, i = 271733878; + for (let n = 0; n < d.length; n += 16) { + let h = m, + t = f, + g = r, + e = i; + f = ii(f = ii(f = ii(f = ii(f = hh(f = hh(f = hh(f = hh(f = gg(f = gg(f = gg(f = gg(f = ff(f = ff(f = ff(f = ff(f, r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 0], 7, -680876936), f, r, d[n + 1], 12, -389564586), m, f, d[n + 2], 17, 606105819), i, m, d[n + 3], 22, -1044525330), r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 4], 7, -176418897), f, r, d[n + 5], 12, 1200080426), m, f, d[n + 6], 17, -1473231341), i, m, d[n + 7], 22, -45705983), r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 8], 7, 1770035416), f, r, d[n + 9], 12, -1958414417), m, f, d[n + 10], 17, -42063), i, m, d[n + 11], 22, -1990404162), r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 12], 7, 1804603682), f, r, d[n + 13], 12, -40341101), m, f, d[n + 14], 17, -1502002290), i, m, d[n + 15], 22, 1236535329), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 1], 5, -165796510), f, r, d[n + 6], 9, -1069501632), m, f, d[n + 11], 14, 643717713), i, m, d[n + 0], 20, -373897302), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 5], 5, -701558691), f, r, d[n + 10], 9, 38016083), m, f, d[n + 15], 14, -660478335), i, m, d[n + 4], 20, -405537848), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 9], 5, 568446438), f, r, d[n + 14], 9, -1019803690), m, f, d[n + 3], 14, -187363961), i, m, d[n + 8], 20, 1163531501), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 13], 5, -1444681467), f, r, d[n + 2], 9, -51403784), m, f, d[n + 7], 14, 1735328473), i, m, d[n + 12], 20, -1926607734), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 5], 4, -378558), f, r, d[n + 8], 11, -2022574463), m, f, d[n + 11], 16, 1839030562), i, m, d[n + 14], 23, -35309556), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 1], 4, -1530992060), f, r, d[n + 4], 11, 1272893353), m, f, d[n + 7], 16, -155497632), i, m, d[n + 10], 23, -1094730640), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 13], 4, 681279174), f, r, d[n + 0], 11, -358537222), m, f, d[n + 3], 16, -722521979), i, m, d[n + 6], 23, 76029189), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 9], 4, -640364487), f, r, d[n + 12], 11, -421815835), m, f, d[n + 15], 16, 530742520), i, m, d[n + 2], 23, -995338651), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 0], 6, -198630844), f, r, d[n + 7], 10, 1126891415), m, f, d[n + 14], 15, -1416354905), i, m, d[n + 5], 21, -57434055), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 12], 6, 1700485571), f, r, d[n + 3], 10, -1894986606), m, f, d[n + 10], 15, -1051523), i, m, d[n + 1], 21, -2054922799), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 8], 6, 1873313359), f, r, d[n + 15], 10, -30611744), m, f, d[n + 6], 15, -1560198380), i, m, d[n + 13], 21, 1309151649), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 4], 6, -145523070), f, r, d[n + 11], 10, -1120210379), m, f, d[n + 2], 15, 718787259), i, m, d[n + 9], 21, -343485551), m = add(m, h), f = add(f, t), r = add(r, g), i = add(i, e); + } + return Array(m, f, r, i); +} + +function cmn(d, g, m, f, r, i) { + return add(rol(add(add(g, d), add(f, i)), r), m); +} + +function ff(d, g, m, f, r, i, n) { + return cmn(g & m | ~g & f, d, g, r, i, n); +} + +function gg(d, g, m, f, r, i, n) { + return cmn(g & f | m & ~f, d, g, r, i, n); +} + +function hh(d, g, m, f, r, i, n) { + return cmn(g ^ m ^ f, d, g, r, i, n); +} + +function ii(d, g, m, f, r, i, n) { + return cmn(m ^ (g | ~f), d, g, r, i, n); +} + +function add(d, g) { + let m = (65535 & d) + (65535 & g); + return (d >> 16) + (g >> 16) + (m >> 16) << 16 | 65535 & m; +} + +function rol(d, g) { + return d << g | d >>> 32 - g; +} diff --git a/public/novnc/core/crypto/rsa.js b/public/novnc/core/crypto/rsa.js new file mode 100644 index 00000000..68e8e869 --- /dev/null +++ b/public/novnc/core/crypto/rsa.js @@ -0,0 +1,132 @@ +import Base64 from "../base64.js"; +import { modPow, bigIntToU8Array, u8ArrayToBigInt } from "./bigint.js"; + +export class RSACipher { + constructor() { + this._keyLength = 0; + this._keyBytes = 0; + this._n = null; + this._e = null; + this._d = null; + this._nBigInt = null; + this._eBigInt = null; + this._dBigInt = null; + this._extractable = false; + } + + get algorithm() { + return { name: "RSA-PKCS1-v1_5" }; + } + + _base64urlDecode(data) { + data = data.replace(/-/g, "+").replace(/_/g, "/"); + data = data.padEnd(Math.ceil(data.length / 4) * 4, "="); + return Base64.decode(data); + } + + _padArray(arr, length) { + const res = new Uint8Array(length); + res.set(arr, length - arr.length); + return res; + } + + static async generateKey(algorithm, extractable, _keyUsages) { + const cipher = new RSACipher; + await cipher._generateKey(algorithm, extractable); + return { privateKey: cipher }; + } + + async _generateKey(algorithm, extractable) { + this._keyLength = algorithm.modulusLength; + this._keyBytes = Math.ceil(this._keyLength / 8); + const key = await window.crypto.subtle.generateKey( + { + name: "RSA-OAEP", + modulusLength: algorithm.modulusLength, + publicExponent: algorithm.publicExponent, + hash: {name: "SHA-256"}, + }, + true, ["encrypt", "decrypt"]); + const privateKey = await window.crypto.subtle.exportKey("jwk", key.privateKey); + this._n = this._padArray(this._base64urlDecode(privateKey.n), this._keyBytes); + this._nBigInt = u8ArrayToBigInt(this._n); + this._e = this._padArray(this._base64urlDecode(privateKey.e), this._keyBytes); + this._eBigInt = u8ArrayToBigInt(this._e); + this._d = this._padArray(this._base64urlDecode(privateKey.d), this._keyBytes); + this._dBigInt = u8ArrayToBigInt(this._d); + this._extractable = extractable; + } + + static async importKey(key, _algorithm, extractable, keyUsages) { + if (keyUsages.length !== 1 || keyUsages[0] !== "encrypt") { + throw new Error("only support importing RSA public key"); + } + const cipher = new RSACipher; + await cipher._importKey(key, extractable); + return cipher; + } + + async _importKey(key, extractable) { + const n = key.n; + const e = key.e; + if (n.length !== e.length) { + throw new Error("the sizes of modulus and public exponent do not match"); + } + this._keyBytes = n.length; + this._keyLength = this._keyBytes * 8; + this._n = new Uint8Array(this._keyBytes); + this._e = new Uint8Array(this._keyBytes); + this._n.set(n); + this._e.set(e); + this._nBigInt = u8ArrayToBigInt(this._n); + this._eBigInt = u8ArrayToBigInt(this._e); + this._extractable = extractable; + } + + async encrypt(_algorithm, message) { + if (message.length > this._keyBytes - 11) { + return null; + } + const ps = new Uint8Array(this._keyBytes - message.length - 3); + window.crypto.getRandomValues(ps); + for (let i = 0; i < ps.length; i++) { + ps[i] = Math.floor(ps[i] * 254 / 255 + 1); + } + const em = new Uint8Array(this._keyBytes); + em[1] = 0x02; + em.set(ps, 2); + em.set(message, ps.length + 3); + const emBigInt = u8ArrayToBigInt(em); + const c = modPow(emBigInt, this._eBigInt, this._nBigInt); + return bigIntToU8Array(c, this._keyBytes); + } + + async decrypt(_algorithm, message) { + if (message.length !== this._keyBytes) { + return null; + } + const msgBigInt = u8ArrayToBigInt(message); + const emBigInt = modPow(msgBigInt, this._dBigInt, this._nBigInt); + const em = bigIntToU8Array(emBigInt, this._keyBytes); + if (em[0] !== 0x00 || em[1] !== 0x02) { + return null; + } + let i = 2; + for (; i < em.length; i++) { + if (em[i] === 0x00) { + break; + } + } + if (i === em.length) { + return null; + } + return em.slice(i + 1, em.length); + } + + async exportKey() { + if (!this._extractable) { + throw new Error("key is not extractable"); + } + return { n: this._n, e: this._e, d: this._d }; + } +} diff --git a/public/novnc/core/decoders/hextile.js b/public/novnc/core/decoders/hextile.js index ac21eff0..cc33e0e1 100644 --- a/public/novnc/core/decoders/hextile.js +++ b/public/novnc/core/decoders/hextile.js @@ -31,10 +31,7 @@ export default class HextileDecoder { return false; } - let rQ = sock.rQ; - let rQi = sock.rQi; - - let subencoding = rQ[rQi]; // Peek + let subencoding = sock.rQpeek8(); if (subencoding > 30) { // Raw throw new Error("Illegal hextile subencoding (subencoding: " + subencoding + ")"); @@ -65,7 +62,7 @@ export default class HextileDecoder { return false; } - let subrects = rQ[rQi + bytes - 1]; // Peek + let subrects = sock.rQpeekBytes(bytes).at(-1); if (subencoding & 0x10) { // SubrectsColoured bytes += subrects * (4 + 2); } else { @@ -79,7 +76,7 @@ export default class HextileDecoder { } // We know the encoding and have a whole tile - rQi++; + sock.rQshift8(); if (subencoding === 0) { if (this._lastsubencoding & 0x01) { // Weird: ignore blanks are RAW @@ -89,42 +86,36 @@ export default class HextileDecoder { } } else if (subencoding & 0x01) { // Raw let pixels = tw * th; + let data = sock.rQshiftBytes(pixels * 4, false); // Max sure the image is fully opaque for (let i = 0;i < pixels;i++) { - rQ[rQi + i * 4 + 3] = 255; + data[i * 4 + 3] = 255; } - display.blitImage(tx, ty, tw, th, rQ, rQi); - rQi += bytes - 1; + display.blitImage(tx, ty, tw, th, data, 0); } else { if (subencoding & 0x02) { // Background - this._background = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; - rQi += 4; + this._background = new Uint8Array(sock.rQshiftBytes(4)); } if (subencoding & 0x04) { // Foreground - this._foreground = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; - rQi += 4; + this._foreground = new Uint8Array(sock.rQshiftBytes(4)); } this._startTile(tx, ty, tw, th, this._background); if (subencoding & 0x08) { // AnySubrects - let subrects = rQ[rQi]; - rQi++; + let subrects = sock.rQshift8(); for (let s = 0; s < subrects; s++) { let color; if (subencoding & 0x10) { // SubrectsColoured - color = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; - rQi += 4; + color = sock.rQshiftBytes(4); } else { color = this._foreground; } - const xy = rQ[rQi]; - rQi++; + const xy = sock.rQshift8(); const sx = (xy >> 4); const sy = (xy & 0x0f); - const wh = rQ[rQi]; - rQi++; + const wh = sock.rQshift8(); const sw = (wh >> 4) + 1; const sh = (wh & 0x0f) + 1; @@ -133,7 +124,6 @@ export default class HextileDecoder { } this._finishTile(display); } - sock.rQi = rQi; this._lastsubencoding = subencoding; this._tiles--; } diff --git a/public/novnc/core/decoders/jpeg.js b/public/novnc/core/decoders/jpeg.js index e1f2bdf8..feb2aeb6 100644 --- a/public/novnc/core/decoders/jpeg.js +++ b/public/novnc/core/decoders/jpeg.js @@ -11,131 +11,136 @@ export default class JPEGDecoder { constructor() { // RealVNC will reuse the quantization tables // and Huffman tables, so we need to cache them. - this._quantTables = []; - this._huffmanTables = []; this._cachedQuantTables = []; this._cachedHuffmanTables = []; - this._jpegLength = 0; this._segments = []; } decodeRect(x, y, width, height, sock, display, depth) { // A rect of JPEG encodings is simply a JPEG file - if (!this._parseJPEG(sock.rQslice(0))) { - return false; - } - const data = sock.rQshiftBytes(this._jpegLength); - if (this._quantTables.length != 0 && this._huffmanTables.length != 0) { - // If there are quantization tables and Huffman tables in the JPEG - // image, we can directly render it. - display.imageRect(x, y, width, height, "image/jpeg", data); - return true; - } else { - // Otherwise we need to insert cached tables. - const sofIndex = this._segments.findIndex( - x => x[1] == 0xC0 || x[1] == 0xC2 - ); - if (sofIndex == -1) { - throw new Error("Illegal JPEG image without SOF"); - } - let segments = this._segments.slice(0, sofIndex); - segments = segments.concat(this._quantTables.length ? - this._quantTables : - this._cachedQuantTables); - segments.push(this._segments[sofIndex]); - segments = segments.concat(this._huffmanTables.length ? - this._huffmanTables : - this._cachedHuffmanTables, - this._segments.slice(sofIndex + 1)); - let length = 0; - for (let i = 0; i < segments.length; i++) { - length += segments[i].length; - } - const data = new Uint8Array(length); - length = 0; - for (let i = 0; i < segments.length; i++) { - data.set(segments[i], length); - length += segments[i].length; - } - display.imageRect(x, y, width, height, "image/jpeg", data); - return true; - } - } - - _parseJPEG(buffer) { - if (this._quantTables.length != 0) { - this._cachedQuantTables = this._quantTables; - } - if (this._huffmanTables.length != 0) { - this._cachedHuffmanTables = this._huffmanTables; - } - this._quantTables = []; - this._huffmanTables = []; - this._segments = []; - let i = 0; - let bufferLength = buffer.length; while (true) { - let j = i; - if (j + 2 > bufferLength) { + let segment = this._readSegment(sock); + if (segment === null) { return false; } - if (buffer[j] != 0xFF) { - throw new Error("Illegal JPEG marker received (byte: " + - buffer[j] + ")"); - } - const type = buffer[j+1]; - j += 2; - if (type == 0xD9) { - this._jpegLength = j; - this._segments.push(buffer.slice(i, j)); - return true; - } else if (type == 0xDA) { - // start of scan - let hasFoundEndOfScan = false; - for (let k = j + 3; k + 1 < bufferLength; k++) { - if (buffer[k] == 0xFF && buffer[k+1] != 0x00 && - !(buffer[k+1] >= 0xD0 && buffer[k+1] <= 0xD7)) { - j = k; - hasFoundEndOfScan = true; - break; - } - } - if (!hasFoundEndOfScan) { - return false; - } - this._segments.push(buffer.slice(i, j)); - i = j; - continue; - } else if (type >= 0xD0 && type < 0xD9 || type == 0x01) { - // No length after marker - this._segments.push(buffer.slice(i, j)); - i = j; - continue; - } - if (j + 2 > bufferLength) { - return false; - } - const length = (buffer[j] << 8) + buffer[j+1] - 2; - if (length < 0) { - throw new Error("Illegal JPEG length received (length: " + - length + ")"); - } - j += 2; - if (j + length > bufferLength) { - return false; - } - j += length; - const segment = buffer.slice(i, j); - if (type == 0xC4) { - // Huffman tables - this._huffmanTables.push(segment); - } else if (type == 0xDB) { - // Quantization tables - this._quantTables.push(segment); - } this._segments.push(segment); - i = j; + // End of image? + if (segment[1] === 0xD9) { + break; + } } + + let huffmanTables = []; + let quantTables = []; + for (let segment of this._segments) { + let type = segment[1]; + if (type === 0xC4) { + // Huffman tables + huffmanTables.push(segment); + } else if (type === 0xDB) { + // Quantization tables + quantTables.push(segment); + } + } + + const sofIndex = this._segments.findIndex( + x => x[1] == 0xC0 || x[1] == 0xC2 + ); + if (sofIndex == -1) { + throw new Error("Illegal JPEG image without SOF"); + } + + if (quantTables.length === 0) { + this._segments.splice(sofIndex+1, 0, + ...this._cachedQuantTables); + } + if (huffmanTables.length === 0) { + this._segments.splice(sofIndex+1, 0, + ...this._cachedHuffmanTables); + } + + let length = 0; + for (let segment of this._segments) { + length += segment.length; + } + + let data = new Uint8Array(length); + length = 0; + for (let segment of this._segments) { + data.set(segment, length); + length += segment.length; + } + + display.imageRect(x, y, width, height, "image/jpeg", data); + + if (huffmanTables.length !== 0) { + this._cachedHuffmanTables = huffmanTables; + } + if (quantTables.length !== 0) { + this._cachedQuantTables = quantTables; + } + + this._segments = []; + + return true; + } + + _readSegment(sock) { + if (sock.rQwait("JPEG", 2)) { + return null; + } + + let marker = sock.rQshift8(); + if (marker != 0xFF) { + throw new Error("Illegal JPEG marker received (byte: " + + marker + ")"); + } + let type = sock.rQshift8(); + if (type >= 0xD0 && type <= 0xD9 || type == 0x01) { + // No length after marker + return new Uint8Array([marker, type]); + } + + if (sock.rQwait("JPEG", 2, 2)) { + return null; + } + + let length = sock.rQshift16(); + if (length < 2) { + throw new Error("Illegal JPEG length received (length: " + + length + ")"); + } + + if (sock.rQwait("JPEG", length-2, 4)) { + return null; + } + + let extra = 0; + if (type === 0xDA) { + // start of scan + extra += 2; + while (true) { + if (sock.rQwait("JPEG", length-2+extra, 4)) { + return null; + } + let data = sock.rQpeekBytes(length-2+extra, false); + if (data.at(-2) === 0xFF && data.at(-1) !== 0x00 && + !(data.at(-1) >= 0xD0 && data.at(-1) <= 0xD7)) { + extra -= 2; + break; + } + extra++; + } + } + + let segment = new Uint8Array(2 + length + extra); + segment[0] = marker; + segment[1] = type; + segment[2] = length >> 8; + segment[3] = length; + segment.set(sock.rQshiftBytes(length-2+extra, false), 4); + + return segment; } } diff --git a/public/novnc/core/decoders/raw.js b/public/novnc/core/decoders/raw.js index d08f7ba9..3c166142 100644 --- a/public/novnc/core/decoders/raw.js +++ b/public/novnc/core/decoders/raw.js @@ -24,41 +24,34 @@ export default class RawDecoder { const pixelSize = depth == 8 ? 1 : 4; const bytesPerLine = width * pixelSize; - if (sock.rQwait("RAW", bytesPerLine)) { - return false; - } - - const curY = y + (height - this._lines); - const currHeight = Math.min(this._lines, - Math.floor(sock.rQlen / bytesPerLine)); - const pixels = width * currHeight; - - let data = sock.rQ; - let index = sock.rQi; - - // Convert data if needed - if (depth == 8) { - const newdata = new Uint8Array(pixels * 4); - for (let i = 0; i < pixels; i++) { - newdata[i * 4 + 0] = ((data[index + i] >> 0) & 0x3) * 255 / 3; - newdata[i * 4 + 1] = ((data[index + i] >> 2) & 0x3) * 255 / 3; - newdata[i * 4 + 2] = ((data[index + i] >> 4) & 0x3) * 255 / 3; - newdata[i * 4 + 3] = 255; + while (this._lines > 0) { + if (sock.rQwait("RAW", bytesPerLine)) { + return false; } - data = newdata; - index = 0; - } - // Max sure the image is fully opaque - for (let i = 0; i < pixels; i++) { - data[index + i * 4 + 3] = 255; - } + const curY = y + (height - this._lines); - display.blitImage(x, curY, width, currHeight, data, index); - sock.rQskipBytes(currHeight * bytesPerLine); - this._lines -= currHeight; - if (this._lines > 0) { - return false; + let data = sock.rQshiftBytes(bytesPerLine, false); + + // Convert data if needed + if (depth == 8) { + const newdata = new Uint8Array(width * 4); + for (let i = 0; i < width; i++) { + newdata[i * 4 + 0] = ((data[i] >> 0) & 0x3) * 255 / 3; + newdata[i * 4 + 1] = ((data[i] >> 2) & 0x3) * 255 / 3; + newdata[i * 4 + 2] = ((data[i] >> 4) & 0x3) * 255 / 3; + newdata[i * 4 + 3] = 255; + } + data = newdata; + } + + // Max sure the image is fully opaque + for (let i = 0; i < width; i++) { + data[i * 4 + 3] = 255; + } + + display.blitImage(x, curY, width, 1, data, 0); + this._lines--; } return true; diff --git a/public/novnc/core/decoders/tight.js b/public/novnc/core/decoders/tight.js index 7952707c..8bc977a7 100644 --- a/public/novnc/core/decoders/tight.js +++ b/public/novnc/core/decoders/tight.js @@ -76,12 +76,8 @@ export default class TightDecoder { return false; } - const rQi = sock.rQi; - const rQ = sock.rQ; - - display.fillRect(x, y, width, height, - [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2]], false); - sock.rQskipBytes(3); + let pixel = sock.rQshiftBytes(3); + display.fillRect(x, y, width, height, pixel, false); return true; } @@ -289,7 +285,73 @@ export default class TightDecoder { } _gradientFilter(streamId, x, y, width, height, sock, display, depth) { - throw new Error("Gradient filter not implemented"); + // assume the TPIXEL is 3 bytes long + const uncompressedSize = width * height * 3; + let data; + + if (uncompressedSize === 0) { + return true; + } + + if (uncompressedSize < 12) { + if (sock.rQwait("TIGHT", uncompressedSize)) { + return false; + } + + data = sock.rQshiftBytes(uncompressedSize); + } else { + data = this._readData(sock); + if (data === null) { + return false; + } + + this._zlibs[streamId].setInput(data); + data = this._zlibs[streamId].inflate(uncompressedSize); + this._zlibs[streamId].setInput(null); + } + + let rgbx = new Uint8Array(4 * width * height); + + let rgbxIndex = 0, dataIndex = 0; + let left = new Uint8Array(3); + for (let x = 0; x < width; x++) { + for (let c = 0; c < 3; c++) { + const prediction = left[c]; + const value = data[dataIndex++] + prediction; + rgbx[rgbxIndex++] = value; + left[c] = value; + } + rgbx[rgbxIndex++] = 255; + } + + let upperIndex = 0; + let upper = new Uint8Array(3), + upperleft = new Uint8Array(3); + for (let y = 1; y < height; y++) { + left.fill(0); + upperleft.fill(0); + for (let x = 0; x < width; x++) { + for (let c = 0; c < 3; c++) { + upper[c] = rgbx[upperIndex++]; + let prediction = left[c] + upper[c] - upperleft[c]; + if (prediction < 0) { + prediction = 0; + } else if (prediction > 255) { + prediction = 255; + } + const value = data[dataIndex++] + prediction; + rgbx[rgbxIndex++] = value; + upperleft[c] = upper[c]; + left[c] = value; + } + rgbx[rgbxIndex++] = 255; + upperIndex++; + } + } + + display.blitImage(x, y, width, height, rgbx, 0, false); + + return true; } _readData(sock) { @@ -316,7 +378,7 @@ export default class TightDecoder { return null; } - let data = sock.rQshiftBytes(this._len); + let data = sock.rQshiftBytes(this._len, false); this._len = 0; return data; diff --git a/public/novnc/core/decoders/zrle.js b/public/novnc/core/decoders/zrle.js index 97fbd58e..49128e79 100644 --- a/public/novnc/core/decoders/zrle.js +++ b/public/novnc/core/decoders/zrle.js @@ -32,7 +32,7 @@ export default class ZRLEDecoder { return false; } - const data = sock.rQshiftBytes(this._length); + const data = sock.rQshiftBytes(this._length, false); this._inflator.setInput(data); diff --git a/public/novnc/core/deflator.js b/public/novnc/core/deflator.js index fe2a8f70..22f6770b 100644 --- a/public/novnc/core/deflator.js +++ b/public/novnc/core/deflator.js @@ -7,7 +7,7 @@ */ import { deflateInit, deflate } from "../vendor/pako/lib/zlib/deflate.js"; -import { Z_FULL_FLUSH } from "../vendor/pako/lib/zlib/deflate.js"; +import { Z_FULL_FLUSH, Z_DEFAULT_COMPRESSION } from "../vendor/pako/lib/zlib/deflate.js"; import ZStream from "../vendor/pako/lib/zlib/zstream.js"; export default class Deflator { @@ -15,9 +15,8 @@ export default class Deflator { this.strm = new ZStream(); this.chunkSize = 1024 * 10 * 10; this.outputBuffer = new Uint8Array(this.chunkSize); - this.windowBits = 5; - deflateInit(this.strm, this.windowBits); + deflateInit(this.strm, Z_DEFAULT_COMPRESSION); } deflate(inData) { diff --git a/public/novnc/core/display.js b/public/novnc/core/display.js index bf8d5fab..fcd62699 100644 --- a/public/novnc/core/display.js +++ b/public/novnc/core/display.js @@ -15,7 +15,7 @@ export default class Display { this._drawCtx = null; this._renderQ = []; // queue drawing actions for in-oder rendering - this._flushing = false; + this._flushPromise = null; // the full frame buffer (logical canvas) size this._fbWidth = 0; @@ -61,10 +61,6 @@ export default class Display { this._scale = 1.0; this._clipViewport = false; - - // ===== EVENT HANDLERS ===== - - this.onflush = () => {}; // A flush request has finished } // ===== PROPERTIES ===== @@ -306,9 +302,14 @@ export default class Display { flush() { if (this._renderQ.length === 0) { - this.onflush(); + return Promise.resolve(); } else { - this._flushing = true; + if (this._flushPromise === null) { + this._flushPromise = new Promise((resolve) => { + this._flushResolve = resolve; + }); + } + return this._flushPromise; } } @@ -517,9 +518,11 @@ export default class Display { } } - if (this._renderQ.length === 0 && this._flushing) { - this._flushing = false; - this.onflush(); + if (this._renderQ.length === 0 && + this._flushPromise !== null) { + this._flushResolve(); + this._flushPromise = null; + this._flushResolve = null; } } } diff --git a/public/novnc/core/encodings.js b/public/novnc/core/encodings.js index 2041b6e0..1a79989d 100644 --- a/public/novnc/core/encodings.js +++ b/public/novnc/core/encodings.js @@ -22,6 +22,7 @@ export const encodings = { pseudoEncodingLastRect: -224, pseudoEncodingCursor: -239, pseudoEncodingQEMUExtendedKeyEvent: -258, + pseudoEncodingQEMULedEvent: -261, pseudoEncodingDesktopName: -307, pseudoEncodingExtendedDesktopSize: -308, pseudoEncodingXvp: -309, diff --git a/public/novnc/core/inflator.js b/public/novnc/core/inflator.js index 4b337607..f851f2a7 100644 --- a/public/novnc/core/inflator.js +++ b/public/novnc/core/inflator.js @@ -14,9 +14,8 @@ export default class Inflate { this.strm = new ZStream(); this.chunkSize = 1024 * 10 * 10; this.strm.output = new Uint8Array(this.chunkSize); - this.windowBits = 5; - inflateInit(this.strm, this.windowBits); + inflateInit(this.strm); } setInput(data) { diff --git a/public/novnc/core/input/keyboard.js b/public/novnc/core/input/keyboard.js index ddb5ce09..68da2312 100644 --- a/public/novnc/core/input/keyboard.js +++ b/public/novnc/core/input/keyboard.js @@ -36,7 +36,7 @@ export default class Keyboard { // ===== PRIVATE METHODS ===== - _sendKeyEvent(keysym, code, down) { + _sendKeyEvent(keysym, code, down, numlock = null, capslock = null) { if (down) { this._keyDownList[code] = keysym; } else { @@ -48,8 +48,9 @@ export default class Keyboard { } Log.Debug("onkeyevent " + (down ? "down" : "up") + - ", keysym: " + keysym, ", code: " + code); - this.onkeyevent(keysym, code, down); + ", keysym: " + keysym, ", code: " + code + + ", numlock: " + numlock + ", capslock: " + capslock); + this.onkeyevent(keysym, code, down, numlock, capslock); } _getKeyCode(e) { @@ -86,6 +87,14 @@ export default class Keyboard { _handleKeyDown(e) { const code = this._getKeyCode(e); let keysym = KeyboardUtil.getKeysym(e); + let numlock = e.getModifierState('NumLock'); + let capslock = e.getModifierState('CapsLock'); + + // getModifierState for NumLock is not supported on mac and ios and always returns false. + // Set to null to indicate unknown/unsupported instead. + if (browser.isMac() || browser.isIOS()) { + numlock = null; + } // Windows doesn't have a proper AltGr, but handles it using // fake Ctrl+Alt. However the remote end might not be Windows, @@ -107,7 +116,7 @@ export default class Keyboard { // key to "AltGraph". keysym = KeyTable.XK_ISO_Level3_Shift; } else { - this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); + this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true, numlock, capslock); } } @@ -118,8 +127,8 @@ export default class Keyboard { // If it's a virtual keyboard then it should be // sufficient to just send press and release right // after each other - this._sendKeyEvent(keysym, code, true); - this._sendKeyEvent(keysym, code, false); + this._sendKeyEvent(keysym, code, true, numlock, capslock); + this._sendKeyEvent(keysym, code, false, numlock, capslock); } stopEvent(e); @@ -157,8 +166,8 @@ export default class Keyboard { // while meta is held down if ((browser.isMac() || browser.isIOS()) && (e.metaKey && code !== 'MetaLeft' && code !== 'MetaRight')) { - this._sendKeyEvent(keysym, code, true); - this._sendKeyEvent(keysym, code, false); + this._sendKeyEvent(keysym, code, true, numlock, capslock); + this._sendKeyEvent(keysym, code, false, numlock, capslock); stopEvent(e); return; } @@ -168,8 +177,8 @@ export default class Keyboard { // which toggles on each press, but not on release. So pretend // it was a quick press and release of the button. if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) { - this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true); - this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false); + this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true, numlock, capslock); + this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false, numlock, capslock); stopEvent(e); return; } @@ -182,8 +191,8 @@ export default class Keyboard { KeyTable.XK_Hiragana, KeyTable.XK_Romaji ]; if (browser.isWindows() && jpBadKeys.includes(keysym)) { - this._sendKeyEvent(keysym, code, true); - this._sendKeyEvent(keysym, code, false); + this._sendKeyEvent(keysym, code, true, numlock, capslock); + this._sendKeyEvent(keysym, code, false, numlock, capslock); stopEvent(e); return; } @@ -199,7 +208,7 @@ export default class Keyboard { return; } - this._sendKeyEvent(keysym, code, true); + this._sendKeyEvent(keysym, code, true, numlock, capslock); } _handleKeyUp(e) { diff --git a/public/novnc/core/input/util.js b/public/novnc/core/input/util.js index 58f84e55..36b69817 100644 --- a/public/novnc/core/input/util.js +++ b/public/novnc/core/input/util.js @@ -67,7 +67,7 @@ export function getKeycode(evt) { // Get 'KeyboardEvent.key', handling legacy browsers export function getKey(evt) { // Are we getting a proper key value? - if (evt.key !== undefined) { + if ((evt.key !== undefined) && (evt.key !== 'Unidentified')) { // Mozilla isn't fully in sync with the spec yet switch (evt.key) { case 'OS': return 'Meta'; diff --git a/public/novnc/core/ra2.js b/public/novnc/core/ra2.js index 81a8a895..d330b848 100644 --- a/public/novnc/core/ra2.js +++ b/public/novnc/core/ra2.js @@ -1,146 +1,25 @@ -import Base64 from './base64.js'; import { encodeUTF8 } from './util/strings.js'; import EventTargetMixin from './util/eventtarget.js'; +import legacyCrypto from './crypto/crypto.js'; -export class AESEAXCipher { +class RA2Cipher { constructor() { - this._rawKey = null; - this._ctrKey = null; - this._cbcKey = null; - this._zeroBlock = new Uint8Array(16); - this._prefixBlock0 = this._zeroBlock; - this._prefixBlock1 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); - this._prefixBlock2 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); - } - - async _encryptBlock(block) { - const encrypted = await window.crypto.subtle.encrypt({ - name: "AES-CBC", - iv: this._zeroBlock, - }, this._cbcKey, block); - return new Uint8Array(encrypted).slice(0, 16); - } - - async _initCMAC() { - const k1 = await this._encryptBlock(this._zeroBlock); - const k2 = new Uint8Array(16); - const v = k1[0] >>> 6; - for (let i = 0; i < 15; i++) { - k2[i] = (k1[i + 1] >> 6) | (k1[i] << 2); - k1[i] = (k1[i + 1] >> 7) | (k1[i] << 1); - } - const lut = [0x0, 0x87, 0x0e, 0x89]; - k2[14] ^= v >>> 1; - k2[15] = (k1[15] << 2) ^ lut[v]; - k1[15] = (k1[15] << 1) ^ lut[v >> 1]; - this._k1 = k1; - this._k2 = k2; - } - - async _encryptCTR(data, counter) { - const encrypted = await window.crypto.subtle.encrypt({ - "name": "AES-CTR", - counter: counter, - length: 128 - }, this._ctrKey, data); - return new Uint8Array(encrypted); - } - - async _decryptCTR(data, counter) { - const decrypted = await window.crypto.subtle.decrypt({ - "name": "AES-CTR", - counter: counter, - length: 128 - }, this._ctrKey, data); - return new Uint8Array(decrypted); - } - - async _computeCMAC(data, prefixBlock) { - if (prefixBlock.length !== 16) { - return null; - } - const n = Math.floor(data.length / 16); - const m = Math.ceil(data.length / 16); - const r = data.length - n * 16; - const cbcData = new Uint8Array((m + 1) * 16); - cbcData.set(prefixBlock); - cbcData.set(data, 16); - if (r === 0) { - for (let i = 0; i < 16; i++) { - cbcData[n * 16 + i] ^= this._k1[i]; - } - } else { - cbcData[(n + 1) * 16 + r] = 0x80; - for (let i = 0; i < 16; i++) { - cbcData[(n + 1) * 16 + i] ^= this._k2[i]; - } - } - let cbcEncrypted = await window.crypto.subtle.encrypt({ - name: "AES-CBC", - iv: this._zeroBlock, - }, this._cbcKey, cbcData); - - cbcEncrypted = new Uint8Array(cbcEncrypted); - const mac = cbcEncrypted.slice(cbcEncrypted.length - 32, cbcEncrypted.length - 16); - return mac; - } - - async setKey(key) { - this._rawKey = key; - this._ctrKey = await window.crypto.subtle.importKey( - "raw", key, {"name": "AES-CTR"}, false, ["encrypt", "decrypt"]); - this._cbcKey = await window.crypto.subtle.importKey( - "raw", key, {"name": "AES-CBC"}, false, ["encrypt", "decrypt"]); - await this._initCMAC(); - } - - async encrypt(message, associatedData, nonce) { - const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0); - const encrypted = await this._encryptCTR(message, nCMAC); - const adCMAC = await this._computeCMAC(associatedData, this._prefixBlock1); - const mac = await this._computeCMAC(encrypted, this._prefixBlock2); - for (let i = 0; i < 16; i++) { - mac[i] ^= nCMAC[i] ^ adCMAC[i]; - } - const res = new Uint8Array(16 + encrypted.length); - res.set(encrypted); - res.set(mac, encrypted.length); - return res; - } - - async decrypt(encrypted, associatedData, nonce, mac) { - const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0); - const adCMAC = await this._computeCMAC(associatedData, this._prefixBlock1); - const computedMac = await this._computeCMAC(encrypted, this._prefixBlock2); - for (let i = 0; i < 16; i++) { - computedMac[i] ^= nCMAC[i] ^ adCMAC[i]; - } - if (computedMac.length !== mac.length) { - return null; - } - for (let i = 0; i < mac.length; i++) { - if (computedMac[i] !== mac[i]) { - return null; - } - } - const res = await this._decryptCTR(encrypted, nCMAC); - return res; - } -} - -export class RA2Cipher { - constructor() { - this._cipher = new AESEAXCipher(); + this._cipher = null; this._counter = new Uint8Array(16); } async setKey(key) { - await this._cipher.setKey(key); + this._cipher = await legacyCrypto.importKey( + "raw", key, { name: "AES-EAX" }, false, ["encrypt, decrypt"]); } async makeMessage(message) { const ad = new Uint8Array([(message.length & 0xff00) >>> 8, message.length & 0xff]); - const encrypted = await this._cipher.encrypt(message, ad, this._counter); + const encrypted = await legacyCrypto.encrypt({ + name: "AES-EAX", + iv: this._counter, + additionalData: ad, + }, this._cipher, message); for (let i = 0; i < 16 && this._counter[i]++ === 255; i++); const res = new Uint8Array(message.length + 2 + 16); res.set(ad); @@ -148,164 +27,18 @@ export class RA2Cipher { return res; } - async receiveMessage(length, encrypted, mac) { + async receiveMessage(length, encrypted) { const ad = new Uint8Array([(length & 0xff00) >>> 8, length & 0xff]); - const res = await this._cipher.decrypt(encrypted, ad, this._counter, mac); + const res = await legacyCrypto.decrypt({ + name: "AES-EAX", + iv: this._counter, + additionalData: ad, + }, this._cipher, encrypted); for (let i = 0; i < 16 && this._counter[i]++ === 255; i++); return res; } } -export class RSACipher { - constructor(keyLength) { - this._key = null; - this._keyLength = keyLength; - this._keyBytes = Math.ceil(keyLength / 8); - this._n = null; - this._e = null; - this._d = null; - this._nBigInt = null; - this._eBigInt = null; - this._dBigInt = null; - } - - _base64urlDecode(data) { - data = data.replace(/-/g, "+").replace(/_/g, "/"); - data = data.padEnd(Math.ceil(data.length / 4) * 4, "="); - return Base64.decode(data); - } - - _u8ArrayToBigInt(arr) { - let hex = '0x'; - for (let i = 0; i < arr.length; i++) { - hex += arr[i].toString(16).padStart(2, '0'); - } - return BigInt(hex); - } - - _padArray(arr, length) { - const res = new Uint8Array(length); - res.set(arr, length - arr.length); - return res; - } - - _bigIntToU8Array(bigint, padLength=0) { - let hex = bigint.toString(16); - if (padLength === 0) { - padLength = Math.ceil(hex.length / 2) * 2; - } - hex = hex.padStart(padLength * 2, '0'); - const length = hex.length / 2; - const arr = new Uint8Array(length); - for (let i = 0; i < length; i++) { - arr[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); - } - return arr; - } - - _modPow(b, e, m) { - if (m === 1n) { - return 0; - } - let r = 1n; - b = b % m; - while (e > 0) { - if (e % 2n === 1n) { - r = (r * b) % m; - } - e = e / 2n; - b = (b * b) % m; - } - return r; - } - - async generateKey() { - this._key = await window.crypto.subtle.generateKey( - { - name: "RSA-OAEP", - modulusLength: this._keyLength, - publicExponent: new Uint8Array([0x01, 0x00, 0x01]), - hash: {name: "SHA-256"}, - }, - true, ["encrypt", "decrypt"]); - const privateKey = await window.crypto.subtle.exportKey("jwk", this._key.privateKey); - this._n = this._padArray(this._base64urlDecode(privateKey.n), this._keyBytes); - this._nBigInt = this._u8ArrayToBigInt(this._n); - this._e = this._padArray(this._base64urlDecode(privateKey.e), this._keyBytes); - this._eBigInt = this._u8ArrayToBigInt(this._e); - this._d = this._padArray(this._base64urlDecode(privateKey.d), this._keyBytes); - this._dBigInt = this._u8ArrayToBigInt(this._d); - } - - setPublicKey(n, e) { - if (n.length !== this._keyBytes || e.length !== this._keyBytes) { - return; - } - this._n = new Uint8Array(this._keyBytes); - this._e = new Uint8Array(this._keyBytes); - this._n.set(n); - this._e.set(e); - this._nBigInt = this._u8ArrayToBigInt(this._n); - this._eBigInt = this._u8ArrayToBigInt(this._e); - } - - encrypt(message) { - if (message.length > this._keyBytes - 11) { - return null; - } - const ps = new Uint8Array(this._keyBytes - message.length - 3); - window.crypto.getRandomValues(ps); - for (let i = 0; i < ps.length; i++) { - ps[i] = Math.floor(ps[i] * 254 / 255 + 1); - } - const em = new Uint8Array(this._keyBytes); - em[1] = 0x02; - em.set(ps, 2); - em.set(message, ps.length + 3); - const emBigInt = this._u8ArrayToBigInt(em); - const c = this._modPow(emBigInt, this._eBigInt, this._nBigInt); - return this._bigIntToU8Array(c, this._keyBytes); - } - - decrypt(message) { - if (message.length !== this._keyBytes) { - return null; - } - const msgBigInt = this._u8ArrayToBigInt(message); - const emBigInt = this._modPow(msgBigInt, this._dBigInt, this._nBigInt); - const em = this._bigIntToU8Array(emBigInt, this._keyBytes); - if (em[0] !== 0x00 || em[1] !== 0x02) { - return null; - } - let i = 2; - for (; i < em.length; i++) { - if (em[i] === 0x00) { - break; - } - } - if (i === em.length) { - return null; - } - return em.slice(i + 1, em.length); - } - - get keyLength() { - return this._keyLength; - } - - get n() { - return this._n; - } - - get e() { - return this._e; - } - - get d() { - return this._d; - } -} - export default class RSAAESAuthenticationState extends EventTargetMixin { constructor(sock, getCredentials) { super(); @@ -406,7 +139,7 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { this._hasStarted = true; // 1: Receive server public key await this._waitSockAsync(4); - const serverKeyLengthBuffer = this._sock.rQslice(0, 4); + const serverKeyLengthBuffer = this._sock.rQpeekBytes(4); const serverKeyLength = this._sock.rQshift32(); if (serverKeyLength < 1024) { throw new Error("RA2: server public key is too short: " + serverKeyLength); @@ -417,26 +150,31 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { await this._waitSockAsync(serverKeyBytes * 2); const serverN = this._sock.rQshiftBytes(serverKeyBytes); const serverE = this._sock.rQshiftBytes(serverKeyBytes); - const serverRSACipher = new RSACipher(serverKeyLength); - serverRSACipher.setPublicKey(serverN, serverE); + const serverRSACipher = await legacyCrypto.importKey( + "raw", { n: serverN, e: serverE }, { name: "RSA-PKCS1-v1_5" }, false, ["encrypt"]); const serverPublickey = new Uint8Array(4 + serverKeyBytes * 2); serverPublickey.set(serverKeyLengthBuffer); serverPublickey.set(serverN, 4); serverPublickey.set(serverE, 4 + serverKeyBytes); // verify server public key + let approveKey = this._waitApproveKeyAsync(); this.dispatchEvent(new CustomEvent("serververification", { detail: { type: "RSA", publickey: serverPublickey } })); - await this._waitApproveKeyAsync(); + await approveKey; // 2: Send client public key const clientKeyLength = 2048; const clientKeyBytes = Math.ceil(clientKeyLength / 8); - const clientRSACipher = new RSACipher(clientKeyLength); - await clientRSACipher.generateKey(); - const clientN = clientRSACipher.n; - const clientE = clientRSACipher.e; + const clientRSACipher = (await legacyCrypto.generateKey({ + name: "RSA-PKCS1-v1_5", + modulusLength: clientKeyLength, + publicExponent: new Uint8Array([1, 0, 1]), + }, true, ["encrypt"])).privateKey; + const clientExportedRSAKey = await legacyCrypto.exportKey("raw", clientRSACipher); + const clientN = clientExportedRSAKey.n; + const clientE = clientExportedRSAKey.e; const clientPublicKey = new Uint8Array(4 + clientKeyBytes * 2); clientPublicKey[0] = (clientKeyLength & 0xff000000) >>> 24; clientPublicKey[1] = (clientKeyLength & 0xff0000) >>> 16; @@ -444,17 +182,20 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { clientPublicKey[3] = clientKeyLength & 0xff; clientPublicKey.set(clientN, 4); clientPublicKey.set(clientE, 4 + clientKeyBytes); - this._sock.send(clientPublicKey); + this._sock.sQpushBytes(clientPublicKey); + this._sock.flush(); // 3: Send client random const clientRandom = new Uint8Array(16); window.crypto.getRandomValues(clientRandom); - const clientEncryptedRandom = serverRSACipher.encrypt(clientRandom); + const clientEncryptedRandom = await legacyCrypto.encrypt( + { name: "RSA-PKCS1-v1_5" }, serverRSACipher, clientRandom); const clientRandomMessage = new Uint8Array(2 + serverKeyBytes); clientRandomMessage[0] = (serverKeyBytes & 0xff00) >>> 8; clientRandomMessage[1] = serverKeyBytes & 0xff; clientRandomMessage.set(clientEncryptedRandom, 2); - this._sock.send(clientRandomMessage); + this._sock.sQpushBytes(clientRandomMessage); + this._sock.flush(); // 4: Receive server random await this._waitSockAsync(2); @@ -462,7 +203,8 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { throw new Error("RA2: wrong encrypted message length"); } const serverEncryptedRandom = this._sock.rQshiftBytes(clientKeyBytes); - const serverRandom = clientRSACipher.decrypt(serverEncryptedRandom); + const serverRandom = await legacyCrypto.decrypt( + { name: "RSA-PKCS1-v1_5" }, clientRSACipher, serverEncryptedRandom); if (serverRandom === null || serverRandom.length !== 16) { throw new Error("RA2: corrupted server encrypted random"); } @@ -494,13 +236,14 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { clientHash = await window.crypto.subtle.digest("SHA-1", clientHash); serverHash = new Uint8Array(serverHash); clientHash = new Uint8Array(clientHash); - this._sock.send(await clientCipher.makeMessage(clientHash)); + this._sock.sQpushBytes(await clientCipher.makeMessage(clientHash)); + this._sock.flush(); await this._waitSockAsync(2 + 20 + 16); if (this._sock.rQshift16() !== 20) { throw new Error("RA2: wrong server hash"); } const serverHashReceived = await serverCipher.receiveMessage( - 20, this._sock.rQshiftBytes(20), this._sock.rQshiftBytes(16)); + 20, this._sock.rQshiftBytes(20 + 16)); if (serverHashReceived === null) { throw new Error("RA2: failed to authenticate the message"); } @@ -516,11 +259,12 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { throw new Error("RA2: wrong subtype"); } let subtype = (await serverCipher.receiveMessage( - 1, this._sock.rQshiftBytes(1), this._sock.rQshiftBytes(16))); + 1, this._sock.rQshiftBytes(1 + 16))); if (subtype === null) { throw new Error("RA2: failed to authenticate the message"); } subtype = subtype[0]; + let waitCredentials = this._waitCredentialsAsync(subtype); if (subtype === 1) { if (this._getCredentials().username === undefined || this._getCredentials().password === undefined) { @@ -537,7 +281,7 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { } else { throw new Error("RA2: wrong subtype"); } - await this._waitCredentialsAsync(subtype); + await waitCredentials; let username; if (subtype === 1) { username = encodeUTF8(this._getCredentials().username).slice(0, 255); @@ -554,7 +298,8 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { for (let i = 0; i < password.length; i++) { credentials[username.length + 2 + i] = password.charCodeAt(i); } - this._sock.send(await clientCipher.makeMessage(credentials)); + this._sock.sQpushBytes(await clientCipher.makeMessage(credentials)); + this._sock.flush(); } get hasStarted() { @@ -564,4 +309,4 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { set hasStarted(s) { this._hasStarted = s; } -} \ No newline at end of file +} diff --git a/public/novnc/core/rfb.js b/public/novnc/core/rfb.js index 6afd7c65..f2deb0e7 100644 --- a/public/novnc/core/rfb.js +++ b/public/novnc/core/rfb.js @@ -21,12 +21,11 @@ import Keyboard from "./input/keyboard.js"; import GestureHandler from "./input/gesturehandler.js"; import Cursor from "./util/cursor.js"; import Websock from "./websock.js"; -import DES from "./des.js"; import KeyTable from "./input/keysym.js"; import XtScancode from "./input/xtscancodes.js"; import { encodings } from "./encodings.js"; import RSAAESAuthenticationState from "./ra2.js"; -import { MD5 } from "./util/md5.js"; +import legacyCrypto from "./crypto/crypto.js"; import RawDecoder from "./decoders/raw.js"; import CopyRectDecoder from "./decoders/copyrect.js"; @@ -258,10 +257,11 @@ export default class RFB extends EventTargetMixin { Log.Error("Display exception: " + exc); throw exc; } - this._display.onflush = this._onFlush.bind(this); this._keyboard = new Keyboard(this._canvas); this._keyboard.onkeyevent = this._handleKeyEvent.bind(this); + this._remoteCapsLock = null; // Null indicates unknown or irrelevant + this._remoteNumLock = null; this._gestures = new GestureHandler(); @@ -960,7 +960,7 @@ export default class RFB extends EventTargetMixin { } _handleMessage() { - if (this._sock.rQlen === 0) { + if (this._sock.rQwait("message", 1)) { Log.Warn("handleMessage called on an empty receive queue"); return; } @@ -977,7 +977,7 @@ export default class RFB extends EventTargetMixin { if (!this._normalMsg()) { break; } - if (this._sock.rQlen === 0) { + if (this._sock.rQwait("message", 1)) { break; } } @@ -995,7 +995,35 @@ export default class RFB extends EventTargetMixin { } } - _handleKeyEvent(keysym, code, down) { + _handleKeyEvent(keysym, code, down, numlock, capslock) { + // If remote state of capslock is known, and it doesn't match the local led state of + // the keyboard, we send a capslock keypress first to bring it into sync. + // If we just pressed CapsLock, or we toggled it remotely due to it being out of sync + // we clear the remote state so that we don't send duplicate or spurious fixes, + // since it may take some time to receive the new remote CapsLock state. + if (code == 'CapsLock' && down) { + this._remoteCapsLock = null; + } + if (this._remoteCapsLock !== null && capslock !== null && this._remoteCapsLock !== capslock && down) { + Log.Debug("Fixing remote caps lock"); + + this.sendKey(KeyTable.XK_Caps_Lock, 'CapsLock', true); + this.sendKey(KeyTable.XK_Caps_Lock, 'CapsLock', false); + // We clear the remote capsLock state when we do this to prevent issues with doing this twice + // before we receive an update of the the remote state. + this._remoteCapsLock = null; + } + + // Logic for numlock is exactly the same. + if (code == 'NumLock' && down) { + this._remoteNumLock = null; + } + if (this._remoteNumLock !== null && numlock !== null && this._remoteNumLock !== numlock && down) { + Log.Debug("Fixing remote num lock"); + this.sendKey(KeyTable.XK_Num_Lock, 'NumLock', true); + this.sendKey(KeyTable.XK_Num_Lock, 'NumLock', false); + this._remoteNumLock = null; + } this.sendKey(keysym, code, down); } @@ -1383,7 +1411,8 @@ export default class RFB extends EventTargetMixin { while (repeaterID.length < 250) { repeaterID += "\0"; } - this._sock.sendString(repeaterID); + this._sock.sQpushString(repeaterID); + this._sock.flush(); return true; } @@ -1393,7 +1422,8 @@ export default class RFB extends EventTargetMixin { const cversion = "00" + parseInt(this._rfbVersion, 10) + ".00" + ((this._rfbVersion * 10) % 10); - this._sock.sendString("RFB " + cversion + "\n"); + this._sock.sQpushString("RFB " + cversion + "\n"); + this._sock.flush(); Log.Debug('Sent ProtocolVersion: ' + cversion); this._rfbInitState = 'Security'; @@ -1445,7 +1475,8 @@ export default class RFB extends EventTargetMixin { return this._fail("Unsupported security types (types: " + types + ")"); } - this._sock.send([this._rfbAuthScheme]); + this._sock.sQpush8(this._rfbAuthScheme); + this._sock.flush(); } else { // Server decides if (this._sock.rQwait("security scheme", 4)) { return false; } @@ -1507,12 +1538,15 @@ export default class RFB extends EventTargetMixin { return false; } - const xvpAuthStr = String.fromCharCode(this._rfbCredentials.username.length) + - String.fromCharCode(this._rfbCredentials.target.length) + - this._rfbCredentials.username + - this._rfbCredentials.target; - this._sock.sendString(xvpAuthStr); + this._sock.sQpush8(this._rfbCredentials.username.length); + this._sock.sQpush8(this._rfbCredentials.target.length); + this._sock.sQpushString(this._rfbCredentials.username); + this._sock.sQpushString(this._rfbCredentials.target); + + this._sock.flush(); + this._rfbAuthScheme = securityTypeVNCAuth; + return this._negotiateAuthentication(); } @@ -1530,7 +1564,9 @@ export default class RFB extends EventTargetMixin { return this._fail("Unsupported VeNCrypt version " + major + "." + minor); } - this._sock.send([0, 2]); + this._sock.sQpush8(0); + this._sock.sQpush8(2); + this._sock.flush(); this._rfbVeNCryptState = 1; } @@ -1589,12 +1625,10 @@ export default class RFB extends EventTargetMixin { return this._fail("Unsupported security types (types: " + subtypes + ")"); } - this._sock.send([this._rfbAuthScheme >> 24, - this._rfbAuthScheme >> 16, - this._rfbAuthScheme >> 8, - this._rfbAuthScheme]); + this._sock.sQpush32(this._rfbAuthScheme); + this._sock.flush(); - this._rfbVeNCryptState == 4; + this._rfbVeNCryptState = 4; return true; } } @@ -1611,20 +1645,11 @@ export default class RFB extends EventTargetMixin { const user = encodeUTF8(this._rfbCredentials.username); const pass = encodeUTF8(this._rfbCredentials.password); - this._sock.send([ - (user.length >> 24) & 0xFF, - (user.length >> 16) & 0xFF, - (user.length >> 8) & 0xFF, - user.length & 0xFF - ]); - this._sock.send([ - (pass.length >> 24) & 0xFF, - (pass.length >> 16) & 0xFF, - (pass.length >> 8) & 0xFF, - pass.length & 0xFF - ]); - this._sock.sendString(user); - this._sock.sendString(pass); + this._sock.sQpush32(user.length); + this._sock.sQpush32(pass.length); + this._sock.sQpushString(user); + this._sock.sQpushString(pass); + this._sock.flush(); this._rfbInitState = "SecurityResult"; return true; @@ -1643,7 +1668,8 @@ export default class RFB extends EventTargetMixin { // TODO(directxman12): make genDES not require an Array const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16)); const response = RFB.genDES(this._rfbCredentials.password, challenge); - this._sock.send(response); + this._sock.sQpushBytes(response); + this._sock.flush(); this._rfbInitState = "SecurityResult"; return true; } @@ -1661,8 +1687,9 @@ export default class RFB extends EventTargetMixin { if (this._rfbCredentials.ardPublicKey != undefined && this._rfbCredentials.ardCredentials != undefined) { // if the async web crypto is done return the results - this._sock.send(this._rfbCredentials.ardCredentials); - this._sock.send(this._rfbCredentials.ardPublicKey); + this._sock.sQpushBytes(this._rfbCredentials.ardCredentials); + this._sock.sQpushBytes(this._rfbCredentials.ardPublicKey); + this._sock.flush(); this._rfbCredentials.ardCredentials = null; this._rfbCredentials.ardPublicKey = null; this._rfbInitState = "SecurityResult"; @@ -1681,77 +1708,35 @@ export default class RFB extends EventTargetMixin { let prime = this._sock.rQshiftBytes(keyLength); // predetermined prime modulus let serverPublicKey = this._sock.rQshiftBytes(keyLength); // other party's public key - let clientPrivateKey = window.crypto.getRandomValues(new Uint8Array(keyLength)); - let padding = Array.from(window.crypto.getRandomValues(new Uint8Array(64)), byte => String.fromCharCode(65+byte%26)).join(''); - - this._negotiateARDAuthAsync(generator, keyLength, prime, serverPublicKey, clientPrivateKey, padding); + let clientKey = legacyCrypto.generateKey( + { name: "DH", g: generator, p: prime }, false, ["deriveBits"]); + this._negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey); return false; } - _modPow(base, exponent, modulus) { + async _negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey) { + const clientPublicKey = legacyCrypto.exportKey("raw", clientKey.publicKey); + const sharedKey = legacyCrypto.deriveBits( + { name: "DH", public: serverPublicKey }, clientKey.privateKey, keyLength * 8); - let baseHex = "0x"+Array.from(base, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join(''); - let exponentHex = "0x"+Array.from(exponent, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join(''); - let modulusHex = "0x"+Array.from(modulus, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join(''); + const username = encodeUTF8(this._rfbCredentials.username).substring(0, 63); + const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63); - let b = BigInt(baseHex); - let e = BigInt(exponentHex); - let m = BigInt(modulusHex); - let r = 1n; - b = b % m; - while (e > 0) { - if (e % 2n === 1n) { - r = (r * b) % m; - } - e = e / 2n; - b = (b * b) % m; + const credentials = window.crypto.getRandomValues(new Uint8Array(128)); + for (let i = 0; i < username.length; i++) { + credentials[i] = username.charCodeAt(i); } - let hexResult = r.toString(16); - - while (hexResult.length/2 String.fromCharCode(byte)).join(''); - let aesKey = await window.crypto.subtle.importKey("raw", MD5(keyString), {name: "AES-CBC"}, false, ["encrypt"]); - let data = new Uint8Array(string.length); - for (let i = 0; i < string.length; ++i) { - data[i] = string.charCodeAt(i); - } - let encrypted = new Uint8Array(data.length); - for (let i=0;i { - this.dispatchEvent(new CustomEvent('securityresult')); + }) + .then(() => { this._rfbInitState = "SecurityResult"; return true; }).finally(() => { @@ -1934,15 +1923,15 @@ export default class RFB extends EventTargetMixin { const g = this._sock.rQshiftBytes(8); const p = this._sock.rQshiftBytes(8); const A = this._sock.rQshiftBytes(8); - const b = window.crypto.getRandomValues(new Uint8Array(8)); - const B = new Uint8Array(this._modPow(g, b, p)); - const secret = new Uint8Array(this._modPow(A, b, p)); + const dhKey = legacyCrypto.generateKey({ name: "DH", g: g, p: p }, true, ["deriveBits"]); + const B = legacyCrypto.exportKey("raw", dhKey.publicKey); + const secret = legacyCrypto.deriveBits({ name: "DH", public: A }, dhKey.privateKey, 64); - const des = new DES(secret); + const key = legacyCrypto.importKey("raw", secret, { name: "DES-CBC" }, false, ["encrypt"]); const username = encodeUTF8(this._rfbCredentials.username).substring(0, 255); const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63); - const usernameBytes = new Uint8Array(256); - const passwordBytes = new Uint8Array(64); + let usernameBytes = new Uint8Array(256); + let passwordBytes = new Uint8Array(64); window.crypto.getRandomValues(usernameBytes); window.crypto.getRandomValues(passwordBytes); for (let i = 0; i < username.length; i++) { @@ -1953,25 +1942,12 @@ export default class RFB extends EventTargetMixin { passwordBytes[i] = password.charCodeAt(i); } passwordBytes[password.length] = 0; - let x = new Uint8Array(secret); - for (let i = 0; i < 32; i++) { - for (let j = 0; j < 8; j++) { - x[j] ^= usernameBytes[i * 8 + j]; - } - x = des.enc8(x); - usernameBytes.set(x, i * 8); - } - x = new Uint8Array(secret); - for (let i = 0; i < 8; i++) { - for (let j = 0; j < 8; j++) { - x[j] ^= passwordBytes[i * 8 + j]; - } - x = des.enc8(x); - passwordBytes.set(x, i * 8); - } - this._sock.send(B); - this._sock.send(usernameBytes); - this._sock.send(passwordBytes); + usernameBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, usernameBytes); + passwordBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, passwordBytes); + this._sock.sQpushBytes(B); + this._sock.sQpushBytes(usernameBytes); + this._sock.sQpushBytes(passwordBytes); + this._sock.flush(); this._rfbInitState = "SecurityResult"; return true; } @@ -1979,7 +1955,11 @@ export default class RFB extends EventTargetMixin { _negotiateAuthentication() { switch (this._rfbAuthScheme) { case securityTypeNone: - this._rfbInitState = 'SecurityResult'; + if (this._rfbVersion >= 3.8) { + this._rfbInitState = 'SecurityResult'; + } else { + this._rfbInitState = 'ClientInitialisation'; + } return true; case securityTypeXVP: @@ -2016,13 +1996,6 @@ export default class RFB extends EventTargetMixin { } _handleSecurityResult() { - // There is no security choice, and hence no security result - // until RFB 3.7 - if (this._rfbVersion < 3.7) { - this._rfbInitState = 'ClientInitialisation'; - return true; - } - if (this._sock.rQwait('VNC auth response ', 4)) { return false; } const status = this._sock.rQshift32(); @@ -2158,6 +2131,7 @@ export default class RFB extends EventTargetMixin { encs.push(encodings.pseudoEncodingDesktopSize); encs.push(encodings.pseudoEncodingLastRect); encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent); + encs.push(encodings.pseudoEncodingQEMULedEvent); encs.push(encodings.pseudoEncodingExtendedDesktopSize); encs.push(encodings.pseudoEncodingXvp); encs.push(encodings.pseudoEncodingFence); @@ -2199,7 +2173,8 @@ export default class RFB extends EventTargetMixin { return this._handleSecurityReason(); case 'ClientInitialisation': - this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation + this._sock.sQpush8(this._shared ? 1 : 0); // ClientInitialisation + this._sock.flush(); this._rfbInitState = 'ServerInitialisation'; return true; @@ -2381,7 +2356,7 @@ export default class RFB extends EventTargetMixin { textData = textData.slice(0, -1); } - textData = textData.replace("\r\n", "\n"); + textData = textData.replaceAll("\r\n", "\n"); this.dispatchEvent(new CustomEvent( "clipboard", @@ -2512,19 +2487,11 @@ export default class RFB extends EventTargetMixin { default: this._fail("Unexpected server message (type " + msgType + ")"); - Log.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30)); + Log.Debug("sock.rQpeekBytes(30): " + this._sock.rQpeekBytes(30)); return true; } } - _onFlush() { - this._flushing = false; - // Resume processing - if (this._sock.rQlen > 0) { - this._handleMessage(); - } - } - _framebufferUpdate() { if (this._FBU.rects === 0) { if (this._sock.rQwait("FBU header", 3, 1)) { return false; } @@ -2535,7 +2502,14 @@ export default class RFB extends EventTargetMixin { // to avoid building up an excessive queue if (this._display.pending()) { this._flushing = true; - this._display.flush(); + this._display.flush() + .then(() => { + this._flushing = false; + // Resume processing + if (!this._sock.rQwait("message", 1)) { + this._handleMessage(); + } + }); return false; } } @@ -2545,13 +2519,13 @@ export default class RFB extends EventTargetMixin { if (this._sock.rQwait("rect header", 12)) { return false; } /* New FramebufferUpdate */ - const hdr = this._sock.rQshiftBytes(12); - this._FBU.x = (hdr[0] << 8) + hdr[1]; - this._FBU.y = (hdr[2] << 8) + hdr[3]; - this._FBU.width = (hdr[4] << 8) + hdr[5]; - this._FBU.height = (hdr[6] << 8) + hdr[7]; - this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) + - (hdr[10] << 8) + hdr[11], 10); + this._FBU.x = this._sock.rQshift16(); + this._FBU.y = this._sock.rQshift16(); + this._FBU.width = this._sock.rQshift16(); + this._FBU.height = this._sock.rQshift16(); + this._FBU.encoding = this._sock.rQshift32(); + /* Encodings are signed */ + this._FBU.encoding >>= 0; } if (!this._handleRect()) { @@ -2593,6 +2567,9 @@ export default class RFB extends EventTargetMixin { case encodings.pseudoEncodingExtendedDesktopSize: return this._handleExtendedDesktopSize(); + case encodings.pseudoEncodingQEMULedEvent: + return this._handleLedEvent(); + default: return this._handleDataRect(); } @@ -2770,6 +2747,21 @@ export default class RFB extends EventTargetMixin { return true; } + _handleLedEvent() { + if (this._sock.rQwait("LED Status", 1)) { + return false; + } + + let data = this._sock.rQshift8(); + // ScrollLock state can be retrieved with data & 1. This is currently not needed. + let numLock = data & 2 ? true : false; + let capsLock = data & 4 ? true : false; + this._remoteCapsLock = capsLock; + this._remoteNumLock = numLock; + + return true; + } + _handleExtendedDesktopSize() { if (this._sock.rQwait("ExtendedDesktopSize", 4)) { return false; @@ -2785,26 +2777,18 @@ export default class RFB extends EventTargetMixin { const firstUpdate = !this._supportsSetDesktopSize; this._supportsSetDesktopSize = true; - // Normally we only apply the current resize mode after a - // window resize event. However there is no such trigger on the - // initial connect. And we don't know if the server supports - // resizing until we've gotten here. - if (firstUpdate) { - this._requestRemoteResize(); - } - this._sock.rQskipBytes(1); // number-of-screens this._sock.rQskipBytes(3); // padding for (let i = 0; i < numberOfScreens; i += 1) { // Save the id and flags of the first screen if (i === 0) { - this._screenID = this._sock.rQshiftBytes(4); // id - this._sock.rQskipBytes(2); // x-position - this._sock.rQskipBytes(2); // y-position - this._sock.rQskipBytes(2); // width - this._sock.rQskipBytes(2); // height - this._screenFlags = this._sock.rQshiftBytes(4); // flags + this._screenID = this._sock.rQshift32(); // id + this._sock.rQskipBytes(2); // x-position + this._sock.rQskipBytes(2); // y-position + this._sock.rQskipBytes(2); // width + this._sock.rQskipBytes(2); // height + this._screenFlags = this._sock.rQshift32(); // flags } else { this._sock.rQskipBytes(16); } @@ -2842,6 +2826,14 @@ export default class RFB extends EventTargetMixin { this._resize(this._FBU.width, this._FBU.height); } + // Normally we only apply the current resize mode after a + // window resize event. However there is no such trigger on the + // initial connect. And we don't know if the server supports + // resizing until we've gotten here. + if (firstUpdate) { + this._requestRemoteResize(); + } + return true; } @@ -2937,28 +2929,22 @@ export default class RFB extends EventTargetMixin { static genDES(password, challenge) { const passwordChars = password.split('').map(c => c.charCodeAt(0)); - return (new DES(passwordChars)).encrypt(challenge); + const key = legacyCrypto.importKey( + "raw", passwordChars, { name: "DES-ECB" }, false, ["encrypt"]); + return legacyCrypto.encrypt({ name: "DES-ECB" }, key, challenge); } } // Class Methods RFB.messages = { keyEvent(sock, keysym, down) { - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(4); // msg-type + sock.sQpush8(down); - buff[offset] = 4; // msg-type - buff[offset + 1] = down; + sock.sQpush16(0); - buff[offset + 2] = 0; - buff[offset + 3] = 0; + sock.sQpush32(keysym); - buff[offset + 4] = (keysym >> 24); - buff[offset + 5] = (keysym >> 16); - buff[offset + 6] = (keysym >> 8); - buff[offset + 7] = keysym; - - sock._sQlen += 8; sock.flush(); }, @@ -2972,46 +2958,28 @@ RFB.messages = { return xtScanCode; } - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(255); // msg-type + sock.sQpush8(0); // sub msg-type - buff[offset] = 255; // msg-type - buff[offset + 1] = 0; // sub msg-type + sock.sQpush16(down); - buff[offset + 2] = (down >> 8); - buff[offset + 3] = down; - - buff[offset + 4] = (keysym >> 24); - buff[offset + 5] = (keysym >> 16); - buff[offset + 6] = (keysym >> 8); - buff[offset + 7] = keysym; + sock.sQpush32(keysym); const RFBkeycode = getRFBkeycode(keycode); - buff[offset + 8] = (RFBkeycode >> 24); - buff[offset + 9] = (RFBkeycode >> 16); - buff[offset + 10] = (RFBkeycode >> 8); - buff[offset + 11] = RFBkeycode; + sock.sQpush32(RFBkeycode); - sock._sQlen += 12; sock.flush(); }, pointerEvent(sock, x, y, mask) { - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(5); // msg-type - buff[offset] = 5; // msg-type + sock.sQpush8(mask); - buff[offset + 1] = mask; + sock.sQpush16(x); + sock.sQpush16(y); - buff[offset + 2] = x >> 8; - buff[offset + 3] = x; - - buff[offset + 4] = y >> 8; - buff[offset + 5] = y; - - sock._sQlen += 6; sock.flush(); }, @@ -3111,14 +3079,11 @@ RFB.messages = { }, clientCutText(sock, data, extended = false) { - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(6); // msg-type - buff[offset] = 6; // msg-type - - buff[offset + 1] = 0; // padding - buff[offset + 2] = 0; // padding - buff[offset + 3] = 0; // padding + sock.sQpush8(0); // padding + sock.sQpush8(0); // padding + sock.sQpush8(0); // padding let length; if (extended) { @@ -3127,121 +3092,63 @@ RFB.messages = { length = data.length; } - buff[offset + 4] = length >> 24; - buff[offset + 5] = length >> 16; - buff[offset + 6] = length >> 8; - buff[offset + 7] = length; - - sock._sQlen += 8; - - // We have to keep track of from where in the data we begin creating the - // buffer for the flush in the next iteration. - let dataOffset = 0; - - let remaining = data.length; - while (remaining > 0) { - - let flushSize = Math.min(remaining, (sock._sQbufferSize - sock._sQlen)); - for (let i = 0; i < flushSize; i++) { - buff[sock._sQlen + i] = data[dataOffset + i]; - } - - sock._sQlen += flushSize; - sock.flush(); - - remaining -= flushSize; - dataOffset += flushSize; - } - + sock.sQpush32(length); + sock.sQpushBytes(data); + sock.flush(); }, setDesktopSize(sock, width, height, id, flags) { - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(251); // msg-type - buff[offset] = 251; // msg-type - buff[offset + 1] = 0; // padding - buff[offset + 2] = width >> 8; // width - buff[offset + 3] = width; - buff[offset + 4] = height >> 8; // height - buff[offset + 5] = height; + sock.sQpush8(0); // padding - buff[offset + 6] = 1; // number-of-screens - buff[offset + 7] = 0; // padding + sock.sQpush16(width); + sock.sQpush16(height); + + sock.sQpush8(1); // number-of-screens + + sock.sQpush8(0); // padding // screen array - buff[offset + 8] = id >> 24; // id - buff[offset + 9] = id >> 16; - buff[offset + 10] = id >> 8; - buff[offset + 11] = id; - buff[offset + 12] = 0; // x-position - buff[offset + 13] = 0; - buff[offset + 14] = 0; // y-position - buff[offset + 15] = 0; - buff[offset + 16] = width >> 8; // width - buff[offset + 17] = width; - buff[offset + 18] = height >> 8; // height - buff[offset + 19] = height; - buff[offset + 20] = flags >> 24; // flags - buff[offset + 21] = flags >> 16; - buff[offset + 22] = flags >> 8; - buff[offset + 23] = flags; + sock.sQpush32(id); + sock.sQpush16(0); // x-position + sock.sQpush16(0); // y-position + sock.sQpush16(width); + sock.sQpush16(height); + sock.sQpush32(flags); - sock._sQlen += 24; sock.flush(); }, clientFence(sock, flags, payload) { - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(248); // msg-type - buff[offset] = 248; // msg-type + sock.sQpush8(0); // padding + sock.sQpush8(0); // padding + sock.sQpush8(0); // padding - buff[offset + 1] = 0; // padding - buff[offset + 2] = 0; // padding - buff[offset + 3] = 0; // padding + sock.sQpush32(flags); - buff[offset + 4] = flags >> 24; // flags - buff[offset + 5] = flags >> 16; - buff[offset + 6] = flags >> 8; - buff[offset + 7] = flags; + sock.sQpush8(payload.length); + sock.sQpushString(payload); - const n = payload.length; - - buff[offset + 8] = n; // length - - for (let i = 0; i < n; i++) { - buff[offset + 9 + i] = payload.charCodeAt(i); - } - - sock._sQlen += 9 + n; sock.flush(); }, enableContinuousUpdates(sock, enable, x, y, width, height) { - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(150); // msg-type - buff[offset] = 150; // msg-type - buff[offset + 1] = enable; // enable-flag + sock.sQpush8(enable); - buff[offset + 2] = x >> 8; // x - buff[offset + 3] = x; - buff[offset + 4] = y >> 8; // y - buff[offset + 5] = y; - buff[offset + 6] = width >> 8; // width - buff[offset + 7] = width; - buff[offset + 8] = height >> 8; // height - buff[offset + 9] = height; + sock.sQpush16(x); + sock.sQpush16(y); + sock.sQpush16(width); + sock.sQpush16(height); - sock._sQlen += 10; sock.flush(); }, pixelFormat(sock, depth, trueColor) { - const buff = sock._sQ; - const offset = sock._sQlen; - let bpp; if (depth > 16) { @@ -3254,100 +3161,69 @@ RFB.messages = { const bits = Math.floor(depth/3); - buff[offset] = 0; // msg-type + sock.sQpush8(0); // msg-type - buff[offset + 1] = 0; // padding - buff[offset + 2] = 0; // padding - buff[offset + 3] = 0; // padding + sock.sQpush8(0); // padding + sock.sQpush8(0); // padding + sock.sQpush8(0); // padding - buff[offset + 4] = bpp; // bits-per-pixel - buff[offset + 5] = depth; // depth - buff[offset + 6] = 0; // little-endian - buff[offset + 7] = trueColor ? 1 : 0; // true-color + sock.sQpush8(bpp); + sock.sQpush8(depth); + sock.sQpush8(0); // little-endian + sock.sQpush8(trueColor ? 1 : 0); - buff[offset + 8] = 0; // red-max - buff[offset + 9] = (1 << bits) - 1; // red-max + sock.sQpush16((1 << bits) - 1); // red-max + sock.sQpush16((1 << bits) - 1); // green-max + sock.sQpush16((1 << bits) - 1); // blue-max - buff[offset + 10] = 0; // green-max - buff[offset + 11] = (1 << bits) - 1; // green-max + sock.sQpush8(bits * 0); // red-shift + sock.sQpush8(bits * 1); // green-shift + sock.sQpush8(bits * 2); // blue-shift - buff[offset + 12] = 0; // blue-max - buff[offset + 13] = (1 << bits) - 1; // blue-max + sock.sQpush8(0); // padding + sock.sQpush8(0); // padding + sock.sQpush8(0); // padding - buff[offset + 14] = bits * 0; // red-shift - buff[offset + 15] = bits * 1; // green-shift - buff[offset + 16] = bits * 2; // blue-shift - - buff[offset + 17] = 0; // padding - buff[offset + 18] = 0; // padding - buff[offset + 19] = 0; // padding - - sock._sQlen += 20; sock.flush(); }, clientEncodings(sock, encodings) { - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(2); // msg-type - buff[offset] = 2; // msg-type - buff[offset + 1] = 0; // padding + sock.sQpush8(0); // padding - buff[offset + 2] = encodings.length >> 8; - buff[offset + 3] = encodings.length; - - let j = offset + 4; + sock.sQpush16(encodings.length); for (let i = 0; i < encodings.length; i++) { - const enc = encodings[i]; - buff[j] = enc >> 24; - buff[j + 1] = enc >> 16; - buff[j + 2] = enc >> 8; - buff[j + 3] = enc; - - j += 4; + sock.sQpush32(encodings[i]); } - sock._sQlen += j - offset; sock.flush(); }, fbUpdateRequest(sock, incremental, x, y, w, h) { - const buff = sock._sQ; - const offset = sock._sQlen; - if (typeof(x) === "undefined") { x = 0; } if (typeof(y) === "undefined") { y = 0; } - buff[offset] = 3; // msg-type - buff[offset + 1] = incremental ? 1 : 0; + sock.sQpush8(3); // msg-type - buff[offset + 2] = (x >> 8) & 0xFF; - buff[offset + 3] = x & 0xFF; + sock.sQpush8(incremental ? 1 : 0); - buff[offset + 4] = (y >> 8) & 0xFF; - buff[offset + 5] = y & 0xFF; + sock.sQpush16(x); + sock.sQpush16(y); + sock.sQpush16(w); + sock.sQpush16(h); - buff[offset + 6] = (w >> 8) & 0xFF; - buff[offset + 7] = w & 0xFF; - - buff[offset + 8] = (h >> 8) & 0xFF; - buff[offset + 9] = h & 0xFF; - - sock._sQlen += 10; sock.flush(); }, xvpOp(sock, ver, op) { - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(250); // msg-type - buff[offset] = 250; // msg-type - buff[offset + 1] = 0; // padding + sock.sQpush8(0); // padding - buff[offset + 2] = ver; - buff[offset + 3] = op; + sock.sQpush8(ver); + sock.sQpush8(op); - sock._sQlen += 4; sock.flush(); } }; diff --git a/public/novnc/core/util/cursor.js b/public/novnc/core/util/cursor.js index 3000cf0e..20e75f1b 100644 --- a/public/novnc/core/util/cursor.js +++ b/public/novnc/core/util/cursor.js @@ -69,7 +69,9 @@ export default class Cursor { this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options); this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options); - document.body.removeChild(this._canvas); + if (document.contains(this._canvas)) { + document.body.removeChild(this._canvas); + } } this._target = null; diff --git a/public/novnc/core/websock.js b/public/novnc/core/websock.js index 37b33fcc..61a3091a 100644 --- a/public/novnc/core/websock.js +++ b/public/novnc/core/websock.js @@ -94,27 +94,7 @@ export default class Websock { return "unknown"; } - get sQ() { - return this._sQ; - } - - get rQ() { - return this._rQ; - } - - get rQi() { - return this._rQi; - } - - set rQi(val) { - this._rQi = val; - } - // Receive Queue - get rQlen() { - return this._rQlen - this._rQi; - } - rQpeek8() { return this._rQ[this._rQi]; } @@ -141,42 +121,47 @@ export default class Websock { for (let byte = bytes - 1; byte >= 0; byte--) { res += this._rQ[this._rQi++] << (byte * 8); } - return res; + return res >>> 0; } rQshiftStr(len) { - if (typeof(len) === 'undefined') { len = this.rQlen; } let str = ""; // Handle large arrays in steps to avoid long strings on the stack for (let i = 0; i < len; i += 4096) { - let part = this.rQshiftBytes(Math.min(4096, len - i)); + let part = this.rQshiftBytes(Math.min(4096, len - i), false); str += String.fromCharCode.apply(null, part); } return str; } - rQshiftBytes(len) { - if (typeof(len) === 'undefined') { len = this.rQlen; } + rQshiftBytes(len, copy=true) { this._rQi += len; - return new Uint8Array(this._rQ.buffer, this._rQi - len, len); + if (copy) { + return this._rQ.slice(this._rQi - len, this._rQi); + } else { + return this._rQ.subarray(this._rQi - len, this._rQi); + } } rQshiftTo(target, len) { - if (len === undefined) { len = this.rQlen; } // TODO: make this just use set with views when using a ArrayBuffer to store the rQ target.set(new Uint8Array(this._rQ.buffer, this._rQi, len)); this._rQi += len; } - rQslice(start, end = this.rQlen) { - return new Uint8Array(this._rQ.buffer, this._rQi + start, end - start); + rQpeekBytes(len, copy=true) { + if (copy) { + return this._rQ.slice(this._rQi, this._rQi + len); + } else { + return this._rQ.subarray(this._rQi, this._rQi + len); + } } // Check to see if we must wait for 'num' bytes (default to FBU.bytes) // to be available in the receive queue. Return true if we need to // wait (and possibly print a debug message), otherwise false. rQwait(msg, num, goback) { - if (this.rQlen < num) { + if (this._rQlen - this._rQi < num) { if (goback) { if (this._rQi < goback) { throw new Error("rQwait cannot backup " + goback + " bytes"); @@ -190,21 +175,56 @@ export default class Websock { // Send Queue + sQpush8(num) { + this._sQensureSpace(1); + this._sQ[this._sQlen++] = num; + } + + sQpush16(num) { + this._sQensureSpace(2); + this._sQ[this._sQlen++] = (num >> 8) & 0xff; + this._sQ[this._sQlen++] = (num >> 0) & 0xff; + } + + sQpush32(num) { + this._sQensureSpace(4); + this._sQ[this._sQlen++] = (num >> 24) & 0xff; + this._sQ[this._sQlen++] = (num >> 16) & 0xff; + this._sQ[this._sQlen++] = (num >> 8) & 0xff; + this._sQ[this._sQlen++] = (num >> 0) & 0xff; + } + + sQpushString(str) { + let bytes = str.split('').map(chr => chr.charCodeAt(0)); + this.sQpushBytes(new Uint8Array(bytes)); + } + + sQpushBytes(bytes) { + for (let offset = 0;offset < bytes.length;) { + this._sQensureSpace(1); + + let chunkSize = this._sQbufferSize - this._sQlen; + if (chunkSize > bytes.length - offset) { + chunkSize = bytes.length - offset; + } + + this._sQ.set(bytes.subarray(offset, offset + chunkSize), this._sQlen); + this._sQlen += chunkSize; + offset += chunkSize; + } + } + flush() { if (this._sQlen > 0 && this.readyState === 'open') { - this._websocket.send(this._encodeMessage()); + this._websocket.send(new Uint8Array(this._sQ.buffer, 0, this._sQlen)); this._sQlen = 0; } } - send(arr) { - this._sQ.set(arr, this._sQlen); - this._sQlen += arr.length; - this.flush(); - } - - sendString(str) { - this.send(str.split('').map(chr => chr.charCodeAt(0))); + _sQensureSpace(bytes) { + if (this._sQbufferSize - this._sQlen < bytes) { + this.flush(); + } } // Event Handlers @@ -283,17 +303,12 @@ export default class Websock { } // private methods - _encodeMessage() { - // Put in a binary arraybuffer - // according to the spec, you can send ArrayBufferViews with the send method - return new Uint8Array(this._sQ.buffer, 0, this._sQlen); - } // We want to move all the unread data to the start of the queue, // e.g. compacting. // The function also expands the receive que if needed, and for // performance reasons we combine these two actions to avoid - // unneccessary copying. + // unnecessary copying. _expandCompactRQ(minFit) { // if we're using less than 1/8th of the buffer even with the incoming bytes, compact in place // instead of resizing @@ -309,7 +324,7 @@ export default class Websock { // we don't want to grow unboundedly if (this._rQbufferSize > MAX_RQ_GROW_SIZE) { this._rQbufferSize = MAX_RQ_GROW_SIZE; - if (this._rQbufferSize - this.rQlen < minFit) { + if (this._rQbufferSize - (this._rQlen - this._rQi) < minFit) { throw new Error("Receive Queue buffer exceeded " + MAX_RQ_GROW_SIZE + " bytes, and the new message could not fit"); } } @@ -327,25 +342,22 @@ export default class Websock { } // push arraybuffer values onto the end of the receive que - _DecodeMessage(data) { - const u8 = new Uint8Array(data); + _recvMessage(e) { + if (this._rQlen == this._rQi) { + // All data has now been processed, this means we + // can reset the receive queue. + this._rQlen = 0; + this._rQi = 0; + } + const u8 = new Uint8Array(e.data); if (u8.length > this._rQbufferSize - this._rQlen) { this._expandCompactRQ(u8.length); } this._rQ.set(u8, this._rQlen); this._rQlen += u8.length; - } - _recvMessage(e) { - this._DecodeMessage(e.data); - if (this.rQlen > 0) { + if (this._rQlen - this._rQi > 0) { this._eventHandlers.message(); - if (this._rQlen == this._rQi) { - // All data has now been processed, this means we - // can reset the receive queue. - this._rQlen = 0; - this._rQi = 0; - } } else { Log.Debug("Ignoring empty message"); } diff --git a/public/novnc/package.json b/public/novnc/package.json new file mode 100644 index 00000000..c032582d --- /dev/null +++ b/public/novnc/package.json @@ -0,0 +1 @@ +{ "version": "1.5.0" } \ No newline at end of file