Change the authentication workflow. This will allow Sshwifty to run on a multi-node autobalanced cluster. After deploy this version, users might have to reload the frontend page before continue using Sshwifty.
This commit is contained in:
129
ui/app.js
129
ui/app.js
@@ -32,6 +32,7 @@ import Home from "./home.vue";
|
||||
import "./landing.css";
|
||||
import Loading from "./loading.vue";
|
||||
import { Socket } from "./socket.js";
|
||||
import * as stream from "./stream/common";
|
||||
import * as xhr from "./xhr.js";
|
||||
|
||||
const backendQueryRetryDelay = 2000;
|
||||
@@ -72,6 +73,19 @@ function startApp(rootEl) {
|
||||
|
||||
let uiControlColor = new ControlColor();
|
||||
|
||||
function getCurrentKeyMixer() {
|
||||
return Number(Math.trunc(new Date().getTime() / 100000)).toString();
|
||||
}
|
||||
|
||||
async function buildSocketKey(privateKey) {
|
||||
return new Uint8Array(
|
||||
await cipher.hmac512(
|
||||
stream.buildBufferFromString(privateKey),
|
||||
stream.buildBufferFromString(getCurrentKeyMixer())
|
||||
)
|
||||
).slice(0, 16);
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: rootEl,
|
||||
components: {
|
||||
@@ -170,11 +184,20 @@ function startApp(rootEl) {
|
||||
isErrored() {
|
||||
return this.authErr.length > 0 || this.loadErr.length > 0;
|
||||
},
|
||||
async getSocketAuthKey(privateKey, randomKey) {
|
||||
const enc = new TextEncoder();
|
||||
async getSocketAuthKey(privateKey) {
|
||||
const enc = new TextEncoder(),
|
||||
rTime = Number(Math.trunc(new Date().getTime() / 100000));
|
||||
|
||||
var finalKey = "";
|
||||
|
||||
if (privateKey.length <= 0) {
|
||||
finalKey = "DEFAULT VERIFY KEY";
|
||||
} else {
|
||||
finalKey = privateKey;
|
||||
}
|
||||
|
||||
return new Uint8Array(
|
||||
await cipher.hmac512(enc.encode(privateKey), enc.encode(randomKey))
|
||||
await cipher.hmac512(enc.encode(finalKey), enc.encode(rTime))
|
||||
).slice(0, 32);
|
||||
},
|
||||
buildBackendSocketURL() {
|
||||
@@ -213,6 +236,40 @@ function startApp(rootEl) {
|
||||
);
|
||||
this.page = "app";
|
||||
},
|
||||
async doAuth(privateKey) {
|
||||
let result = await this.requestAuth(privateKey);
|
||||
|
||||
if (result.key) {
|
||||
this.key = result.key;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
async requestAuth(privateKey) {
|
||||
let authKey =
|
||||
!privateKey || !this.key
|
||||
? null
|
||||
: await this.getSocketAuthKey(privateKey);
|
||||
|
||||
let h = await xhr.get(socksVerificationInterface, {
|
||||
"X-Key": authKey
|
||||
? btoa(String.fromCharCode.apply(null, authKey))
|
||||
: "",
|
||||
});
|
||||
|
||||
let serverDate = h.getResponseHeader("Date");
|
||||
|
||||
return {
|
||||
result: h.status,
|
||||
key: h.getResponseHeader("X-Key"),
|
||||
timeout: h.getResponseHeader("X-Timeout"),
|
||||
heartbeat: h.getResponseHeader("X-Heartbeat"),
|
||||
date: serverDate ? new Date(serverDate) : null,
|
||||
data: h.responseText,
|
||||
onlyAllowPresetRemotes:
|
||||
h.getResponseHeader("X-OnlyAllowPresetRemotes") === "yes",
|
||||
};
|
||||
},
|
||||
async tryInitialAuth() {
|
||||
try {
|
||||
let result = await this.doAuth("");
|
||||
@@ -235,17 +292,14 @@ function startApp(rootEl) {
|
||||
}
|
||||
|
||||
let self = this;
|
||||
|
||||
switch (result.result) {
|
||||
case 200:
|
||||
this.executeHomeApp(result, {
|
||||
data: result.key,
|
||||
data: await buildSocketKey(atob(result.key) + "+"),
|
||||
async fetch() {
|
||||
if (this.data) {
|
||||
let dKey = this.data;
|
||||
|
||||
this.data = null;
|
||||
|
||||
return dKey;
|
||||
}
|
||||
|
||||
@@ -259,7 +313,7 @@ function startApp(rootEl) {
|
||||
);
|
||||
}
|
||||
|
||||
return result.key;
|
||||
return await buildSocketKey(atob(result.key) + "+");
|
||||
},
|
||||
});
|
||||
break;
|
||||
@@ -281,52 +335,37 @@ function startApp(rootEl) {
|
||||
this.loadErr = "Unable to initialize client application: " + e;
|
||||
}
|
||||
},
|
||||
async doAuth(privateKey) {
|
||||
let result = await this.requestAuth(privateKey);
|
||||
|
||||
if (result.key) {
|
||||
this.key = result.key;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
async requestAuth(privateKey) {
|
||||
let authKey =
|
||||
!privateKey || !this.key
|
||||
? null
|
||||
: await this.getSocketAuthKey(privateKey, this.key);
|
||||
|
||||
let h = await xhr.get(socksVerificationInterface, {
|
||||
"X-Key": authKey
|
||||
? btoa(String.fromCharCode.apply(null, authKey))
|
||||
: "",
|
||||
});
|
||||
|
||||
let serverDate = h.getResponseHeader("Date");
|
||||
|
||||
return {
|
||||
result: h.status,
|
||||
key: h.getResponseHeader("X-Key"),
|
||||
timeout: h.getResponseHeader("X-Timeout"),
|
||||
heartbeat: h.getResponseHeader("X-Heartbeat"),
|
||||
date: serverDate ? new Date(serverDate) : null,
|
||||
data: h.responseText,
|
||||
onlyAllowPresetRemotes:
|
||||
h.getResponseHeader("X-OnlyAllowPresetRemotes") === "yes",
|
||||
};
|
||||
},
|
||||
async submitAuth(passphrase) {
|
||||
this.authErr = "";
|
||||
|
||||
try {
|
||||
let result = await this.doAuth(passphrase);
|
||||
|
||||
let self = this;
|
||||
switch (result.result) {
|
||||
case 200:
|
||||
this.executeHomeApp(result, {
|
||||
data: passphrase,
|
||||
fetch() {
|
||||
return this.data;
|
||||
data: await buildSocketKey(atob(result.key) + "+" + passphrase),
|
||||
async fetch() {
|
||||
if (this.data) {
|
||||
let dKey = this.data;
|
||||
this.data = null;
|
||||
return dKey;
|
||||
}
|
||||
|
||||
let result = await self.doAuth(passphrase);
|
||||
|
||||
if (result.result !== 200) {
|
||||
throw new Error(
|
||||
"Unable to fetch key from remote, unexpected " +
|
||||
"error code: " +
|
||||
result.result
|
||||
);
|
||||
}
|
||||
|
||||
return await buildSocketKey(
|
||||
atob(result.key) + "+" + passphrase
|
||||
);
|
||||
},
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -27,7 +27,7 @@ export async function hmac512(secret, data) {
|
||||
secret,
|
||||
{
|
||||
name: "HMAC",
|
||||
hash: { name: "SHA-512" }
|
||||
hash: { name: "SHA-512" },
|
||||
},
|
||||
false,
|
||||
["sign", "verify"]
|
||||
@@ -50,7 +50,7 @@ export function buildGCMKey(keyData) {
|
||||
keyData,
|
||||
{
|
||||
name: "AES-GCM",
|
||||
length: GCMKeyBitLen
|
||||
length: GCMKeyBitLen,
|
||||
},
|
||||
false,
|
||||
["encrypt", "decrypt"]
|
||||
|
||||
225
ui/socket.js
225
ui/socket.js
@@ -15,10 +15,10 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import * as streams from "./stream/streams.js";
|
||||
import * as crypt from "./crypto.js";
|
||||
import * as reader from "./stream/reader.js";
|
||||
import * as sender from "./stream/sender.js";
|
||||
import * as crypt from "./crypto.js";
|
||||
import * as streams from "./stream/streams.js";
|
||||
|
||||
export const ECHO_FAILED = streams.ECHO_FAILED;
|
||||
|
||||
@@ -56,7 +56,7 @@ class Dial {
|
||||
timeoutTimer = setTimeout(() => {
|
||||
ws.close();
|
||||
}, timeout),
|
||||
myRes = w => {
|
||||
myRes = (w) => {
|
||||
if (promised) {
|
||||
return;
|
||||
}
|
||||
@@ -66,7 +66,7 @@ class Dial {
|
||||
|
||||
return resolve(w);
|
||||
},
|
||||
myRej = e => {
|
||||
myRej = (e) => {
|
||||
if (promised) {
|
||||
return;
|
||||
}
|
||||
@@ -77,11 +77,11 @@ class Dial {
|
||||
return reject(e);
|
||||
};
|
||||
|
||||
ws.addEventListener("open", _event => {
|
||||
ws.addEventListener("open", (_event) => {
|
||||
myRes(ws);
|
||||
});
|
||||
|
||||
ws.addEventListener("close", event => {
|
||||
ws.addEventListener("close", (event) => {
|
||||
event.toString = () => {
|
||||
return "WebSocket Error (" + event.code + ")";
|
||||
};
|
||||
@@ -89,7 +89,7 @@ class Dial {
|
||||
myRej(event);
|
||||
});
|
||||
|
||||
ws.addEventListener("error", _event => {
|
||||
ws.addEventListener("error", (_event) => {
|
||||
ws.close();
|
||||
});
|
||||
});
|
||||
@@ -100,15 +100,7 @@ class Dial {
|
||||
*
|
||||
*/
|
||||
async buildKeyString() {
|
||||
const enc = new TextEncoder();
|
||||
|
||||
let rTime = Number(Math.trunc(new Date().getTime() / 100000)),
|
||||
key = await crypt.hmac512(
|
||||
enc.encode(await this.privateKey.fetch()),
|
||||
enc.encode(rTime)
|
||||
);
|
||||
|
||||
return key.slice(0, 16);
|
||||
return this.privateKey.fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,12 +123,14 @@ class Dial {
|
||||
*
|
||||
*/
|
||||
async dial(callbacks) {
|
||||
let ws = await this.connect(this.timeout),
|
||||
rd = new reader.Reader(new reader.Multiple(() => {}), data => {
|
||||
return new Promise(resolve => {
|
||||
let ws = await this.connect(this.timeout);
|
||||
|
||||
try {
|
||||
let rd = new reader.Reader(new reader.Multiple(() => {}), (data) => {
|
||||
return new Promise((resolve) => {
|
||||
let bufferReader = new FileReader();
|
||||
|
||||
bufferReader.onload = event => {
|
||||
bufferReader.onload = (event) => {
|
||||
let d = new Uint8Array(event.target.result);
|
||||
|
||||
resolve(d);
|
||||
@@ -148,105 +142,112 @@ class Dial {
|
||||
});
|
||||
});
|
||||
|
||||
ws.addEventListener("message", event => {
|
||||
callbacks.inbound(event.data);
|
||||
ws.addEventListener("message", (event) => {
|
||||
callbacks.inbound(event.data);
|
||||
|
||||
rd.feed(event.data);
|
||||
});
|
||||
rd.feed(event.data);
|
||||
});
|
||||
|
||||
ws.addEventListener("error", event => {
|
||||
event.toString = () => {
|
||||
return (
|
||||
"WebSocket Error (" + (event.code ? event.code : "Unknown") + ")"
|
||||
ws.addEventListener("error", (event) => {
|
||||
event.toString = () => {
|
||||
return (
|
||||
"WebSocket Error (" + (event.code ? event.code : "Unknown") + ")"
|
||||
);
|
||||
};
|
||||
|
||||
rd.closeWithReason(event);
|
||||
});
|
||||
|
||||
ws.addEventListener("close", (_event) => {
|
||||
rd.closeWithReason("Connection is closed");
|
||||
});
|
||||
|
||||
let sdDataConvert = (rawData) => {
|
||||
return rawData;
|
||||
},
|
||||
getSdDataConvert = () => {
|
||||
return sdDataConvert;
|
||||
},
|
||||
sd = new sender.Sender(
|
||||
async (rawData) => {
|
||||
try {
|
||||
let data = await getSdDataConvert()(rawData);
|
||||
|
||||
ws.send(data.buffer);
|
||||
callbacks.outbound(data);
|
||||
} catch (e) {
|
||||
ws.close();
|
||||
rd.closeWithReason(e);
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
4096 - 64, // Server has a 4096 bytes receive buffer, can be no greater,
|
||||
minSenderDelay, // 30ms input delay
|
||||
10 // max 10 buffered requests
|
||||
);
|
||||
|
||||
let senderNonce = crypt.generateNonce();
|
||||
sd.send(senderNonce);
|
||||
|
||||
let receiverNonce = await reader.readN(rd, crypt.GCMNonceSize);
|
||||
|
||||
let key = await this.buildKey();
|
||||
|
||||
sdDataConvert = async (rawData) => {
|
||||
let encoded = await crypt.encryptGCM(key, senderNonce, rawData);
|
||||
|
||||
crypt.increaseNonce(senderNonce);
|
||||
|
||||
let dataToSend = new Uint8Array(encoded.byteLength + 2);
|
||||
|
||||
dataToSend[0] = (encoded.byteLength >> 8) & 0xff;
|
||||
dataToSend[1] = encoded.byteLength & 0xff;
|
||||
|
||||
dataToSend.set(new Uint8Array(encoded), 2);
|
||||
|
||||
return dataToSend;
|
||||
};
|
||||
|
||||
rd.closeWithReason(event);
|
||||
});
|
||||
let cgmReader = new reader.Multiple(async (r) => {
|
||||
try {
|
||||
let dSizeBytes = await reader.readN(rd, 2),
|
||||
dSize = 0;
|
||||
|
||||
ws.addEventListener("close", _event => {
|
||||
rd.closeWithReason("Connection is closed");
|
||||
});
|
||||
dSize = dSizeBytes[0];
|
||||
dSize <<= 8;
|
||||
dSize |= dSizeBytes[1];
|
||||
|
||||
let sdDataConvert = rawData => {
|
||||
return rawData;
|
||||
},
|
||||
getSdDataConvert = () => {
|
||||
return sdDataConvert;
|
||||
},
|
||||
sd = new sender.Sender(
|
||||
async rawData => {
|
||||
try {
|
||||
let data = await getSdDataConvert()(rawData);
|
||||
let decoded = await crypt.decryptGCM(
|
||||
key,
|
||||
receiverNonce,
|
||||
await reader.readN(rd, dSize)
|
||||
);
|
||||
|
||||
ws.send(data.buffer);
|
||||
callbacks.outbound(data);
|
||||
} catch (e) {
|
||||
ws.close();
|
||||
rd.closeWithReason(e);
|
||||
crypt.increaseNonce(receiverNonce);
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.error(e);
|
||||
}
|
||||
r.feed(
|
||||
new reader.Buffer(new Uint8Array(decoded), () => {}),
|
||||
() => {}
|
||||
);
|
||||
} catch (e) {
|
||||
r.closeWithReason(e);
|
||||
}
|
||||
});
|
||||
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
4096 - 64, // Server has a 4096 bytes receive buffer, can be no greater,
|
||||
minSenderDelay, // 30ms input delay
|
||||
10 // max 10 buffered requests
|
||||
);
|
||||
|
||||
let senderNonce = crypt.generateNonce();
|
||||
sd.send(senderNonce);
|
||||
|
||||
let receiverNonce = await reader.readN(rd, crypt.GCMNonceSize);
|
||||
|
||||
let key = await this.buildKey();
|
||||
|
||||
sdDataConvert = async rawData => {
|
||||
let encoded = await crypt.encryptGCM(key, senderNonce, rawData);
|
||||
|
||||
crypt.increaseNonce(senderNonce);
|
||||
|
||||
let dataToSend = new Uint8Array(encoded.byteLength + 2);
|
||||
|
||||
dataToSend[0] = (encoded.byteLength >> 8) & 0xff;
|
||||
dataToSend[1] = encoded.byteLength & 0xff;
|
||||
|
||||
dataToSend.set(new Uint8Array(encoded), 2);
|
||||
|
||||
return dataToSend;
|
||||
};
|
||||
|
||||
let cgmReader = new reader.Multiple(async r => {
|
||||
try {
|
||||
let dSizeBytes = await reader.readN(rd, 2),
|
||||
dSize = 0;
|
||||
|
||||
dSize = dSizeBytes[0];
|
||||
dSize <<= 8;
|
||||
dSize |= dSizeBytes[1];
|
||||
|
||||
let decoded = await crypt.decryptGCM(
|
||||
key,
|
||||
receiverNonce,
|
||||
await reader.readN(rd, dSize)
|
||||
);
|
||||
|
||||
crypt.increaseNonce(receiverNonce);
|
||||
|
||||
r.feed(new reader.Buffer(new Uint8Array(decoded), () => {}), () => {});
|
||||
} catch (e) {
|
||||
r.closeWithReason(e);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
reader: cgmReader,
|
||||
sender: sd,
|
||||
ws: ws
|
||||
};
|
||||
return {
|
||||
reader: cgmReader,
|
||||
sender: sd,
|
||||
ws: ws,
|
||||
};
|
||||
} catch (e) {
|
||||
ws.close();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,7 +329,7 @@ export class Socket {
|
||||
},
|
||||
outbound(data) {
|
||||
callbacks.traffic(0, data.length);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
let streamHandler = new streams.Streams(conn.reader, conn.sender, {
|
||||
@@ -357,12 +358,12 @@ export class Socket {
|
||||
// risk sending things out
|
||||
conn.ws.close();
|
||||
callbacks.close(e);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
callbacks.connected();
|
||||
|
||||
streamHandler.serve().catch(e => {
|
||||
streamHandler.serve().catch((e) => {
|
||||
if (process.env.NODE_ENV !== "development") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -75,3 +75,35 @@ export function separateBuffer(buf, max) {
|
||||
start += remain;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Uint8Array out of given binary string
|
||||
*
|
||||
* @param {string} str binary string
|
||||
*
|
||||
* @returns {Uint8Array} Separated buffers
|
||||
*
|
||||
*/
|
||||
export function buildBufferFromString(str) {
|
||||
let r = [],
|
||||
t = [];
|
||||
|
||||
for (let i in str) {
|
||||
let c = str.charCodeAt(i);
|
||||
|
||||
while (c > 0xff) {
|
||||
t.push(c & 0xff);
|
||||
c >>= 8;
|
||||
}
|
||||
|
||||
r.push(c);
|
||||
|
||||
for (let j = t.length; j > 0; j--) {
|
||||
r.push(t[j]);
|
||||
}
|
||||
|
||||
t = [];
|
||||
}
|
||||
|
||||
return new Uint8Array(r);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user