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

View File

@@ -23,7 +23,6 @@ import (
"crypto/hmac" "crypto/hmac"
"crypto/rand" "crypto/rand"
"crypto/sha512" "crypto/sha512"
"encoding/base64"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@@ -66,32 +65,14 @@ type socket struct {
commonCfg configuration.Common commonCfg configuration.Common
serverCfg configuration.Server serverCfg configuration.Server
randomKey string
authKey []byte
upgrader websocket.Upgrader upgrader websocket.Upgrader
commander command.Commander commander command.Commander
} }
func getNewSocketCtlRandomSharedKey() string { func hashCombineSocketKeys(addedKey string, privateKey string) []byte {
b := [32]byte{} h := hmac.New(sha512.New, []byte(privateKey))
io.ReadFull(rand.Reader, b[:]) h.Write([]byte(addedKey))
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))
return h.Sum(nil) return h.Sum(nil)
} }
@@ -101,13 +82,9 @@ func newSocketCtl(
cfg configuration.Server, cfg configuration.Server,
cmds command.Commands, cmds command.Commands,
) socket { ) socket {
randomKey := getNewSocketCtlRandomSharedKey()
return socket{ return socket{
commonCfg: commonCfg, commonCfg: commonCfg,
serverCfg: cfg, serverCfg: cfg,
randomKey: randomKey,
authKey: getSocketAuthKey(randomKey, commonCfg.SharedKey)[:32],
upgrader: buildWebsocketUpgrader(cfg), upgrader: buildWebsocketUpgrader(cfg),
commander: command.New(cmds), commander: command.New(cmds),
} }
@@ -234,19 +211,18 @@ func (s socket) createCipher(key []byte) (cipher.AEAD, cipher.AEAD, error) {
return gcmRead, gcmWrite, nil return gcmRead, gcmWrite, nil
} }
func (s socket) privateKey() string { func (s socket) mixerKey(r *http.Request) []byte {
if len(s.commonCfg.SharedKey) > 0 { return hashCombineSocketKeys(
return s.commonCfg.SharedKey r.UserAgent(), s.commonCfg.SharedKey+"+"+s.commonCfg.HostName)
}
return s.randomKey
} }
func (s socket) buildCipherKey() [16]byte { func (s socket) buildCipherKey(r *http.Request) [16]byte {
key := [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 return key
} }
@@ -300,7 +276,7 @@ func (s socket) Get(
"Unable to send server nonce to client: %s", nonceSendErr.Error())) "Unable to send server nonce to client: %s", nonceSendErr.Error()))
} }
cipherKey := s.buildCipherKey() cipherKey := s.buildCipherKey(r)
readCipher, writeCipher, cipherCreationErr := s.createCipher(cipherKey[:]) readCipher, writeCipher, cipherCreationErr := s.createCipher(cipherKey[:])

View File

