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:
NI
2021-03-02 20:54:58 +08:00
parent 6001d6dd5e
commit c6683b1311
6 changed files with 264 additions and 198 deletions

129
ui/app.js
View File

@@ -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;

View File

@@ -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"]

View File

@@ -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;
}

View File

@@ -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);
}