Fixed the labels not being hidden in inoutdivs, made inoutdivs use a flexbox or table to avoid the use of a calc() and a transform(), updated to AES-256 GCM Native crypto from CryptoJS, added a migration page to migrate from CryptoJS, remove the Argon2 Compatibility thing, remove all backwards-compatibility measures, updated the changelog to be accurate, made NoScript cancel-able, made the inoutdiv dynamically scale, told people what a homeserver is, switched to argon2id from sha-3 and sha-512, add a PoW captcha using WASM, make a really long commit message.

If you are still reading this, I admire your dedication. I spent 8 hours and 33 minutes on this commit alone.
This commit is contained in:
Tracker-Friendly 2024-07-20 17:12:25 +01:00
parent 33f0517a9d
commit 05279a0811
13 changed files with 1296 additions and 10319 deletions

View File

@ -6,9 +6,8 @@
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" type="text/css" href="/static/css/style.css" />
<link rel="stylesheet" type="text/css" href="/static/css/style.css"/>
<script type="text/javascript" src="/static/js/hash-wasm.js"></script>
<script type="text/javascript" src="/static/js/crypto-js.js"></script>
<link rel="icon" href="/static/svg/favicon.svg">
</head>

View File

@ -14,14 +14,58 @@
<body>
<img src="/static/img/background.jpg" class="background" alt="">
<div class="inoutdiv">
<h2>Alternative servers</h2>
<p>Enter your custom homeserver URL <a href="/">What does this mean?</a></p>
<noscript>
<style>
.checkMark {
width: inherit !important;
height: inherit !important;
display: block !important;
position: fixed !important;
left: 50%;
top: 50%;
transform: translate(calc(-50% - 125px), calc(-50% - 16px));
}
<span id="inputNameBox" style="margin-right: 5px;color: var(--text-color);">Server URL:</span><input type="text" placeholder="https://example.org" id="homeserverBox"><br>
<button id="changeButton">Change</button><button class="nonimportant" id="backButton">Cancel</button><br><br>
.noContent {
display: inherit;
}
<p id="statusBox">Loading...</p>
<p>Please put in the URL in standard format; https://, http://, etc.</p>
.content {
display: none;
}
.checkMark:checked ~ .content {
display: inherit !important;
}
.checkMark:checked ~ .noContent {
display: none !important;
}
.checkMark:checked {
display: none !important;
}
</style>
</noscript>
<input type="checkbox" class="checkMark">
<div class="noContent">
<h2 style="display: block !important;">Your web browser is unsupported</h2>
<p>Please enable JavaScript in your web browser to continue</p>
<p style="margin-bottom: 135px;">False alarm? Click the checkmark to ignore this warning.</p>
</div>
<div class="content">
<h2>Alternative servers</h2>
<p>Enter your custom homeserver URL</p>
<div class="inputContainer" id="inputContainer">
<div class="vAlign"><span id="inputNameBox">URL:</span></div>
<input type="text" placeholder="https://example.org" id="homeserverBox"><br>
</div>
<p>Please put in the URL in standard format; https://, http://, etc.</p>
<button id="changeButton" class="clickButton">Change</button>
<button class="clickButton nonimportant" id="backButton">Cancel</button><br><br>
<p>A homeserver is the server that your notes and account are stored on.</p>
<p id="statusBox">Loading...</p>
</div>
</div>
<script type="text/javascript" src="/static/js/homeserver.js"></script>

View File

