From c6683b13116fe37d915a71825e467d46ca04fa96 Mon Sep 17 00:00:00 2001 From: NI Date: Tue, 2 Mar 2021 20:54:58 +0800 Subject: [PATCH] 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. --- application/controller/socket.go | 48 ++--- application/controller/socket_verify.go | 24 ++- ui/app.js | 129 +++++++++----- ui/crypto.js | 4 +- ui/socket.js | 225 ++++++++++++------------ ui/stream/common.js | 32 ++++ 6 files changed, 264 insertions(+), 198 deletions(-) diff --git a/application/controller/socket.go b/application/controller/socket.go index 61733aa..1aa616a 100644 --- a/application/controller/socket.go +++ b/application/controller/socket.go @@ -23,7 +23,6 @@ import ( "crypto/hmac" "crypto/rand" "crypto/sha512" - "encoding/base64" "fmt" "io" "net/http" @@ -66,32 +65,14 @@ type socket struct { commonCfg configuration.Common serverCfg configuration.Server - randomKey string - authKey []byte upgrader websocket.Upgrader commander command.Commander } -func getNewSocketCtlRandomSharedKey() string { - b := [32]byte{} +func hashCombineSocketKeys(addedKey string, privateKey string) []byte { + h := hmac.New(sha512.New, []byte(privateKey)) - io.ReadFull(rand.Reader, b[:]) - - return base64.StdEncoding.EncodeToString(b[:]) -} - -func getSocketAuthKey(randomKey string, sharedKey string) []byte { - var k []byte - - if len(sharedKey) > 0 { - k = []byte(sharedKey) - } else { - k = []byte(randomKey) - } - - h := hmac.New(sha512.New, k) - - h.Write([]byte(randomKey)) + h.Write([]byte(addedKey)) return h.Sum(nil) } @@ -101,13 +82,9 @@ func newSocketCtl( cfg configuration.Server, cmds command.Commands, ) socket { - randomKey := getNewSocketCtlRandomSharedKey() - return socket{ commonCfg: commonCfg, serverCfg: cfg, - randomKey: randomKey, - authKey: getSocketAuthKey(randomKey, commonCfg.SharedKey)[:32], upgrader: buildWebsocketUpgrader(cfg), commander: command.New(cmds), } @@ -234,19 +211,18 @@ func (s socket) createCipher(key []byte) (cipher.AEAD, cipher.AEAD, error) { return gcmRead, gcmWrite, nil } -func (s socket) privateKey() string { - if len(s.commonCfg.SharedKey) > 0 { - return s.commonCfg.SharedKey - } - - return s.randomKey +func (s socket) mixerKey(r *http.Request) []byte { + return hashCombineSocketKeys( + r.UserAgent(), s.commonCfg.SharedKey+"+"+s.commonCfg.HostName) } -func (s socket) buildCipherKey() [16]byte { +func (s socket) buildCipherKey(r *http.Request) [16]byte { key := [16]byte{} - now := strconv.FormatInt(time.Now().Unix()/100, 10) - copy(key[:], getSocketAuthKey(now, s.privateKey())) + copy(key[:], hashCombineSocketKeys( + strconv.FormatInt(time.Now().Unix()/100, 10), + string(s.mixerKey(r))+"+"+s.commonCfg.SharedKey, + )) return key } @@ -300,7 +276,7 @@ func (s socket) Get( "Unable to send server nonce to client: %s", nonceSendErr.Error())) } - cipherKey := s.buildCipherKey() + cipherKey := s.buildCipherKey(r) readCipher, writeCipher, cipherCreationErr := s.createCipher(cipherKey[:]) diff --git a/application/controller/socket_verify.go b/application/controller/socket_verify.go index 0ebc309..36a4c83 100644 --- a/application/controller/socket_verify.go +++ b/application/controller/socket_verify.go @@ -81,6 +81,22 @@ func newSocketVerification( } } +func (s socketVerification) authKey(r *http.Request) []byte { + timeMixer := strconv.FormatInt(time.Now().Unix()/100, 10) + + if len(s.commonCfg.SharedKey) > 0 { + return hashCombineSocketKeys( + timeMixer, + s.commonCfg.SharedKey, + )[:32] + } + + return hashCombineSocketKeys( + timeMixer, + "DEFAULT VERIFY KEY", + )[:32] +} + func (s socketVerification) setServerConfigRespond( hd *http.Header, w http.ResponseWriter) { hd.Add("X-Heartbeat", s.heartbeat) @@ -104,7 +120,7 @@ func (s socketVerification) Get( key := r.Header.Get("X-Key") if len(key) <= 0 { - hd.Add("X-Key", s.randomKey) + hd.Add("X-Key", base64.StdEncoding.EncodeToString(s.mixerKey(r))) if len(s.commonCfg.SharedKey) <= 0 { s.setServerConfigRespond(&hd, w) @@ -129,11 +145,13 @@ func (s socketVerification) Get( return NewError(http.StatusBadRequest, decodedKeyErr.Error()) } - if !hmac.Equal(s.authKey, decodedKey) { + authKey := s.authKey(r) + + if !hmac.Equal(authKey, decodedKey) { return ErrSocketAuthFailed } - hd.Add("X-Key", s.randomKey) + hd.Add("X-Key", base64.StdEncoding.EncodeToString(s.mixerKey(r))) s.setServerConfigRespond(&hd, w) return nil diff --git a/ui/app.js b/ui/app.js index f1c5391..f674456 100644 --- a/ui/app.js +++ b/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; diff --git a/ui/crypto.js b/ui/crypto.js index ea25a8f..ded1346 100644 --- a/ui/crypto.js +++ b/ui/crypto.js @@ -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"] diff --git a/ui/socket.js b/ui/socket.js index dc32fe4..ffbfdef 100644 --- a/ui/socket.js +++ b/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 . -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; } diff --git a/ui/stream/common.js b/ui/stream/common.js index 890cf1f..cc71f15 100644 --- a/ui/stream/common.js +++ b/ui/stream/common.js @@ -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); +}