cred-vault/app.js
2026-03-14 21:38:19 -04:00

266 lines
7.1 KiB
JavaScript

const $ = van.tags;
const writeKey = "vault";
const encrypt =(secret)=>
{
const serialized = JSON.stringify(vault.val);
// todo add actual encryption here using secret
const encrypted = serialized+"|"+secret;
//
localStorage.setItem(writeKey, encrypted);
vaultSerialized.val = encrypted;
};
const decrypt =(secret)=>
{
try
{
const encrypted = localStorage.getItem(writeKey)||"";
vaultSerialized.val = encrypted;
// todo add actual decryption here using secret
const [decrypted, actual] = encrypted.split("|");
if(actual !== secret){return false;}
//
vault.val = JSON.parse(decrypted);
return true;
}
catch(e)
{
console.log("decrypt error", e);
return false;
}
};
const copyToClipboard =(str)=>
{
alert("copied to clipboard")
// todo copy string to clipboard;
}
/** @typedef {[domain:string, username:string, password:string, codegen:string, timestamp:number, custom_kvp:Record<string, string>]} CredSet */
const vault = van.state(/**@type{CredSet[]}*/([]));
const vaultSerialized = van.state("");
const password = van.state("");
const attemptsFailed = van.state(0);
const preexisting = (localStorage.getItem(writeKey)||"");
const editCred = van.state(/**@type{null|CredSet}*/null);
const editInd = van.state(0);
const PasswordChange =()=>
{
if(preexisting && attemptsFailed.val > -1)
{
const success = decrypt(password.val);
if(success)
{
attemptsFailed.val = -1;
}
else
{
attemptsFailed.val = attemptsFailed.val+1;
return;
}
}
encrypt(password.val);
console.log("password change");
};
/** @type {(credSet:CredSet)=>void} */
const VaultAdd =(credSet)=>{
const clone = [...vault.rawVal];
let i;
for(i=0; i<clone.length; i++)
{
if(credSet[0] < clone[i][0])
{
break;
}
}
clone.splice(i, 0, credSet);
vault.val = clone;
encrypt(password.val);
};
const Components = {
Form()
{
/** @type {(label:string, control:HTMLInputElement)=>HTMLFieldSetElement} */
const block =(label, control)=>
{
control.id = label;
return $.fieldset(
$.label({for:label}, label),
control
)
};
const inputSite = $.input();
const inputUser = $.input();
const inputPass = $.input();
const inputCode = $.input();
return $.form(
{
onsubmit(e){
e.preventDefault();
VaultAdd([inputSite.value, inputUser.value, inputPass.value, inputCode.value, new Date().getTime(), {}])
}
},
block("Site", inputSite),
block("User", inputUser),
block("Pass", inputPass),
block("Code", inputCode),
$.button("Add")
)
},
/**@type {(label:string, cred:CredSet, credIndex:Util.Indices<CredSet>)=>HTMLFieldSetElement} */
SingleFieldForm(label, cred, credIndex){
const edit = van.state(false);
const show = van.state(false);
return $.fieldset(
$.label(
{onclick(){
if(!edit.rawVal)
{
copyToClipboard()
}
}},
label),
$.button({onclick(){editCred.val = cred; editInd.val = credIndex;}}, "Edit")
//()=> $.button({onclick(){show.val = !show.val}}, show.val ? "Hide":"Show"),
//()=> $.button({onclick(){edit.val = !edit.val}}, edit.val ? "Cancel":"Edit"),
//()=> edit.val ? $.input({type:"text", value:cred[credIndex].toString()}) : null,
//()=> edit.val ? $.button({onclick(){}}, "Save") : null,
)
},
/**@type {(cred:CredSet)=>HTMLDivElement} */
ExistingForm(cred)
{
return $.div(
$.details(
$.summary(
$.h2(cred[0]),
),
Components.SingleFieldForm("User", cred, 1),
Components.SingleFieldForm("Pass", cred, 2),
$.details(
$.summary("Delete this record"),
$.button("Delete")
)
)
)
},
Root()
{
const input = $.input({id:"passwordField", value:password.val});
const status =()=>
{
let message = "";
let showVault = false;
let button = "Re-encrypt";
if(preexisting)
{
if(attemptsFailed.val > -1)
{
message = `Existing session data found. Enter the correct password to recover it.`;
button = `Recover`;
if(attemptsFailed.val > 0)
{
message += ` Password Incorrect (${attemptsFailed.val} attempts).`;
}
}
else
{
message = `Password changes will re-encrypt your active session data.`;
showVault = true;
}
}
else
{
message = `You are starting a new session.`;
button = `Encrypt`;
showVault = true;
}
return [message, button, showVault]
}
const [message, buttonText, showVault] = status();
return $.div(
$.h1(message),
$.label({for:input.id}, "Password"),
()=>input,
$.button({onclick(){
password.val = input.value;
PasswordChange();
}}, buttonText),
$.div(
$.h3("debug zone"),
$.ul(
$.li($.strong("preexisting "), preexisting),
$.li($.strong("attemptsFailed "), attemptsFailed.val),
$.li($.strong("vaultSerialized "), vaultSerialized.val),
$.li($.strong("password "), password.val),
)
),
$.hr(),
showVault ? Components.Vault() : null
);
},
Modal()
{
if(editCred.val)
{
const input = $.input({value:editCred.val[editInd.val]});
return $.div(
input,
$.button({onclick(){
editCred.val[editInd.val] = input.value;
editCred.val = null;
encrypt(password.val);
}}, "Save"),
$.button({onclick(){
editCred.val = null;
}}, "Cancel")
)
}
else
{
return "no compoent"
}
},
Vault()
{
return $.div(
$.h2("Vault"),
Components.Form,
Components.Modal,
vault.val.map(Components.ExistingForm)
);
}
}
van.add(document.body, Components.Root);