@ -15,26 +15,60 @@
<img src="/static/img/background.jpg" class="background" alt="">
<div class="inoutdiv">
<noscript>
<h2 style="display: block !important;">Your web browser is unsupported</h2>
<p style="margin-bottom: 135px;">Please enable JavaScript in your web browser to continue</p>
<style>
h2, button, br {
.checkMark {
width: inherit !important;
height: inherit !important;
display: block !important;
position: fixed !important;
left: 50%;
top: 50%;
transform: translate(calc(-50% - 125px), calc(-50% - 16px));
}
.noContent {
display: inherit;
}
.content {
display: none;
}
.checkMark:checked ~ .content {
display: inherit !important;
}
.checkMark:checked ~ .noContent {
display: none !important;
}
.checkMark:checked {
display: none !important;
}
</style>
</noscript>
<h2>Sign in</h2>
<p id="statusBox"></p>
<span id="inputNameBox" style="margin-right: 10px;color: var(--text-color);"></span>
<input id="usernameBox" class="hidden" type="text" placeholder="Enter your username">
<input id="passwordBox" class="hidden" type="password" placeholder="Enter your password">
<button class="clickButton" id="signupButton">Next</button>
<button id="backButton" class="clickButton hidden nonimportant">Back</button>
<button id="opButton" class="clickButton nonimportant">Create account</button>
<br><br>
<div style="display: flex;"><p class="hidden" id="homeserver">Your homeserver is loading... </p><div style="display: flex;flex-direction: column;justify-content: center;"><a class="hidden" href="/homeserver">Change</a></div></div>
<a class="iconbutton" title="Change homeserver" href="/homeserver/"><img src="/static/svg/server.svg"></a><br><br>
<a href="/privacy/">Privacy &amp; Terms</a>
<input type="checkbox" class="checkMark">
<div class="noContent">
<h2 style="display: block !important;">Your web browser is unsupported</h2>
<p>Please enable JavaScript in your web browser to continue</p>
<p style="margin-bottom: 135px;">False alarm? Click the checkmark to ignore this warning.</p>
</div>
<div class="content">
<h2>Sign in</h2>
<p id="statusBox"></p>
<div class="inputContainer" id="inputContainer">
<div class="vAlign"><span id="inputNameBox"></span></div>
<input id="usernameBox" class="hidden" type="text" placeholder="Enter your username">
<input id="passwordBox" class="hidden" type="password" placeholder="Enter your password">
</div>
<button class="clickButton" id="signupButton">Next</button>
<button id="backButton" class="clickButton hidden nonimportant">Back</button>
<button id="opButton" class="clickButton nonimportant">Create account</button>
<br><br>
<div style="display: flex;"><p class="hidden" id="homeserver">Your homeserver is loading... </p><div style="display: flex;flex-direction: column;justify-content: center;"><a class="hidden" href="/homeserver">Change</a></div></div>
<a class="iconbutton" title="Change homeserver" href="/homeserver/"><img src="/static/svg/server.svg" alt="Homeservers"></a><br><br>
<a href="/privacy/">Privacy &amp; Terms</a>
</div>
</div>
<script type="text/javascript" src="../static/js/login.js"></script>

64
migrate/index.html Normal file
View File

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Burgernotes</title>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" type="text/css" href="/static/css/style.css"/>
<script type="text/javascript" src="/static/js/hash-wasm.js"></script>
<script type="text/javascript" src="/static/js/crypto-js.js"></script>
<script type="text/javascript" src="/static/js/migrate.js"></script>
<link rel="icon" href="/static/svg/favicon.svg">
</head>
<body>
<img src="/static/img/background.jpg" class="background" alt="">
<div class="inoutdiv">
<noscript>
<style>
.checkMark {
width: inherit !important;
height: inherit !important;
display: block !important;
position: fixed !important;
left: 50%;
top: 50%;
transform: translate(calc(-50% - 125px), calc(-50% - 16px));
}
.noContent {
display: inherit;
}
.content {
display: none;
}
.checkMark:checked ~ .content {
display: inherit !important;
}
.checkMark:checked ~ .noContent {
display: none !important;
}
.checkMark:checked {
display: none !important;
}
</style>
</noscript>
<input type="checkbox" class="checkMark">
<div class="noContent">
<h2 style="display: block !important;">Your web browser is unsupported</h2>
<p>Please enable JavaScript in your web browser to continue</p>
<p style="margin-bottom: 135px;">False alarm? Click the checkmark to ignore this warning.</p>
</div>
<div class="content">
<h2 id="title">Burgernotes Migrator</h2>
<p id="information">Welcome to the Burgernotes Migration wizard! Before we begin migration, there are a few things you should know.</p>
<button onclick="buttonClick()" class="clickButton">Continue</button>
<button id="backButton" onclick="back()" class="clickButton nonimportant hidden">Back</button>
</div>
</div>
</body>
</html>

View File

@ -8,23 +8,72 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" type="text/css" href="/static/css/style.css" />
<script src="/static/js/hash-wasm.js"></script>
<script src="/static/js/wasm_exec.js"></script>
<link rel="icon" href="/static/svg/favicon.svg">
</head>
<body>
<img src="/static/img/background.jpg" class="background" alt="">
<div class="inoutdiv">
<h2>Create a Burgernotes Account</h2>
<p>Get started by picking a username and password</p>
<p style="color: #b3261e; font-size: 14px; margin-bottom: 2px; margin-top: 1px;" id="statusBox"></p>
<span id="inputNameBox" style="margin-right: 10px;color: var(--text-color);">Username:</span>
<input id="usernameBox" type="text" placeholder="Username">
<span id="inputNameBox" style="margin-right: 13px;color: var(--text-color);">Password: </span>
<input id="passwordBox" type="password" placeholder="Password"><br>
<button class="clickButton" id="signupButton">Create account</button><button id="opButton" class="clickButton nonimportant">Already have an account</button><br><br>
<div style="display: flex;"><p class="hidden" id="homeserver">Your homeserver is loading... </p><div style="display: flex;flex-direction: column;justify-content: center;"><a class="hidden" href="/homeserver">Change</a></div></div>
<a class="iconbutton" title="Change homeserver" href="/homeserver/"><img src="/static/svg/server.svg"></a>
<a href="/privacy/">Privacy &amp; Terms</a>
<noscript>
<style>
.checkMark {
width: inherit !important;
height: inherit !important;
display: block !important;
position: fixed !important;
left: 50%;
top: 50%;
transform: translate(calc(-50% - 125px), calc(-50% - 16px));
}
.noContent {
display: inherit;
}
.content {
display: none;
}
.checkMark:checked ~ .content {
display: inherit !important;
}
.checkMark:checked ~ .noContent {
display: none !important;
}
.checkMark:checked {
display: none !important;
}
</style>
</noscript>
<input type="checkbox" class="checkMark">
<div class="noContent">
<h2 style="display: block !important;">Your web browser is unsupported</h2>
<p>Please enable JavaScript in your web browser to continue</p>
<p style="margin-bottom: 135px;">False alarm? Click the checkmark to ignore this warning.</p>
</div>
<div class="content">
<h2>Create a Burgernotes Account</h2>
<p>Get started by picking a username and password</p>
<p style="color: #b3261e; font-size: 14px; margin-bottom: 2px; margin-top: 1px;" id="statusBox"></p>
<table id="inputContainer">
<tr>
<td><span id="inputNameBox">Username:</span></td>
<td class="inputBox"><input id="usernameBox" type="text" placeholder="Username"></td>
</tr>
<tr>
<td><span id="inputPasswordBox">Password: </span></td>
<td class="inputBox"><input id="passwordBox" type="password" placeholder="Password"><br></td>
</tr>
</table>
<br>
<button class="clickButton" id="signupButton">Create account</button><button id="opButton" class="clickButton nonimportant">Already have an account</button><br><br>
<div style="display: flex;"><p class="hidden" id="homeserver">Your homeserver is loading... </p><div style="display: flex;flex-direction: column;justify-content: center;"><a class="hidden" href="/homeserver">Change</a></div></div>
<a class="iconbutton" title="Change homeserver" href="/homeserver/"><img src="/static/svg/server.svg" alt="Homeservers"></a>
<a href="/privacy/">Privacy &amp; Terms</a>
</div>
</div>
<script type="text/javascript" src="/static/js/signup.js"></script>
<script src="/static/js/signup.js"></script>
</body>

View File

@ -12,7 +12,7 @@
--note-theme-color: #CDE1EC;
--hover-theme-color: #4990e7;
--nonimporant-theme-color: #EBEBEB;
--hover-nonimportant-theme-color: #dbdbdb;
--hover-nonimportant-theme-color: #dbdbdb;
--nonimportant-text-color: #000;
--portal-background-color: #FAFAFA;
--theme-text-color: #ffffff;
@ -41,7 +41,7 @@
--note-theme-color: #293B53;
--hover-theme-color: #4990e7;
--nonimporant-theme-color: #4A4A4A;
--hover-nonimportant-theme-color: #595959;
--hover-nonimportant-theme-color: #595959;
--nonimportant-text-color: #fff;
--portal-background-color: #242424;
--theme-text-color: #ffffff;
@ -321,10 +321,10 @@ body {
.notesBar .loadingStuff {
border: none;
background:
linear-gradient(0.25turn, transparent, var(--contrast2), transparent),
linear-gradient(var(--contrast), var(--contrast)),
radial-gradient(38px circle at 19px 19px, #eee 50%, transparent 51%),
linear-gradient(var(--contrast), var(--contrast));
linear-gradient(0.25turn, transparent, var(--contrast2), transparent),
linear-gradient(var(--contrast), var(--contrast)),
radial-gradient(38px circle at 19px 19px, #eee 50%, transparent 51%),
linear-gradient(var(--contrast), var(--contrast));
background-repeat: no-repeat;
background-size: 200% 200%, 100% 200%, 50% 50%, 140% 12%;
background-position: -200% 0, 0 0, 0 76%, 16% 78%;
@ -408,32 +408,32 @@ body {
}
.pell-content {
width: 100%;
height: calc(100% - 20px);
overflow-y: scroll;
width: 100%;
height: calc(100% - 20px);
overflow-y: scroll;
}
.pell-button {
background-color: var(--note-button);
border: 1px var(--border-color) solid;
min-width: 35px;
max-width: 35px;
height: 35px;
margin-left: 1px;
margin-right: 1px;
border-radius: 10px;
background-color: var(--note-button);
border: 1px var(--border-color) solid;
min-width: 35px;
max-width: 35px;
height: 35px;
margin-left: 1px;
margin-right: 1px;
border-radius: 10px;
}
button:hover {
background-color: var(--note-button-hover);
background-color: var(--note-button-hover);
}
.pell-actionbar {
display: flex;
margin-bottom: 5px;
overflow-x: auto;
position: fixed;
top: 8px;
display: flex;
margin-bottom: 5px;
overflow-x: auto;
position: fixed;
top: 8px;
}
@media only screen and (max-width: 860px) {
@ -652,22 +652,44 @@ button:hover {
transform: translate(-50%, -50%);
position: fixed;
min-width: 585px;
height: 270px;
max-height: 270px;
min-height: 270px;
border-radius: 16px;
padding: 35px;
background-color: var(--portal-background-color);
}
.noContent {
display: none;
}
.inoutdiv .checkMark {
display: none;
}
.inoutdiv h2 {
font-family: "Space Grotesk", sans-serif;
font-optical-sizing: auto;
}
.inoutdiv input {
width: calc(100% - 120px);
height: 30px;
.inoutdiv .inputBox {
width: 100%
}
.inoutdiv .inputContainer {
display: flex;
width: 100%;
margin-right: 30px;
margin-bottom: 10px;
}
.inoutdiv table {
margin-right: 30px;
}
.inoutdiv input {
width: 100%;
margin-left: 10px;
height: 30px;
padding-left: 10px;
padding-right: 10px;
@ -703,7 +725,7 @@ button:hover {
.inoutdiv button {
transition: none !important;
}
}
}
.inoutdiv .nonimportant {
background-color: var(--nonimporant-theme-color);
@ -848,7 +870,7 @@ button:hover {
}
.startDiv {
}
.startDiv h1 {
@ -903,7 +925,7 @@ button:hover {
.mainDiv a {
transition: none !important;
}
}
}
.mainDiv .burgerText {
width: 75%;

View File

@ -19,10 +19,12 @@ function showElements(yesorno) {
if (!yesorno) {
homeserverBox.classList.add("hidden")
changeButton.classList.add("hidden")
inputContainer.classList.add("hidden")
}
else {
homeserverBox.classList.remove("hidden")
changeButton.classList.remove("hidden")
inputContainer.classList.remove("hidden")
}
}

View File

@ -12,6 +12,7 @@ if (remote == null) {
remote = "https://notes.hectabit.org"
}
let inputContainer = document.getElementById("inputContainer")
let usernameBox = document.getElementById("usernameBox")
let passwordBox = document.getElementById("passwordBox")
let statusBox = document.getElementById("statusBox")
@ -20,53 +21,16 @@ let inputNameBox = document.getElementById("inputNameBox")
let backButton = document.getElementById("backButton")
let opButton = document.getElementById("opButton")
async function loginFetch(username, password, changePass, newPass) {
if (localStorage.getItem("legacy") !== true) {
return await fetch(remote + "/api/login", {
method: "POST",
body: JSON.stringify({
username: username,
password: password,
}),
headers: {
"Content-Type": "application/json; charset=UTF-8",
"X-Burgernotes-Version": "200"
}
})
} else {
let passwordChange, newPassChecked
if (changePass) {
passwordChange = "yes"
newPassChecked = newPass
} else {
passwordChange = "no"
newPassChecked = password
}
return await fetch(remote + "/api/login", {
method: "POST",
body: JSON.stringify({
username: username,
password: password,
passwordchange: passwordChange,
newpass: newPassChecked
}),
headers: {
"Content-Type": "application/json; charset=UTF-8",
}
})
}
}
async function addLegacyPassword(secretKey, password) {
return await fetch(remote + "/api/v2/addlegacypassword", {
async function loginFetch(username, password, modern) {
return await fetch(remote + "/api/login", {
method: "POST",
body: JSON.stringify({
secretKey: secretKey,
legacyPassword: password,
username: username,
password: password,
modern: modern
}),
headers: {
"Content-Type": "application/json; charset=UTF-8",
"X-Burgernotes-Version": "200"
}
})
}
@ -80,24 +44,23 @@ async function migrateLegacyPassword(secretKey, password) {
}),
headers: {
"Content-Type": "application/json; charset=UTF-8",
"X-Burgernotes-Version": "200"
}
})
}
async function hashpassold(pass) {
async function hashPass(pass) {
return await hashwasm.argon2id({
password: pass,
salt: await hashwasm.sha512(pass),
salt: new TextEncoder().encode("I munch Burgers!!"),
parallelism: 1,
iterations: 256,
memorySize: 512,
iterations: 32,
memorySize: 19264,
hashLength: 32,
outputType: "encoded"
outputType: "hex"
})
}
async function hashpass(pass) {
async function hashPassLegacy(pass) {
let key = pass
for (let i = 0; i < 128; i++) {
key = await hashwasm.sha3(key)
@ -111,35 +74,39 @@ inputNameBox.innerText = "Username:"
let currentInputType = 0
function showInput(inputType) {
if (inputType === 0) {
usernameBox.classList.remove("hidden")
passwordBox.classList.add("hidden")
backButton.classList.add("hidden")
opButton.classList.remove("hidden")
inputNameBox.innerText = "Username:"
statusBox.innerText = "Sign in with your Burgernotes account"
currentInputType = 0
} else if (inputType === 1) {
usernameBox.classList.add("hidden")
passwordBox.classList.remove("hidden")
backButton.classList.remove("hidden")
opButton.classList.add("hidden")
inputNameBox.innerText = "Password:"
currentInputType = 1
} else if (inputType === 2) {
usernameBox.classList.add("hidden")
passwordBox.classList.add("hidden")
signupButton.classList.add("hidden")
backButton.classList.add("hidden")
inputNameBox.classList.add("hidden")
opButton.classList.add("hidden")
inputNameBox.innerText = "Password:"
currentInputType = 2
switch (inputType) {
case 0:
usernameBox.classList.remove("hidden")
passwordBox.classList.add("hidden")
backButton.classList.add("hidden")
opButton.classList.remove("hidden")
inputContainer.classList.remove("hidden")
inputNameBox.innerText = "Username:"
statusBox.innerText = "Sign in with your Burgernotes account"
currentInputType = 0
break
case 1:
usernameBox.classList.add("hidden")
passwordBox.classList.remove("hidden")
backButton.classList.remove("hidden")
inputContainer.classList.remove("hidden")
opButton.classList.add("hidden")
inputNameBox.innerText = "Password:"
currentInputType = 1
break
case 2:
inputContainer.classList.add("hidden")
signupButton.classList.add("hidden")
backButton.classList.add("hidden")
inputNameBox.classList.add("hidden")
opButton.classList.add("hidden")
inputNameBox.innerText = "Password:"
currentInputType = 2
}
}
function showElements(yesorno) {
if (!yesorno) {
function showElements(show) {
if (!show) {
usernameBox.classList.add("hidden")
passwordBox.classList.add("hidden")
signupButton.classList.add("hidden")
@ -186,39 +153,61 @@ signupButton.addEventListener("click", () => {
showInput(2)
showElements(true)
statusBox.innerText = "Signing in..."
statusBox.innerText = "Hashing password..."
const hashedPass = await hashpass(password)
const login = await loginFetch(username, hashedPass, false, "")
const hashedPass = await hashPass(password)
const login = await loginFetch(username, hashedPass, true)
const loginData = await login.json()
if (login.status === 401) {
// Trying hashpassold
const loginOld = await loginFetch(username, await hashpassold(password), true, hashedPass)
const loginDataOld = await loginOld.json()
if (loginOld.status === 401) {
statusBox.innerText = "Username or password incorrect!"
showInput(1)
showElements(true)
} else if (loginOld.status === 200) {
localStorage.setItem("DONOTSHARE-secretkey", loginDataOld["key"])
localStorage.setItem("DONOTSHARE-password", await hashwasm.sha512(password))
if (loginDataOld["legacyPasswordNeeded"] === true) {
await addLegacyPassword(username, await hashpass(await hashpassold(password)))
if (loginData["migrated"] !== true) {
statusBox.innerText = "Migrating to Burgernotes 2.0..."
const loginOld = await loginFetch(username, await hashPassLegacy(password), false)
const loginDataOld = await loginOld.json()
if (loginOld.status === 401) {
statusBox.innerText = "Username or password incorrect!"
showInput(1)
showElements(true)
} else if (loginOld.status === 200) {
statusBox.innerText = "Setting up encryption keys..."
localStorage.setItem("DONOTSHARE-secretkey", loginDataOld["key"])
localStorage.setItem("DONOTSHARE-password", await hashwasm.argon2id({
password: password,
salt: new TextEncoder().encode("I love Burgernotes!"),
parallelism: 1,
iterations: 32,
memorySize: 19264,
hashLength: 32,
outputType: "hex"
}))
await migrateLegacyPassword(loginDataOld["key"], hashedPass)
statusBox.innerText = "Welcome back!"
await new Promise(r => setTimeout(r, 200))
window.location.href = "/app/"
} else {
statusBox.innerText = loginDataOld["error"]
showInput(1)
showElements(true)
}
await migrateLegacyPassword(loginDataOld["key"], hashedPass)
window.location.replace("/app/")
} else {
statusBox.innerText = loginDataOld["error"]
statusBox.innerText = "Username or password incorrect!"
showInput(1)
showElements(true)
}
} else if (login.status === 200) {
statusBox.innerText = "Setting up encryption keys..."
localStorage.setItem("DONOTSHARE-secretkey", loginData["key"])
localStorage.setItem("DONOTSHARE-password", await hashwasm.sha512(password))
if (loginData["legacyPasswordNeeded"] === true) {
await addLegacyPassword(username, await hashpass(await hashpassold(password)))
}
window.location.replace("/app/")
localStorage.setItem("DONOTSHARE-password", await hashwasm.argon2id({
password: password,
salt: new TextEncoder().encode("I love Burgernotes!"),
parallelism: 1,
iterations: 32,
memorySize: 19264,
hashLength: 32,
outputType: "hex"
}))
statusBox.innerText = "Welcome back!"
await new Promise(r => setTimeout(r, 200))
window.location.href = "/app/"
} else {
statusBox.innerText = loginData["error"]
showInput(1)

View File

@ -1,11 +1,6 @@
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0
if (localStorage.getItem("DONOTSHARE-secretkey") === null) {
window.location.replace("/login")
document.body.innerHTML = "Redirecting..."
throw new Error();
}
if (localStorage.getItem("DONOTSHARE-password") === null) {
if (localStorage.getItem("DONOTSHARE-secretkey") === null || localStorage.getItem("DONOTSHARE-password") === null) {
window.location.replace("/login")
document.body.innerHTML = "Redirecting..."
throw new Error();
@ -17,7 +12,10 @@ if (remote == null) {
remote = "https://notes.hectabit.org"
}
function formatBytes(a, b = 2) { if (!+a) return "0 Bytes"; const c = 0 > b ? 0 : b, d = Math.floor(Math.log(a) / Math.log(1000)); return `${parseFloat((a / Math.pow(1000, d)).toFixed(c))} ${["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"][d]}` }
function formatBytes(a, b = 2) {
if (!+a) return "0 Bytes"; const c = 0 > b ? 0 : b, d = Math.floor(Math.log(a) / Math.log(1000));
return `${parseFloat((a / Math.pow(1000, d)).toFixed(c))} ${["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"][d]}`
}
let secretkey = localStorage.getItem("DONOTSHARE-secretkey")
let password = localStorage.getItem("DONOTSHARE-password")
@ -61,7 +59,6 @@ let textPlusBox = document.getElementById('textPlusBox');
let textMinusBox = document.getElementById('textMinusBox');
let wordCountBox = document.getElementById('wordCountBox');
let removeBox = document.getElementById("removeBox")
let importFile = document.getElementById("importFile")
let importFileConfirm = document.getElementById("importFileConfirm")
let selectedNote = 0
@ -71,39 +68,61 @@ let indiv = false
let mobile = false
let selectLatestNote = false
function arrayBufferToBase64(buffer) {
const uint8Array = new Uint8Array(buffer);
return btoa(String.fromCharCode.apply(null, uint8Array))
}
function base64ToArrayBuffer(base64) {
const binaryString = atob(base64);
const length = binaryString.length;
const buffer = new ArrayBuffer(length);
const uint8Array = new Uint8Array(buffer);
for (let i = 0; i < length; i++) {
uint8Array[i] = binaryString.charCodeAt(i);
}
return buffer;
}
async function getKey() {
let password = localStorage.getItem("DONOTSHARE-password")
let cryptoKey = await window.crypto.subtle.importKey("raw", new TextEncoder().encode(password), "PBKDF2", false, ["deriveBits", "deriveKey"])
let salt = new TextEncoder().encode("I love Burgernotes!")
let cryptoKey = await window.crypto.subtle.importKey("raw", new TextEncoder().encode(password), "PBKDF2", false, ["deriveBits", "deriveKey"])
return await window.crypto.subtle.deriveKey({
name: "PBKDF2",
salt,
iterations: 100000,
iterations: 1,
hash: "SHA-512"
}, cryptoKey, {name: "AES-GCM", length: 256}, true, ["encrypt", "decrypt"])
}
function encrypt(text) {
getKey()
.then((key) => {
let iv = window.crypto.getRandomValues(new Uint8Array(12))
window.crypto.subtle.encrypt({
name: "AES-GCM",
iv: iv
}, key, new TextEncoder().encode(text))
.then((encrypted) => {
return btoa(JSON.stringify({
encrypted: encrypted,
iv: iv
}))
})
})
async function encrypt(text) {
let cryptoKey = await getKey()
let iv = window.crypto.getRandomValues(new Uint8Array(12))
let encrypted = await window.crypto.subtle.encrypt({
name: "AES-GCM",
iv: iv
}, cryptoKey, new TextEncoder().encode(text))
return btoa(JSON.stringify({
encrypted: arrayBufferToBase64(encrypted),
iv: arrayBufferToBase64(iv)
}))
}
function decrypt(encrypted) {
getKey()
.then((key) => {
})
async function decrypt(encrypted) {
if (encrypted === "") {
return ""
} else {
let cryptoKey = await getKey()
let jsonData = JSON.parse(atob(encrypted))
let encryptedData = base64ToArrayBuffer(jsonData.encrypted)
let iv = base64ToArrayBuffer(jsonData.iv)
let decrypted = await window.crypto.subtle.decrypt({
name: "AES-GCM",
iv: iv
}, cryptoKey, encryptedData)
return new TextDecoder().decode(decrypted)
}
}
function handleGesture() {
@ -136,7 +155,9 @@ function handleGesture() {
document.addEventListener("DOMContentLoaded", function() {
pell.init({
element: pellAttacher,
onChange: html => console.log(html),
onChange: function(html) {
// Having a nice day? This does nothing.
},
defaultParagraphSeparator: 'br',
styleWithCSS: false,
classes: {
@ -502,7 +523,6 @@ document.addEventListener("DOMContentLoaded", function() {
} else {
closeErrorButton.classList.remove("hidden")
const data = await response.json()
console.log(data)
displayError(data["error"])
}
}
@ -638,8 +658,16 @@ document.addEventListener("DOMContentLoaded", function() {
async function doStuff() {
let responseData = await response.json()
let bytes = CryptoJS.AES.decrypt(responseData["content"], password);
let cleanedHTML = bytes.toString(CryptoJS.enc.Utf8).replace(/<(?!\/?(h1|h2|br|img|blockquote|ol|li|b|i|u|strike|p|pre|ul|hr|a)\b)[^>]*>/gi, '')
let htmlNote
try {
htmlNote = await decrypt(responseData["content"])
} catch (e) {
console.log(e)
console.log(responseData)
}
console.log(htmlNote)
let cleanedHTML = htmlNote.replace(/<(?!\/?(h1|h2|br|img|blockquote|ol|li|b|i|u|strike|p|pre|ul|hr|a)\b)[^>]*>/gi, '(potential XSS tag was here)')
noteBox.innerHTML = cleanedHTML.replace("\n", "<br>")
updateWordCount()
@ -647,21 +675,19 @@ document.addEventListener("DOMContentLoaded", function() {
noteBox.addEventListener("input", () => {
updateWordCount()
clearTimeout(timer);
timer = setTimeout(() => {
let preEncryptedTitle = noteBox.innerText
timer = setTimeout(async () => {
let preEncryptedTitle = "New note"
if (noteBox.innerText.substring(0, noteBox.innerText.indexOf("\n")) !== "") {
preEncryptedTitle = noteBox.innerText.substring(0, noteBox.innerText.indexOf("\n"));
if (noteBox.innerText !== "") {
preEncryptedTitle = new RegExp('(.+?)(?=\n)|[\s\S]*?(\S+)(?=\n)').exec(noteBox.innerText)[0]
}
preEncryptedTitle = truncateString(preEncryptedTitle, 15)
document.getElementById(nameithink).innerText = preEncryptedTitle
let encryptedText = CryptoJS.AES.encrypt(noteBox.innerHTML, password).toString();
let encryptedTitle = CryptoJS.AES.encrypt(preEncryptedTitle, password).toString();
console.log(encryptedTitle)
console.log(encryptedText)
console.log(noteBox.innerHTML)
let encryptedText = await encrypt(noteBox.innerHTML)
let encryptedTitle = await encrypt(preEncryptedTitle)
if (selectedNote === nameithink) {
fetch(remote + "/api/editnote", {
@ -694,7 +720,6 @@ document.addEventListener("DOMContentLoaded", function() {
}
function updateNotes() {
console.log("Notes updated")
fetch(remote + "/api/listnotes", {
method: "POST",
body: JSON.stringify({
@ -720,15 +745,17 @@ document.addEventListener("DOMContentLoaded", function() {
for (let i in responseData) {
noteData = responseData[i]
let bytes = CryptoJS.AES.decrypt(noteData["title"], password);
noteData["title"] = bytes.toString(CryptoJS.enc.Utf8)
try {
noteData["title"] = await decrypt(noteData["title"])
} catch (e) {
location.href = "/migrate"
}
if (noteData["id"] > highestID) {
highestID = noteData["id"]
}
decryptedResponseData.push(noteData)
console.log(noteData)
}
document.querySelectorAll(".noteButton").forEach((el) => el.remove());
@ -739,11 +766,7 @@ document.addEventListener("DOMContentLoaded", function() {
noteButton.classList.add("noteButton")
notesDiv.append(noteButton)
console.log(noteData["title"])
if (noteData["title"] === "") {
console.log(noteData["title"])
console.log("case")
noteData["title"] = "New note"
}
@ -787,10 +810,9 @@ document.addEventListener("DOMContentLoaded", function() {
updateNotes()
newNote.addEventListener("click", () => {
newNote.addEventListener("click", async () => {
let noteName = "New note"
selectLatestNote = true
console.log(selectLatestNote)
// create fake item
document.querySelectorAll(".noteButton").forEach((el) => el.classList.remove("selected"));
@ -802,7 +824,8 @@ document.addEventListener("DOMContentLoaded", function() {
noteButton.classList.add("selected")
noteBox.click()
let encryptedName = CryptoJS.AES.encrypt(noteName, password).toString(CryptoJS.enc.Utf8);
let encryptedName
encryptedName = await encrypt(noteName)
fetch(remote + "/api/newnote", {
method: "POST",
@ -851,22 +874,20 @@ document.addEventListener("DOMContentLoaded", function() {
for (let i in responseData) {
exportNotes.innerText = "Decrypting " + i + "/" + noteCount
let bytes = CryptoJS.AES.decrypt(responseData[i]["title"], password);
responseData[i]["title"] = bytes.toString(CryptoJS.enc.Utf8)
let bytesd = CryptoJS.AES.decrypt(responseData[i]["content"], password);
responseData[i]["content"] = bytesd.toString(CryptoJS.enc.Utf8)
try {
responseData[i]["title"] = await decrypt(responseData[i]["title"])
responseData[i]["content"] = await decrypt(responseData[i]["content"])
} catch (e) {
location.href = "/migrate"
}
}
return responseData
}
async function importNotes(plaintextNotes) {
for (let i in plaintextNotes) {
let originalTitle = plaintextNotes[i]["title"];
plaintextNotes[i]["title"] = CryptoJS.AES.encrypt(originalTitle, password).toString();
let originalContent = plaintextNotes[i]["content"];
plaintextNotes[i]["content"] = CryptoJS.AES.encrypt(originalContent, password).toString();
plaintextNotes[i]["title"] = await encrypt(plaintextNotes[i]["title"])
plaintextNotes[i]["content"] = await encrypt(plaintextNotes[i]["content"])
}
let importNotesFetch = await fetch(remote + "/api/importnotes", {
method: "POST",
@ -948,7 +969,7 @@ document.addEventListener("DOMContentLoaded", function() {
});
if (firstNewVersion()) {
displayError("What's new in Burgernotes 2.0?\nRestyled client\nAdded changing passwords\nAdded importing notes")
displayError("What's new in Burgernotes 2.0?\nRestyled client\nAdded changing passwords\nAdded importing notes\nChange the use of CryptoJS to Native AES GCM\nUse Argon2ID for hashing rather than the SHA family\nAdded a Proof-Of-Work CAPTCHA during signup\nMade the signup and login statuses more descriptive\nFixed various bugs and issues\nAdded markdown notes\nAdded support for uploading photos\nImproved privacy policy to be clearer about what is and isn't added\nRemoved some useless uses of cookies and replaced with localStorage\nFixed the privacy policy not redirecting correctly\nAdded a list of native clients\nMade the client support LibreJS and therefore GNU Icecat")
}
checknetwork()

191
static/js/migrate.js Normal file
View File

@ -0,0 +1,191 @@
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0
if (localStorage.getItem("DONOTSHARE-secretkey") === null || localStorage.getItem("DONOTSHARE-password") === null) {
window.location.replace("/login")
document.body.innerHTML = "Redirecting..."
throw new Error();
}
let remote = localStorage.getItem("homeserverURL")
if (remote == null) {
localStorage.setItem("homeserverURL", "https://notes.hectabit.org")
remote = "https://notes.hectabit.org"
}
let notesPlainText = ""
let information
let backButton
let titleBox
let fileInput
let inputTypeGlobal = 0
document.addEventListener("DOMContentLoaded", function() {
information = document.getElementById("information")
backButton = document.getElementById("backButton")
titleBox = document.getElementById("title")
fileInput = document.getElementById("fileInput")
})
function showInput(inputType) {
inputTypeGlobal = inputType
switch (inputType) {
case 0:
information.innerText = "Welcome to the Burgernotes Migration wizard! Before we begin migration, there are a few things you should know."
titleBox.innerText = "Burgernotes Migrator"
backButton.classList.add("hidden")
break
case 1:
information.innerText = "This migration process may corrupt your data if interrupted. Please ensure you have a stable internet connection. Press continue to download a backup of your notes, just in case."
titleBox.innerText = "Disclaimer"
backButton.classList.remove("hidden")
break
case 2:
information.innerText = "Now that you have a backup of your notes, you can proceed to the next step. Press continue to begin migration."
titleBox.innerText = "Backup Complete"
break
case 3:
information.innerText = "You have successfully migrated to new Burgernotes! Enjoy the more secure and feature-rich experience. Click continue to return to the app."
titleBox.innerText = "Migration Complete"
fileInput.classList.remove("hidden")
break
}
}
function buttonClick() {
switch (inputTypeGlobal) {
case 0:
showInput(1)
break
case 1:
exportNotes().then((data) => {
if (data !== undefined) {
notesPlainText = data
let blob = new Blob([JSON.stringify(data)], {type: "application/json"})
let url = URL.createObjectURL(blob)
let a = document.createElement("a")
a.href = url
a.download = "backup.json"
a.click()
showInput(2)
}
})
break
case 2:
importNotes(notesPlainText).then((status) => {
if (status === 200) {
showInput(3)
} else if (status === -1 || status === -2) {
titleBox.innerText = "Error!"
information.innerText = "An error occurred while migrating your notes. Please try again by pressing continue."
console.log(status)
} else {
titleBox.innerText = "Critical Error!"
information.innerText = "All your notes have been lost. Good thing you have a backup! Press continue to return to the app, so you can import your backup."
inputTypeGlobal = 3
}
})
break
case 3:
window.location.href = "/app"
break
}
}
function back() {
showInput(inputTypeGlobal - 1)
}
async function getKey() {
let password = localStorage.getItem("DONOTSHARE-password")
let salt = new TextEncoder().encode("I love Burgernotes!")
let cryptoKey = await window.crypto.subtle.importKey("raw", new TextEncoder().encode(password), "PBKDF2", false, ["deriveBits", "deriveKey"])
return await window.crypto.subtle.deriveKey({
name: "PBKDF2",
salt,
iterations: 1,
hash: "SHA-512"
}, cryptoKey, {name: "AES-GCM", length: 256}, true, ["encrypt", "decrypt"])
}
function arrayBufferToBase64(buffer) {
const uint8Array = new Uint8Array(buffer);
return btoa(String.fromCharCode.apply(null, uint8Array))
}
async function encrypt(text) {
let cryptoKey = await getKey()
let iv = window.crypto.getRandomValues(new Uint8Array(12))
let encrypted = await window.crypto.subtle.encrypt({
name: "AES-GCM",
iv: iv
}, cryptoKey, new TextEncoder().encode(text))
return btoa(JSON.stringify({
encrypted: arrayBufferToBase64(encrypted),
iv: arrayBufferToBase64(iv)
}))
}
async function importNotes(plaintextNotes) {
try {
for (let i in plaintextNotes) {
plaintextNotes[i]["title"] = await encrypt(plaintextNotes[i]["title"])
plaintextNotes[i]["content"] = await encrypt(plaintextNotes[i]["content"])
}
let purgeNotesFetch = await fetch(remote + "/api/purgenotes", {
method: "POST",
body: JSON.stringify({
"secretKey": localStorage.getItem("DONOTSHARE-secretkey"),
}),
headers: {
"Content-Type": "application/json; charset=UTF-8"
}
})
if (purgeNotesFetch.status !== 200) {
return -2
}
let importNotesFetch = await fetch(remote + "/api/importnotes", {
method: "POST",
body: JSON.stringify({
"secretKey": localStorage.getItem("DONOTSHARE-secretkey"),
"notes": JSON.stringify(plaintextNotes)
}),
headers: {
"Content-Type": "application/json; charset=UTF-8"
}
})
return importNotesFetch.status
} catch (e) {
return -1
}
}
async function exportNotes() {
try {
titleBox.innerText = "Decrypting Notes..."
let oldPasswordFormat = await hashwasm.sha512(prompt("Please enter your password to decrypt your notes."))
let exportNotesFetch = await fetch(remote + "/api/exportnotes", {
method: "POST",
body: JSON.stringify({
secretKey: localStorage.getItem("DONOTSHARE-secretkey")
}),
headers: {
"Content-Type": "application/json; charset=UTF-8"
}
})
let responseData = await exportNotesFetch.json()
for (let i in responseData) {
information.innerText = "Decrypting " + i + "/" + responseData.length
responseData[i]["title"] = CryptoJS.AES.decrypt(responseData[i]["title"], oldPasswordFormat).toString(CryptoJS.enc.Utf8)
responseData[i]["content"] = CryptoJS.AES.decrypt(responseData[i]["content"], oldPasswordFormat).toString(CryptoJS.enc.Utf8)
}
titleBox.innerText = "Notes decrypted!"
information.innerText = "Now if anything goes wrong, you can import this backup to restore your notes in the settings panel."
return responseData
} catch (e) {
titleBox.innerText = "Error!"
information.innerText = "An error occurred while decrypting your notes. Good thing we found out before we started migration! Please try again by pressing continue."
}
}
// @license-end

View File

@ -18,24 +18,18 @@ let statusBox = document.getElementById("statusBox")
let signupButton = document.getElementById("signupButton")
let opButton = document.getElementById("opButton")
async function hashpassold(pass) {
return await hashwasm.argon2id({
password: pass,
salt: await hashwasm.sha512(pass),
parallelism: 1,
iterations: 256,
memorySize: 512,
hashLength: 32,
outputType: "encoded"
})
}
// Leave these variables alone, they are used in the WASM code.
async function hashpass(pass) {
let key = pass
for (let i = 0; i < 128; i++) {
key = await hashwasm.sha3(key)
}
return key
return await hashwasm.argon2id({
password: pass,
salt: new TextEncoder().encode("I munch Burgers!!"),
parallelism: 1,
iterations: 32,
memorySize: 19264,
hashLength: 32,
outputType: "hex"
})
}
function showElements(yesorno) {
@ -44,12 +38,14 @@ function showElements(yesorno) {
passwordBox.classList.add("hidden")
signupButton.classList.add("hidden")
opButton.classList.add("hidden")
inputContainer.classList.add("hidden")
}
else {
usernameBox.classList.remove("hidden")
passwordBox.classList.remove("hidden")
signupButton.classList.remove("hidden")
opButton.classList.remove("hidden")
inputContainer.classList.remove("hidden")
}
}
@ -61,53 +57,98 @@ opButton.addEventListener("click", () => {
window.location.href = "/login"
});
complete = new Event("completed");
window.returnCode = undefined;
window.returnVar = undefined;
// This is for the WASM code to call when it's done. Do not remove it, even if it looks like it's never called.
function WASMComplete() {
window.dispatchEvent(complete);
}
signupButton.addEventListener("click", () => {
async function doStuff() {
let username = usernameBox.value
let password = passwordBox.value
if (username === "") {
statusBox.innerText = "Username required ⚠️"
statusBox.innerText = "Username required!"
return
}
if ((username).length > 20) {
statusBox.innerText = "Username cannot be more than 20 characters ⚠️"
statusBox.innerText = "Username cannot be more than 20 characters!"
return
}
if (password === "") {
statusBox.innerText = "Password required ⚠️"
statusBox.innerText = "Password required!"
return
}
if ((password).length < 8) {
statusBox.innerText = "Password must be at least 8 characters ⚠️"
statusBox.innerText = "Password must be at least 8 characters!"
return
}
showElements(false)
statusBox.innerText = "Creating account, please hold on..."
statusBox.innerText = "Beginning proof-of-work challenge..."
fetch(remote + "/api/signup", {
method: "POST",
body: JSON.stringify({
username: username,
password: await hashpass(password),
legacyPassword: await hashpass(await hashpassold(password))
}),
headers: {
"Content-Type": "application/json; charset=UTF-8",
"X-Burgernotes-Version": "200"
}
/*
* Compiled version of:
* hashcat-wasm (https://concord.hectabit.org/hectabit/hashcat-wasm)
* (c) Arzumify
* @license AGPL-3.0
* Since this is my software, if you use it with proprietary servers, I will make sure you will walk across hot coals (just kidding, probably).
* I'm not kidding about the license though.
* I should stop including comments into JS and possibly minify this code. Oh, well.
*/
const go = new Go();
WebAssembly.instantiateStreaming(fetch("/static/wasm/hashcat.wasm"), go.importObject).then((result) => {
go.run(result.instance);
})
.then((response) => response)
.then((response) => {
async function doStuff() {
window.addEventListener("completed", async () => {
if (window.returnCode === 1) {
statusBox.innerText = "Please do not expose your computer to cosmic rays (an impossible logical event has occurred)."
showElements(true)
return
} else if (window.returnCode === 2) {
statusBox.innerText = "The PoW Challenge has failed. Please try again."
showElements(true)
return
}
statusBox.innerText = "Hashing password..."
let hashedPassword = await hashpass(password)
statusBox.innerText = "Contacting server..."
fetch(remote + "/api/signup", {
method: "POST",
body: JSON.stringify({
username: username,
password: hashedPassword,
stamp: window.returnVar,
}),
headers: {
"Content-Type": "application/json; charset=UTF-8",
}
})
.then((response) => response)
.then(async (response) => {
let responseData = await response.json()
if (response.status === 200) {
statusBox.innerText = "Redirecting...."
statusBox.innerText = "Setting up encryption keys..."
localStorage.setItem("DONOTSHARE-secretkey", responseData["key"])
localStorage.setItem("DONOTSHARE-password", await hashwasm.sha512(password))
localStorage.setItem("DONOTSHARE-password", await hashwasm.argon2id({
password: password,
salt: new TextEncoder().encode("I love Burgernotes!"),
parallelism: 1,
iterations: 32,
memorySize: 19264,
hashLength: 32,
outputType: "hex"
}))
statusBox.innerText = "Welcome!"
await new Promise(r => setTimeout(r, 200))
window.location.href = "/app/"
} else if (response.status === 409) {
statusBox.innerText = "Username already taken!"
@ -115,15 +156,15 @@ signupButton.addEventListener("click", () => {
} else if (response.status === 429) {
statusBox.innerText = "Please don't sign up to new accounts that quickly. If you are using a VPN, please turn it off!"
showElements(true)
} else if (response.status === 500) {
statusBox.innerText = responseData["error"]
showElements(true)
} else {
statusBox.innerText = "Something went wrong! (error code: " + response.status + ")"
statusBox.innerText = "Something went wrong! (error: " + responseData["error"] + ")"
showElements(true)
}
}
doStuff()
});
}
doStuff()
})
})
});
// @license-end

File diff suppressed because one or more lines are too long

567
static/js/wasm_exec.js Normal file
View File

@ -0,0 +1,567 @@
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
/*
* wasm_exec (https://github.com/golang/go)
* (c) The Go Authors
* @license BSD-3-Clause
*/
"use strict";
(() => {
const enosys = () => {
const err = new Error("not implemented");
err.code = "ENOSYS";
return err;
};
if (!globalThis.fs) {
let outputBuf = "";
globalThis.fs = {
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
writeSync(fd, buf) {
outputBuf += decoder.decode(buf);
const nl = outputBuf.lastIndexOf("\n");
if (nl != -1) {
console.log(outputBuf.substring(0, nl));
outputBuf = outputBuf.substring(nl + 1);
}
return buf.length;
},
write(fd, buf, offset, length, position, callback) {
if (offset !== 0 || length !== buf.length || position !== null) {
callback(enosys());
return;
}
const n = this.writeSync(fd, buf);
callback(null, n);
},
chmod(path, mode, callback) { callback(enosys()); },
chown(path, uid, gid, callback) { callback(enosys()); },
close(fd, callback) { callback(enosys()); },
fchmod(fd, mode, callback) { callback(enosys()); },
fchown(fd, uid, gid, callback) { callback(enosys()); },
fstat(fd, callback) { callback(enosys()); },
fsync(fd, callback) { callback(null); },
ftruncate(fd, length, callback) { callback(enosys()); },
lchown(path, uid, gid, callback) { callback(enosys()); },
link(path, link, callback) { callback(enosys()); },
lstat(path, callback) { callback(enosys()); },
mkdir(path, perm, callback) { callback(enosys()); },
open(path, flags, mode, callback) { callback(enosys()); },
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
readdir(path, callback) { callback(enosys()); },
readlink(path, callback) { callback(enosys()); },
rename(from, to, callback) { callback(enosys()); },
rmdir(path, callback) { callback(enosys()); },
stat(path, callback) { callback(enosys()); },
symlink(path, link, callback) { callback(enosys()); },
truncate(path, length, callback) { callback(enosys()); },
unlink(path, callback) { callback(enosys()); },
utimes(path, atime, mtime, callback) { callback(enosys()); },
};
}
if (!globalThis.process) {
globalThis.process = {
getuid() { return -1; },
getgid() { return -1; },
geteuid() { return -1; },
getegid() { return -1; },
getgroups() { throw enosys(); },
pid: -1,
ppid: -1,
umask() { throw enosys(); },
cwd() { throw enosys(); },
chdir() { throw enosys(); },
}
}
if (!globalThis.crypto) {
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
}
if (!globalThis.performance) {
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
}
if (!globalThis.TextEncoder) {
throw new Error("globalThis.TextEncoder is not available, polyfill required");
}
if (!globalThis.TextDecoder) {
throw new Error("globalThis.TextDecoder is not available, polyfill required");
}
const encoder = new TextEncoder("utf-8");
const decoder = new TextDecoder("utf-8");
globalThis.Go = class {
constructor() {
this.argv = ["js"];
this.env = {};
this.exit = (code) => {
if (code !== 0) {
console.warn("exit code:", code);
}
};
this._exitPromise = new Promise((resolve) => {
this._resolveExitPromise = resolve;
});
this._pendingEvent = null;
this._scheduledTimeouts = new Map();
this._nextCallbackTimeoutID = 1;
const setInt64 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
}
const setInt32 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
}
const getInt64 = (addr) => {
const low = this.mem.getUint32(addr + 0, true);
const high = this.mem.getInt32(addr + 4, true);
return low + high * 4294967296;
}
const loadValue = (addr) => {
const f = this.mem.getFloat64(addr, true);
if (f === 0) {
return undefined;
}
if (!isNaN(f)) {
return f;
}
const id = this.mem.getUint32(addr, true);
return this._values[id];
}
const storeValue = (addr, v) => {
const nanHead = 0x7FF80000;
if (typeof v === "number" && v !== 0) {
if (isNaN(v)) {
this.mem.setUint32(addr + 4, nanHead, true);
this.mem.setUint32(addr, 0, true);
return;
}
this.mem.setFloat64(addr, v, true);
return;
}
if (v === undefined) {
this.mem.setFloat64(addr, 0, true);
return;
}
let id = this._ids.get(v);
if (id === undefined) {
id = this._idPool.pop();
if (id === undefined) {
id = this._values.length;
}
this._values[id] = v;
this._goRefCounts[id] = 0;
this._ids.set(v, id);
}
this._goRefCounts[id]++;
let typeFlag = 0;
switch (typeof v) {
case "object":
if (v !== null) {
typeFlag = 1;
}
break;
case "string":
typeFlag = 2;
break;
case "symbol":
typeFlag = 3;
break;
case "function":
typeFlag = 4;
break;
}
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
this.mem.setUint32(addr, id, true);
}
const loadSlice = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
}
const loadSliceOfValues = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
const a = new Array(len);
for (let i = 0; i < len; i++) {
a[i] = loadValue(array + i * 8);
}
return a;
}
const loadString = (addr) => {
const saddr = getInt64(addr + 0);
const len = getInt64(addr + 8);
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
}
const timeOrigin = Date.now() - performance.now();
this.importObject = {
_gotest: {
add: (a, b) => a + b,
},
gojs: {
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
// This changes the SP, thus we have to update the SP used by the imported function.
// func wasmExit(code int32)
"runtime.wasmExit": (sp) => {
sp >>>= 0;
const code = this.mem.getInt32(sp + 8, true);
this.exited = true;
delete this._inst;
delete this._values;
delete this._goRefCounts;
delete this._ids;
delete this._idPool;
this.exit(code);
},
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
"runtime.wasmWrite": (sp) => {
sp >>>= 0;
const fd = getInt64(sp + 8);
const p = getInt64(sp + 16);
const n = this.mem.getInt32(sp + 24, true);
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
},
// func resetMemoryDataView()
"runtime.resetMemoryDataView": (sp) => {
sp >>>= 0;
this.mem = new DataView(this._inst.exports.mem.buffer);
},
// func nanotime1() int64
"runtime.nanotime1": (sp) => {
sp >>>= 0;
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
},
// func walltime() (sec int64, nsec int32)
"runtime.walltime": (sp) => {
sp >>>= 0;
const msec = (new Date).getTime();
setInt64(sp + 8, msec / 1000);
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
},
// func scheduleTimeoutEvent(delay int64) int32
"runtime.scheduleTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this._nextCallbackTimeoutID;
this._nextCallbackTimeoutID++;
this._scheduledTimeouts.set(id, setTimeout(
() => {
this._resume();
while (this._scheduledTimeouts.has(id)) {
// for some reason Go failed to register the timeout event, log and try again
// (temporary workaround for https://github.com/golang/go/issues/28975)
console.warn("scheduleTimeoutEvent: missed timeout event");
this._resume();
}
},
getInt64(sp + 8),
));
this.mem.setInt32(sp + 16, id, true);
},
// func clearTimeoutEvent(id int32)
"runtime.clearTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this.mem.getInt32(sp + 8, true);
clearTimeout(this._scheduledTimeouts.get(id));
this._scheduledTimeouts.delete(id);
},
// func getRandomData(r []byte)
"runtime.getRandomData": (sp) => {
sp >>>= 0;
crypto.getRandomValues(loadSlice(sp + 8));
},
// func finalizeRef(v ref)
"syscall/js.finalizeRef": (sp) => {
sp >>>= 0;
const id = this.mem.getUint32(sp + 8, true);
this._goRefCounts[id]--;
if (this._goRefCounts[id] === 0) {
const v = this._values[id];
this._values[id] = null;
this._ids.delete(v);
this._idPool.push(id);
}
},
// func stringVal(value string) ref
"syscall/js.stringVal": (sp) => {
sp >>>= 0;
storeValue(sp + 24, loadString(sp + 8));
},
// func valueGet(v ref, p string) ref
"syscall/js.valueGet": (sp) => {
sp >>>= 0;
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 32, result);
},
// func valueSet(v ref, p string, x ref)
"syscall/js.valueSet": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
},
// func valueDelete(v ref, p string)
"syscall/js.valueDelete": (sp) => {
sp >>>= 0;
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
},
// func valueIndex(v ref, i int) ref
"syscall/js.valueIndex": (sp) => {
sp >>>= 0;
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
},
// valueSetIndex(v ref, i int, x ref)
"syscall/js.valueSetIndex": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
},
// func valueCall(v ref, m string, args []ref) (ref, bool)
"syscall/js.valueCall": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const m = Reflect.get(v, loadString(sp + 16));
const args = loadSliceOfValues(sp + 32);
const result = Reflect.apply(m, v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, result);
this.mem.setUint8(sp + 64, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, err);
this.mem.setUint8(sp + 64, 0);
}
},
// func valueInvoke(v ref, args []ref) (ref, bool)
"syscall/js.valueInvoke": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.apply(v, undefined, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueNew(v ref, args []ref) (ref, bool)
"syscall/js.valueNew": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.construct(v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueLength(v ref) int
"syscall/js.valueLength": (sp) => {
sp >>>= 0;
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
},
// valuePrepareString(v ref) (ref, int)
"syscall/js.valuePrepareString": (sp) => {
sp >>>= 0;
const str = encoder.encode(String(loadValue(sp + 8)));
storeValue(sp + 16, str);
setInt64(sp + 24, str.length);
},
// valueLoadString(v ref, b []byte)
"syscall/js.valueLoadString": (sp) => {
sp >>>= 0;
const str = loadValue(sp + 8);
loadSlice(sp + 16).set(str);
},
// func valueInstanceOf(v ref, t ref) bool
"syscall/js.valueInstanceOf": (sp) => {
sp >>>= 0;
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
},
// func copyBytesToGo(dst []byte, src ref) (int, bool)
"syscall/js.copyBytesToGo": (sp) => {
sp >>>= 0;
const dst = loadSlice(sp + 8);
const src = loadValue(sp + 32);
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
// func copyBytesToJS(dst ref, src []byte) (int, bool)
"syscall/js.copyBytesToJS": (sp) => {
sp >>>= 0;
const dst = loadValue(sp + 8);
const src = loadSlice(sp + 16);
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
"debug": (value) => {
console.log(value);
},
}
};
}
async run(instance) {
if (!(instance instanceof WebAssembly.Instance)) {
throw new Error("Go.run: WebAssembly.Instance expected");
}
this._inst = instance;
this.mem = new DataView(this._inst.exports.mem.buffer);
this._values = [ // JS values that Go currently has references to, indexed by reference id
NaN,
0,
null,
true,
false,
globalThis,
this,
];
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
this._ids = new Map([ // mapping from JS values to reference ids
[0, 1],
[null, 2],
[true, 3],
[false, 4],
[globalThis, 5],
[this, 6],
]);
this._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
let offset = 4096;
const strPtr = (str) => {
const ptr = offset;
const bytes = encoder.encode(str + "\0");
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
offset += bytes.length;
if (offset % 8 !== 0) {
offset += 8 - (offset % 8);
}
return ptr;
};
const argc = this.argv.length;
const argvPtrs = [];
this.argv.forEach((arg) => {
argvPtrs.push(strPtr(arg));
});
argvPtrs.push(0);
const keys = Object.keys(this.env).sort();
keys.forEach((key) => {
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
});
argvPtrs.push(0);
const argv = offset;
argvPtrs.forEach((ptr) => {
this.mem.setUint32(offset, ptr, true);
this.mem.setUint32(offset + 4, 0, true);
offset += 8;
});
// The linker guarantees global data starts from at least wasmMinDataAddr.
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
const wasmMinDataAddr = 4096 + 8192;
if (offset >= wasmMinDataAddr) {
throw new Error("total length of command line and environment variables exceeds limit");
}
this._inst.exports.run(argc, argv);
if (this.exited) {
this._resolveExitPromise();
}
await this._exitPromise;
}
_resume() {
if (this.exited) {
throw new Error("Go program has already exited");
}
this._inst.exports.resume();
if (this.exited) {
this._resolveExitPromise();
}
}
_makeFuncWrapper(id) {
const go = this;
return function () {
const event = { id: id, this: this, args: arguments };
go._pendingEvent = event;
go._resume();
return event.result;
};
}
}
})();
// @license-end