@@ -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( func (s socketVerification) setServerConfigRespond(
hd *http.Header, w http.ResponseWriter) { hd *http.Header, w http.ResponseWriter) {
hd.Add("X-Heartbeat", s.heartbeat) hd.Add("X-Heartbeat", s.heartbeat)
@@ -104,7 +120,7 @@ func (s socketVerification) Get(
key := r.Header.Get("X-Key") key := r.Header.Get("X-Key")
if len(key) <= 0 { 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 { if len(s.commonCfg.SharedKey) <= 0 {
s.setServerConfigRespond(&hd, w) s.setServerConfigRespond(&hd, w)
@@ -129,11 +145,13 @@ func (s socketVerification) Get(
return NewError(http.StatusBadRequest, decodedKeyErr.Error()) return NewError(http.StatusBadRequest, decodedKeyErr.Error())
} }
if !hmac.Equal(s.authKey, decodedKey) { authKey := s.authKey(r)
if !hmac.Equal(authKey, decodedKey) {
return ErrSocketAuthFailed return ErrSocketAuthFailed
} }
hd.Add("X-Key", s.randomKey) hd.Add("X-Key", base64.StdEncoding.EncodeToString(s.mixerKey(r)))
s.setServerConfigRespond(&hd, w) s.setServerConfigRespond(&hd, w)
return nil return nil

129
ui/app.js
View File

@@ -32,6 +32,7 @@ import Home from "./home.vue";
import "./landing.css"; import "./landing.css";
import Loading from "./loading.vue"; import Loading from "./loading.vue";
import { Socket } from "./socket.js"; import { Socket } from "./socket.js";
import * as stream from "./stream/common";
import * as xhr from "./xhr.js"; import * as xhr from "./xhr.js";
const backendQueryRetryDelay = 2000; const backendQueryRetryDelay = 2000;
@@ -72,6 +73,19 @@ function startApp(rootEl) {
let uiControlColor = new ControlColor(); 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({ new Vue({
el: rootEl, el: rootEl,
components: { components: {
@@ -170,11 +184,20 @@ function startApp(rootEl) {
isErrored() { isErrored() {
return this.authErr.length > 0 || this.loadErr.length > 0; return this.authErr.length > 0 || this.loadErr.length > 0;
}, },
async getSocketAuthKey(privateKey, randomKey) { async getSocketAuthKey(privateKey) {
const enc = new TextEncoder(); 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( return new Uint8Array(
await cipher.hmac512(enc.encode(privateKey), enc.encode(randomKey)) await cipher.hmac512(enc.encode(finalKey), enc.encode(rTime))
).slice(0, 32); ).slice(0, 32);
}, },
buildBackendSocketURL() { buildBackendSocketURL() {
@@ -213,6 +236,40 @@ function startApp(rootEl) {
); );
this.page = "app"; 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() { async tryInitialAuth() {
try { try {
let result = await this.doAuth(""); let result = await this.doAuth("");
@@ -235,17 +292,14 @@ function startApp(rootEl) {
} }
let self = this; let self = this;
switch (result.result) { switch (result.result) {
case 200: case 200:
this.executeHomeApp(result, { this.executeHomeApp(result, {
data: result.key, data: await buildSocketKey(atob(result.key) + "+"),
async fetch() { async fetch() {
if (this.data) { if (this.data) {
let dKey = this.data; let dKey = this.data;
this.data = null; this.data = null;
return dKey; return dKey;
} }
@@ -259,7 +313,7 @@ function startApp(rootEl) {
); );
} }
return result.key; return await buildSocketKey(atob(result.key) + "+");
}, },
}); });
break; break;
@@ -281,52 +335,37 @@ function startApp(rootEl) {
this.loadErr = "Unable to initialize client application: " + e; 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) { async submitAuth(passphrase) {
this.authErr = ""; this.authErr = "";
try { try {
let result = await this.doAuth(passphrase); let result = await this.doAuth(passphrase);
let self = this;
switch (result.result) { switch (result.result) {
case 200: case 200:
this.executeHomeApp(result, { this.executeHomeApp(result, {
data: passphrase, data: await buildSocketKey(atob(result.key) + "+" + passphrase),
fetch() { async fetch() {
return this.data; 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; break;

View File

@@ -27,7 +27,7 @@ export async function hmac512(secret, data) {
secret, secret,
{ {
name: "HMAC", name: "HMAC",
hash: { name: "SHA-512" } hash: { name: "SHA-512" },
}, },
false, false,
["sign", "verify"] ["sign", "verify"]
@@ -50,7 +50,7 @@ export function buildGCMKey(keyData) {
keyData, keyData,
{ {
name: "AES-GCM", name: "AES-GCM",
length: GCMKeyBitLen length: GCMKeyBitLen,
}, },
false, false,
["encrypt", "decrypt"] ["encrypt", "decrypt"]

View File

@@ -15,10 +15,10 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // 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 reader from "./stream/reader.js";
import * as sender from "./stream/sender.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; export const ECHO_FAILED = streams.ECHO_FAILED;
@@ -56,7 +56,7 @@ class Dial {
timeoutTimer = setTimeout(() => { timeoutTimer = setTimeout(() => {
ws.close(); ws.close();
}, timeout), }, timeout),
myRes = w => { myRes = (w) => {
if (promised) { if (promised) {
return; return;
} }
@@ -66,7 +66,7 @@ class Dial {
return resolve(w); return resolve(w);
}, },
myRej = e => { myRej = (e) => {
if (promised) { if (promised) {
return; return;
} }
@@ -77,11 +77,11 @@ class Dial {
return reject(e); return reject(e);
}; };
ws.addEventListener("open", _event => { ws.addEventListener("open", (_event) => {
myRes(ws); myRes(ws);
}); });
ws.addEventListener("close", event => { ws.addEventListener("close", (event) => {
event.toString = () => { event.toString = () => {
return "WebSocket Error (" + event.code + ")"; return "WebSocket Error (" + event.code + ")";
}; };
@@ -89,7 +89,7 @@ class Dial {
myRej(event); myRej(event);
}); });
ws.addEventListener("error", _event => { ws.addEventListener("error", (_event) => {
ws.close(); ws.close();
}); });
}); });
@@ -100,15 +100,7 @@ class Dial {
* *
*/ */
async buildKeyString() { async buildKeyString() {
const enc = new TextEncoder(); return this.privateKey.fetch();
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);
} }
/** /**
@@ -131,12 +123,14 @@ class Dial {
* *
*/ */
async dial(callbacks) { async dial(callbacks) {
let ws = await this.connect(this.timeout), let ws = await this.connect(this.timeout);
rd = new reader.Reader(new reader.Multiple(() => {}), data => {
return new Promise(resolve => { try {
let rd = new reader.Reader(new reader.Multiple(() => {}), (data) => {
return new Promise((resolve) => {
let bufferReader = new FileReader(); let bufferReader = new FileReader();
bufferReader.onload = event => { bufferReader.onload = (event) => {
let d = new Uint8Array(event.target.result); let d = new Uint8Array(event.target.result);
resolve(d); resolve(d);
@@ -148,105 +142,112 @@ class Dial {
}); });
}); });
ws.addEventListener("message", event => { ws.addEventListener("message", (event) => {
callbacks.inbound(event.data); callbacks.inbound(event.data);
rd.feed(event.data); rd.feed(event.data);
}); });
ws.addEventListener("error", event => { ws.addEventListener("error", (event) => {
event.toString = () => { event.toString = () => {
return ( return (
"WebSocket Error (" + (event.code ? event.code : "Unknown") + ")" "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 => { dSize = dSizeBytes[0];
rd.closeWithReason("Connection is closed"); dSize <<= 8;
}); dSize |= dSizeBytes[1];
let sdDataConvert = rawData => { let decoded = await crypt.decryptGCM(
return rawData; key,
}, receiverNonce,
getSdDataConvert = () => { await reader.readN(rd, dSize)
return sdDataConvert; );
},
sd = new sender.Sender(
async rawData => {
try {
let data = await getSdDataConvert()(rawData);
ws.send(data.buffer); crypt.increaseNonce(receiverNonce);
callbacks.outbound(data);
} catch (e) {
ws.close();
rd.closeWithReason(e);
if (process.env.NODE_ENV === "development") { r.feed(
console.error(e); new reader.Buffer(new Uint8Array(decoded), () => {}),
} () => {}
);
} catch (e) {
r.closeWithReason(e);
}
});
throw e; return {
} reader: cgmReader,
}, sender: sd,
4096 - 64, // Server has a 4096 bytes receive buffer, can be no greater, ws: ws,
minSenderDelay, // 30ms input delay };
10 // max 10 buffered requests } catch (e) {
); ws.close();
throw e;
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
};
} }
} }
@@ -328,7 +329,7 @@ export class Socket {
}, },
outbound(data) { outbound(data) {
callbacks.traffic(0, data.length); callbacks.traffic(0, data.length);
} },
}); });
let streamHandler = new streams.Streams(conn.reader, conn.sender, { let streamHandler = new streams.Streams(conn.reader, conn.sender, {
@@ -357,12 +358,12 @@ export class Socket {
// risk sending things out // risk sending things out
conn.ws.close(); conn.ws.close();
callbacks.close(e); callbacks.close(e);
} },
}); });
callbacks.connected(); callbacks.connected();
streamHandler.serve().catch(e => { streamHandler.serve().catch((e) => {
if (process.env.NODE_ENV !== "development") { if (process.env.NODE_ENV !== "development") {
return; return;
} }

View File

@@ -75,3 +75,35 @@ export function separateBuffer(buf, max) {
start += remain; 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);
}