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:
@@ -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
|
||||
func (s socket) mixerKey(r *http.Request) []byte {
|
||||
return hashCombineSocketKeys(
|
||||
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{}
|
||||
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[:])
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
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"]
|
||||
|
||||
65
ui/socket.js
65
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,13 +142,13 @@ class Dial {
|
||||
});
|
||||
});
|
||||
|
||||
ws.addEventListener("message", event => {
|
||||
ws.addEventListener("message", (event) => {
|
||||
callbacks.inbound(event.data);
|
||||
|
||||
rd.feed(event.data);
|
||||
});
|
||||
|
||||
ws.addEventListener("error", event => {
|
||||
ws.addEventListener("error", (event) => {
|
||||
event.toString = () => {
|
||||
return (
|
||||
"WebSocket Error (" + (event.code ? event.code : "Unknown") + ")"
|
||||
@@ -164,18 +158,18 @@ class Dial {
|
||||
rd.closeWithReason(event);
|
||||
});
|
||||
|
||||
ws.addEventListener("close", _event => {
|
||||
ws.addEventListener("close", (_event) => {
|
||||
rd.closeWithReason("Connection is closed");
|
||||
});
|
||||
|
||||
let sdDataConvert = rawData => {
|
||||
let sdDataConvert = (rawData) => {
|
||||
return rawData;
|
||||
},
|
||||
getSdDataConvert = () => {
|
||||
return sdDataConvert;
|
||||
},
|
||||
sd = new sender.Sender(
|
||||
async rawData => {
|
||||
async (rawData) => {
|
||||
try {
|
||||
let data = await getSdDataConvert()(rawData);
|
||||
|
||||
@@ -204,7 +198,7 @@ class Dial {
|
||||
|
||||
let key = await this.buildKey();
|
||||
|
||||
sdDataConvert = async rawData => {
|
||||
sdDataConvert = async (rawData) => {
|
||||
let encoded = await crypt.encryptGCM(key, senderNonce, rawData);
|
||||
|
||||
crypt.increaseNonce(senderNonce);
|
||||
@@ -219,7 +213,7 @@ class Dial {
|
||||
return dataToSend;
|
||||
};
|
||||
|
||||
let cgmReader = new reader.Multiple(async r => {
|
||||
let cgmReader = new reader.Multiple(async (r) => {
|
||||
try {
|
||||
let dSizeBytes = await reader.readN(rd, 2),
|
||||
dSize = 0;
|
||||
@@ -236,7 +230,10 @@ class Dial {
|
||||
|
||||
crypt.increaseNonce(receiverNonce);
|
||||
|
||||
r.feed(new reader.Buffer(new Uint8Array(decoded), () => {}), () => {});
|
||||
r.feed(
|
||||
new reader.Buffer(new Uint8Array(decoded), () => {}),
|
||||
() => {}
|
||||
);
|
||||
} catch (e) {
|
||||
r.closeWithReason(e);
|
||||
}
|
||||
@@ -245,8 +242,12 @@ class Dial {
|
||||
return {
|
||||
reader: cgmReader,
|
||||
sender: sd,
|
||||
ws: ws
|
||||
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