Work on cryptokeys
This commit is contained in:
parent
56a6e7145f
commit
87a802e210
39
key.go
39
key.go
@ -14,7 +14,7 @@ type Key struct {
|
|||||||
Key string
|
Key string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (session Session) Keys() (keys []Key, err error) {
|
func (session Session) Keys() (keys []Key, err error) {// {{{
|
||||||
var rows *sqlx.Rows
|
var rows *sqlx.Rows
|
||||||
if rows, err = db.Queryx(`SELECT * FROM crypto_key WHERE user_id=$1`, session.UserID); err != nil {
|
if rows, err = db.Queryx(`SELECT * FROM crypto_key WHERE user_id=$1`, session.UserID); err != nil {
|
||||||
return
|
return
|
||||||
@ -30,5 +30,42 @@ func (session Session) Keys() (keys []Key, err error) {
|
|||||||
keys = append(keys, key)
|
keys = append(keys, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}// }}}
|
||||||
|
func (session Session) KeyCreate(description, keyEncoded string) (key Key, err error) {// {{{
|
||||||
|
var row *sqlx.Rows
|
||||||
|
if row, err = db.Queryx(
|
||||||
|
`INSERT INTO crypto_key(user_id, description, key)
|
||||||
|
VALUES($1, $2, $3)
|
||||||
|
RETURNING *`,
|
||||||
|
session.UserID,
|
||||||
|
description,
|
||||||
|
keyEncoded,
|
||||||
|
); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer row.Close()
|
||||||
|
|
||||||
|
key = Key{}
|
||||||
|
for row.Next() {
|
||||||
|
if err = row.StructScan(&key); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}// }}}
|
||||||
|
func (session Session) KeyCounter() (counter int64, err error) {// {{{
|
||||||
|
var rows *sqlx.Rows
|
||||||
|
rows, err = db.Queryx(`SELECT nextval('aes_ccm_counter') AS counter`)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
rows.Next()
|
||||||
|
err = rows.Scan(&counter)
|
||||||
|
return
|
||||||
|
}// }}}
|
||||||
|
|
||||||
|
// vim: foldmethod=marker
|
||||||
|
59
main.go
59
main.go
@ -22,7 +22,7 @@ import (
|
|||||||
|
|
||||||
const VERSION = "v0.2.2";
|
const VERSION = "v0.2.2";
|
||||||
const LISTEN_HOST = "0.0.0.0";
|
const LISTEN_HOST = "0.0.0.0";
|
||||||
const DB_SCHEMA = 7
|
const DB_SCHEMA = 10
|
||||||
|
|
||||||
var (
|
var (
|
||||||
flagPort int
|
flagPort int
|
||||||
@ -77,6 +77,8 @@ func main() {// {{{
|
|||||||
http.HandleFunc("/node/upload", nodeUpload)
|
http.HandleFunc("/node/upload", nodeUpload)
|
||||||
http.HandleFunc("/node/download", nodeDownload)
|
http.HandleFunc("/node/download", nodeDownload)
|
||||||
http.HandleFunc("/key/retrieve", keyRetrieve)
|
http.HandleFunc("/key/retrieve", keyRetrieve)
|
||||||
|
http.HandleFunc("/key/create", keyCreate)
|
||||||
|
http.HandleFunc("/key/counter", keyCounter)
|
||||||
http.HandleFunc("/ws", websocketHandler)
|
http.HandleFunc("/ws", websocketHandler)
|
||||||
http.HandleFunc("/", staticHandler)
|
http.HandleFunc("/", staticHandler)
|
||||||
|
|
||||||
@ -288,13 +290,14 @@ func nodeUpdate(w http.ResponseWriter, r *http.Request) {// {{{
|
|||||||
req := struct {
|
req := struct {
|
||||||
NodeID int
|
NodeID int
|
||||||
Content string
|
Content string
|
||||||
|
CryptoKeyID int
|
||||||
}{}
|
}{}
|
||||||
if err = parseRequest(r, &req); err != nil {
|
if err = parseRequest(r, &req); err != nil {
|
||||||
responseError(w, err)
|
responseError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = session.UpdateNode(req.NodeID, req.Content)
|
err = session.UpdateNode(req.NodeID, req.Content, req.CryptoKeyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
responseError(w, err)
|
responseError(w, err)
|
||||||
return
|
return
|
||||||
@ -562,6 +565,58 @@ func keyRetrieve(w http.ResponseWriter, r *http.Request) {// {{{
|
|||||||
"Keys": keys,
|
"Keys": keys,
|
||||||
})
|
})
|
||||||
}// }}}
|
}// }}}
|
||||||
|
func keyCreate(w http.ResponseWriter, r *http.Request) {// {{{
|
||||||
|
log.Println("/key/create")
|
||||||
|
var err error
|
||||||
|
var session Session
|
||||||
|
|
||||||
|
if session, _, err = ValidateSession(r, true); err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := struct {
|
||||||
|
Description string
|
||||||
|
Key string
|
||||||
|
}{}
|
||||||
|
if err = parseRequest(r, &req); err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := session.KeyCreate(req.Description, req.Key)
|
||||||
|
if err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData(w, map[string]interface{}{
|
||||||
|
"OK": true,
|
||||||
|
"Key": key,
|
||||||
|
})
|
||||||
|
}// }}}
|
||||||
|
func keyCounter(w http.ResponseWriter, r *http.Request) {// {{{
|
||||||
|
log.Println("/key/counter")
|
||||||
|
var err error
|
||||||
|
var session Session
|
||||||
|
|
||||||
|
if session, _, err = ValidateSession(r, true); err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
counter, err := session.KeyCounter()
|
||||||
|
if err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData(w, map[string]interface{}{
|
||||||
|
"OK": true,
|
||||||
|
// Javascript uses int32, thus getting a string counter for Javascript BigInt to parse.
|
||||||
|
"Counter": strconv.FormatInt(counter, 10),
|
||||||
|
})
|
||||||
|
}// }}}
|
||||||
|
|
||||||
func newTemplate(requestPath string) (tmpl *template.Template, err error) {// {{{
|
func newTemplate(requestPath string) (tmpl *template.Template, err error) {// {{{
|
||||||
// Append index.html if needed for further reading of the file
|
// Append index.html if needed for further reading of the file
|
||||||
|
21
node.go
21
node.go
@ -12,6 +12,7 @@ type Node struct {
|
|||||||
ID int
|
ID int
|
||||||
UserID int `db:"user_id"`
|
UserID int `db:"user_id"`
|
||||||
ParentID int `db:"parent_id"`
|
ParentID int `db:"parent_id"`
|
||||||
|
CryptoKeyID int `db:"crypto_key_id"`
|
||||||
Name string
|
Name string
|
||||||
Content string
|
Content string
|
||||||
Updated time.Time
|
Updated time.Time
|
||||||
@ -138,6 +139,7 @@ func (session Session) Node(nodeID int) (node Node, err error) {// {{{
|
|||||||
id,
|
id,
|
||||||
user_id,
|
user_id,
|
||||||
COALESCE(parent_id, 0) AS parent_id,
|
COALESCE(parent_id, 0) AS parent_id,
|
||||||
|
COALESCE(crypto_key_id, 0) AS crypto_key_id,
|
||||||
name,
|
name,
|
||||||
content,
|
content,
|
||||||
0 AS level
|
0 AS level
|
||||||
@ -152,6 +154,7 @@ func (session Session) Node(nodeID int) (node Node, err error) {// {{{
|
|||||||
n.id,
|
n.id,
|
||||||
n.user_id,
|
n.user_id,
|
||||||
n.parent_id,
|
n.parent_id,
|
||||||
|
COALESCE(n.crypto_key_id, 0) AS crypto_key_id,
|
||||||
n.name,
|
n.name,
|
||||||
'' AS content,
|
'' AS content,
|
||||||
r.level + 1 AS level
|
r.level + 1 AS level
|
||||||
@ -188,6 +191,7 @@ func (session Session) Node(nodeID int) (node Node, err error) {// {{{
|
|||||||
node.ID = row.ID
|
node.ID = row.ID
|
||||||
node.UserID = row.UserID
|
node.UserID = row.UserID
|
||||||
node.ParentID = row.ParentID
|
node.ParentID = row.ParentID
|
||||||
|
node.CryptoKeyID = row.CryptoKeyID
|
||||||
node.Name = row.Name
|
node.Name = row.Name
|
||||||
node.Content = row.Content
|
node.Content = row.Content
|
||||||
node.Complete = true
|
node.Complete = true
|
||||||
@ -198,6 +202,7 @@ func (session Session) Node(nodeID int) (node Node, err error) {// {{{
|
|||||||
ID: row.ID,
|
ID: row.ID,
|
||||||
UserID: row.UserID,
|
UserID: row.UserID,
|
||||||
ParentID: row.ParentID,
|
ParentID: row.ParentID,
|
||||||
|
CryptoKeyID: row.CryptoKeyID,
|
||||||
Name: row.Name,
|
Name: row.Name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -281,13 +286,23 @@ func (session Session) CreateNode(parentID int, name string) (node Node, err err
|
|||||||
node.Crumbs, err = session.NodeCrumbs(node.ID)
|
node.Crumbs, err = session.NodeCrumbs(node.ID)
|
||||||
return
|
return
|
||||||
}// }}}
|
}// }}}
|
||||||
func (session Session) UpdateNode(nodeID int, content string) (err error) {// {{{
|
func (session Session) UpdateNode(nodeID int, content string, cryptoKeyID int) (err error) {// {{{
|
||||||
_, err = db.Exec(`
|
_, err = db.Exec(`
|
||||||
UPDATE node SET content = $1 WHERE user_id = $2 AND id = $3
|
UPDATE node
|
||||||
|
SET
|
||||||
|
content = $1,
|
||||||
|
crypto_key_id = CASE $2::int
|
||||||
|
WHEN 0 THEN NULL
|
||||||
|
ELSE $2
|
||||||
|
END
|
||||||
|
WHERE
|
||||||
|
id = $3 AND
|
||||||
|
user_id = $4
|
||||||
`,
|
`,
|
||||||
content,
|
content,
|
||||||
session.UserID,
|
cryptoKeyID,
|
||||||
nodeID,
|
nodeID,
|
||||||
|
session.UserID,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}// }}}
|
}// }}}
|
||||||
|
2
sql/0008.sql
Normal file
2
sql/0008.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE public.node ADD crypto_key_id int4 NULL;
|
||||||
|
ALTER TABLE public.node ADD CONSTRAINT crypto_key_fk FOREIGN KEY (crypto_key_id) REFERENCES public.crypto_key(id) ON DELETE RESTRICT ON UPDATE RESTRICT;
|
1
sql/0009.sql
Normal file
1
sql/0009.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
CREATE SEQUENCE aes_ccm_counter AS int8 INCREMENT BY 1 NO CYCLE;
|
1
sql/0010.sql
Normal file
1
sql/0010.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE public.crypto_key ADD CONSTRAINT crypto_user_description_un UNIQUE (user_id, description);
|
@ -20,14 +20,15 @@ body {
|
|||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
font-size: 1em;
|
font-size: 1.5em;
|
||||||
}
|
}
|
||||||
h2 {
|
h2 {
|
||||||
margin-top: 32px;
|
margin-top: 32px;
|
||||||
font-size: 0.9em;
|
font-size: 1.25em;
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
|
padding: 6px;
|
||||||
}
|
}
|
||||||
#blackout {
|
#blackout {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -76,7 +77,6 @@ button {
|
|||||||
}
|
}
|
||||||
#upload input {
|
#upload input {
|
||||||
border: 1px solid #000;
|
border: 1px solid #000;
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
}
|
||||||
#upload .files {
|
#upload .files {
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -274,6 +274,9 @@ header .menu {
|
|||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
}
|
}
|
||||||
|
#node-content.encrypted {
|
||||||
|
color: #a00;
|
||||||
|
}
|
||||||
.node-content {
|
.node-content {
|
||||||
grid-area: content;
|
grid-area: content;
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
@ -349,7 +352,6 @@ header .menu {
|
|||||||
grid-template-columns: 1fr min-content;
|
grid-template-columns: 1fr min-content;
|
||||||
grid-gap: 8px 16px;
|
grid-gap: 8px 16px;
|
||||||
color: #444;
|
color: #444;
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
}
|
||||||
#file-section .files .filename {
|
#file-section .files .filename {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@ -373,11 +375,10 @@ header .menu {
|
|||||||
}
|
}
|
||||||
#keys .key-list {
|
#keys .key-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: min-content 1fr min-content;
|
grid-template-columns: min-content min-content min-content;
|
||||||
grid-gap: 12px 12px;
|
grid-gap: 12px 12px;
|
||||||
align-items: end;
|
align-items: end;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
}
|
||||||
#keys .key-list .status {
|
#keys .key-list .status {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -396,12 +397,15 @@ header .menu {
|
|||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
#keys .key-list .view {
|
#keys .key-list .view {
|
||||||
white-space: nowrap;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
margin-left: 16px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
#keys .key-list .hex-key {
|
#keys .key-list .hex-key {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
@ -409,6 +413,30 @@ header .menu {
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
#key-create .fields {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-gap: 16px;
|
||||||
|
max-width: 48ex;
|
||||||
|
}
|
||||||
|
#key-create .fields #key-description {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
font-size: 1em;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
#key-create .fields #key-key {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
height: 4em;
|
||||||
|
resize: none;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
#key-create .fields #key-pass1,
|
||||||
|
#key-create .fields #key-pass2 {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
#key-create .fields button.generate {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
.layout-tree {
|
.layout-tree {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas: "header header" "tree crumbs" "tree child-nodes" "tree name" "tree content" "tree files" "tree blank";
|
grid-template-areas: "header header" "tree crumbs" "tree child-nodes" "tree name" "tree content" "tree files" "tree blank";
|
||||||
|
46
static/images/padlock-black.svg
Normal file
46
static/images/padlock-black.svg
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
fill="#000000"
|
||||||
|
height="800"
|
||||||
|
width="557.57574"
|
||||||
|
version="1.1"
|
||||||
|
id="Layer_1"
|
||||||
|
viewBox="0 0 229.99999 330"
|
||||||
|
xml:space="preserve"
|
||||||
|
sodipodi:docname="padlock-black.svg"
|
||||||
|
inkscape:version="1.2.1 (9c6d41e, 2022-07-14)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
id="defs6955" /><sodipodi:namedview
|
||||||
|
id="namedview6953"
|
||||||
|
pagecolor="#efb591"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="1.34875"
|
||||||
|
inkscape:cx="36.329935"
|
||||||
|
inkscape:cy="400.37071"
|
||||||
|
inkscape:window-width="2556"
|
||||||
|
inkscape:window-height="1404"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="16"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="Layer_1" />
|
||||||
|
<g
|
||||||
|
id="XMLID_509_"
|
||||||
|
transform="translate(-50)"
|
||||||
|
style="fill:#a02c2c">
|
||||||
|
<path
|
||||||
|
id="XMLID_510_"
|
||||||
|
d="m 65,330 h 200 c 8.284,0 15,-6.716 15,-15 V 145 c 0,-8.284 -6.716,-15 -15,-15 H 250 V 85 C 250,38.131 211.869,0 165,0 118.131,0 80,38.131 80,85 v 45 H 65 c -8.284,0 -15,6.716 -15,15 v 170 c 0,8.284 6.716,15 15,15 z M 180,234.986 V 255 c 0,8.284 -6.716,15 -15,15 -8.284,0 -15,-6.716 -15,-15 v -20.014 c -6.068,-4.565 -10,-11.824 -10,-19.986 0,-13.785 11.215,-25 25,-25 13.785,0 25,11.215 25,25 0,8.162 -3.932,15.421 -10,19.986 z M 110,85 c 0,-30.327 24.673,-55 55,-55 30.327,0 55,24.673 55,55 v 45 H 110 Z"
|
||||||
|
style="fill:#a02c2c" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
@ -1,18 +1,20 @@
|
|||||||
export default class Crypto {
|
export default class Crypto {
|
||||||
constructor(key) {
|
constructor(key) {//{{{
|
||||||
|
if(key === null)
|
||||||
|
throw new Error("No key provided")
|
||||||
|
|
||||||
if(typeof key === 'string')
|
if(typeof key === 'string')
|
||||||
this.key = sjcl.codec.base64.toBits(base64_key)
|
this.key = sjcl.codec.base64.toBits(base64_key)
|
||||||
else
|
else
|
||||||
this.key = key
|
this.key = key
|
||||||
|
|
||||||
this.aes = new sjcl.cipher.aes(this.key)
|
this.aes = new sjcl.cipher.aes(this.key)
|
||||||
}
|
}//}}}
|
||||||
|
|
||||||
static generate_key() {
|
static generate_key() {//{{{
|
||||||
return sjcl.random.randomWords(8)
|
return sjcl.random.randomWords(8)
|
||||||
}
|
}//}}}
|
||||||
|
static pass_to_key(pass, salt = null) {//{{{
|
||||||
static pass_to_key(pass, salt = null) {
|
|
||||||
if(salt === null)
|
if(salt === null)
|
||||||
salt = sjcl.random.randomWords(4) // 128 bits (16 bytes)
|
salt = sjcl.random.randomWords(4) // 128 bits (16 bytes)
|
||||||
let key = sjcl.misc.pbkdf2(pass, salt, 10000)
|
let key = sjcl.misc.pbkdf2(pass, salt, 10000)
|
||||||
@ -21,9 +23,9 @@ export default class Crypto {
|
|||||||
salt,
|
salt,
|
||||||
key,
|
key,
|
||||||
}
|
}
|
||||||
}
|
}//}}}
|
||||||
|
|
||||||
encrypt(plaintext_data_in_bits, counter, return_encoded = true) {
|
encrypt(plaintext_data_in_bits, counter, return_encoded = true) {//{{{
|
||||||
// 8 bytes of random data, (1 word = 4 bytes) * 2
|
// 8 bytes of random data, (1 word = 4 bytes) * 2
|
||||||
// with 8 bytes of byte encoded counter is used as
|
// with 8 bytes of byte encoded counter is used as
|
||||||
// IV to guarantee a non-repeated IV (which is a catastrophe).
|
// IV to guarantee a non-repeated IV (which is a catastrophe).
|
||||||
@ -51,9 +53,8 @@ export default class Crypto {
|
|||||||
)
|
)
|
||||||
else
|
else
|
||||||
return iv.concat(encrypted)
|
return iv.concat(encrypted)
|
||||||
}
|
}//}}}
|
||||||
|
decrypt(encrypted_base64_data) {//{{{
|
||||||
decrypt(encrypted_base64_data) {
|
|
||||||
try {
|
try {
|
||||||
let encoded = sjcl.codec.base64.toBits(encrypted_base64_data)
|
let encoded = sjcl.codec.base64.toBits(encrypted_base64_data)
|
||||||
let iv = encoded.slice(0, 4) // in words (4 bytes), not bytes
|
let iv = encoded.slice(0, 4) // in words (4 bytes), not bytes
|
||||||
@ -65,5 +66,7 @@ export default class Crypto {
|
|||||||
else
|
else
|
||||||
throw(err)
|
throw(err)
|
||||||
}
|
}
|
||||||
|
}//}}}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// vim: foldmethod=marker
|
||||||
|
@ -7,9 +7,13 @@ const html = htm.bind(h)
|
|||||||
export class Keys extends Component {
|
export class Keys extends Component {
|
||||||
constructor(props) {//{{{
|
constructor(props) {//{{{
|
||||||
super(props)
|
super(props)
|
||||||
this.retrieveKeys()
|
this.state = {
|
||||||
|
create: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
props.nodeui.retrieveKeys()
|
||||||
}//}}}
|
}//}}}
|
||||||
render({ nodeui }) {//{{{
|
render({ nodeui }, { create }) {//{{{
|
||||||
let keys = nodeui.keys.value
|
let keys = nodeui.keys.value
|
||||||
.sort((a,b)=>{
|
.sort((a,b)=>{
|
||||||
if(a.description < b.description) return -1
|
if(a.description < b.description) return -1
|
||||||
@ -20,31 +24,128 @@ export class Keys extends Component {
|
|||||||
html`<${KeyComponent} key=${`key-${key.ID}`} model=${key} />`
|
html`<${KeyComponent} key=${`key-${key.ID}`} model=${key} />`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let createButton = ''
|
||||||
|
let createComponents = ''
|
||||||
|
if(create) {
|
||||||
|
createComponents = html`
|
||||||
|
<div id="key-create">
|
||||||
|
<h2>New key</h2>
|
||||||
|
|
||||||
|
<div class="fields">
|
||||||
|
<input type="text" id="key-description" placeholder="Name" />
|
||||||
|
|
||||||
|
<input type="password" id="key-pass1" placeholder="Password" />
|
||||||
|
<input type="password" id="key-pass2" placeholder="Repeat password" />
|
||||||
|
|
||||||
|
<textarea id="key-key" placeholder="Key"></textarea>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button class="generate" onclick=${()=>this.generateKey()}>Generate</button>
|
||||||
|
<button class="create" onclick=${()=>this.createKey()}>Create</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
} else {
|
||||||
|
createButton = html`<div style="margin-top: 16px;"><button onclick=${()=>this.setState({ create: true })}>Create new key</button></div>`
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div id="keys">
|
<div id="keys">
|
||||||
<h1>Keys</h1>
|
<h1>Encryption keys</h1>
|
||||||
|
<p>
|
||||||
|
Unlock a key by clicking its name. Lock it by clicking it again.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Copy the key and store it in a very secure place to have a way to access notes
|
||||||
|
in case the password is forgotten, or database is corrupted.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>Click "View key" after unlocking it.</p>
|
||||||
|
|
||||||
|
${createButton}
|
||||||
|
${createComponents}
|
||||||
|
|
||||||
|
<h2>Keys</h2>
|
||||||
<div class="key-list">
|
<div class="key-list">
|
||||||
${keys}
|
${keys}
|
||||||
</div>
|
</div>
|
||||||
</div>`
|
</div>`
|
||||||
}//}}}
|
}//}}}
|
||||||
|
|
||||||
retrieveKeys() {//{{{
|
generateKey() {//{{{
|
||||||
window._app.current.request('/key/retrieve', {})
|
let keyTextarea = document.getElementById('key-key')
|
||||||
|
let key = sjcl.codec.hex.fromBits(Crypto.generate_key()).replace(/(....)/g, '$1 ').trim()
|
||||||
|
keyTextarea.value = key
|
||||||
|
}//}}}
|
||||||
|
validateNewKey() {//{{{
|
||||||
|
let keyDescription = document.getElementById('key-description').value
|
||||||
|
let keyTextarea = document.getElementById('key-key').value
|
||||||
|
let pass1 = document.getElementById('key-pass1').value
|
||||||
|
let pass2 = document.getElementById('key-pass2').value
|
||||||
|
|
||||||
|
if(keyDescription.trim() == '')
|
||||||
|
throw new Error('The key has to have a description')
|
||||||
|
|
||||||
|
if(pass1.trim() == '' || pass1.length < 4)
|
||||||
|
throw new Error('The password has to be at least 4 characters long.')
|
||||||
|
|
||||||
|
if(pass1 != pass2)
|
||||||
|
throw new Error(`Passwords doesn't match`)
|
||||||
|
|
||||||
|
let cleanKey = keyTextarea.replace(/\s+/g, '')
|
||||||
|
if(!cleanKey.match(/^[0-9a-f]{64}$/i))
|
||||||
|
throw new Error('Invalid key - has to be 64 characters of 0-9 and A-F')
|
||||||
|
}//}}}
|
||||||
|
createKey() {//{{{
|
||||||
|
try {
|
||||||
|
this.validateNewKey()
|
||||||
|
|
||||||
|
let description = document.getElementById('key-description').value
|
||||||
|
let keyAscii = document.getElementById('key-key').value
|
||||||
|
let pass1 = document.getElementById('key-pass1').value
|
||||||
|
|
||||||
|
// Key in hex taken from user.
|
||||||
|
let actual_key = sjcl.codec.hex.toBits(keyAscii.replace(/\s+/g, ''))
|
||||||
|
|
||||||
|
// Key generated from password, used to encrypt the actual key.
|
||||||
|
let pass_gen = Crypto.pass_to_key(pass1)
|
||||||
|
|
||||||
|
let crypto = new Crypto(pass_gen.key)
|
||||||
|
let encrypted_actual_key = crypto.encrypt(actual_key, 0x1n, false)
|
||||||
|
|
||||||
|
// Database value is salt + actual key, needed to generate the same key from the password.
|
||||||
|
let db_encoded = sjcl.codec.hex.fromBits(
|
||||||
|
pass_gen.salt.concat(encrypted_actual_key)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create on server.
|
||||||
|
window._app.current.request('/key/create', {
|
||||||
|
description,
|
||||||
|
key: db_encoded,
|
||||||
|
})
|
||||||
.then(res=>{
|
.then(res=>{
|
||||||
this.props.nodeui.keys.value = res.Keys.map(keyData=>new Key(keyData))
|
let key = new Key(res.Key, this.props.nodeui.keyCounter)
|
||||||
|
this.props.nodeui.keys.value = this.props.nodeui.keys.value.concat(key)
|
||||||
})
|
})
|
||||||
.catch(window._app.current.responseError)
|
.catch(window._app.current.responseError)
|
||||||
|
} catch(err) {
|
||||||
|
alert(err.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
}//}}}
|
}//}}}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Key {
|
export class Key {
|
||||||
constructor(data) {//{{{
|
constructor(data, counter_callback) {//{{{
|
||||||
this.ID = data.ID
|
this.ID = data.ID
|
||||||
this.description = data.Description
|
this.description = data.Description
|
||||||
this.unlockedKey = data.Key
|
this.encryptedKey = data.Key
|
||||||
this.key = null
|
this.key = null
|
||||||
|
|
||||||
|
this._counter_cbk = counter_callback
|
||||||
|
|
||||||
let hex_key = window.sessionStorage.getItem(`key-${this.ID}`)
|
let hex_key = window.sessionStorage.getItem(`key-${this.ID}`)
|
||||||
if(hex_key)
|
if(hex_key)
|
||||||
this.key = sjcl.codec.hex.toBits(hex_key)
|
this.key = sjcl.codec.hex.toBits(hex_key)
|
||||||
@ -59,17 +160,26 @@ export class Key {
|
|||||||
window.sessionStorage.removeItem(`key-${this.ID}`)
|
window.sessionStorage.removeItem(`key-${this.ID}`)
|
||||||
}//}}}
|
}//}}}
|
||||||
unlock(password) {//{{{
|
unlock(password) {//{{{
|
||||||
let db = sjcl.codec.hex.toBits(this.unlockedKey)
|
let db = sjcl.codec.hex.toBits(this.encryptedKey)
|
||||||
let salt = db.slice(0, 4)
|
let salt = db.slice(0, 4)
|
||||||
let pass_key = Crypto.pass_to_key(password, salt)
|
let pass_key = Crypto.pass_to_key(password, salt)
|
||||||
let crypto = new Crypto(pass_key.key)
|
let crypto = new Crypto(pass_key.key)
|
||||||
this.key = crypto.decrypt(sjcl.codec.base64.fromBits(db.slice(4)))
|
this.key = crypto.decrypt(sjcl.codec.base64.fromBits(db.slice(4)))
|
||||||
window.sessionStorage.setItem(`key-${this.ID}`, sjcl.codec.hex.fromBits(this.key))
|
window.sessionStorage.setItem(`key-${this.ID}`, sjcl.codec.hex.fromBits(this.key))
|
||||||
}//}}}
|
}//}}}
|
||||||
|
async counter() {//{{{
|
||||||
|
return this._counter_cbk()
|
||||||
|
}//}}}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class KeyComponent extends Component {
|
export class KeyComponent extends Component {
|
||||||
render({ model }) {//{{{
|
constructor({ model }) {//{{{
|
||||||
|
super({ model })
|
||||||
|
this.state = {
|
||||||
|
show_key: false,
|
||||||
|
}
|
||||||
|
}//}}}
|
||||||
|
render({ model }, { show_key }) {//{{{
|
||||||
let status = ''
|
let status = ''
|
||||||
switch(model.status()) {
|
switch(model.status()) {
|
||||||
case 'locked':
|
case 'locked':
|
||||||
@ -81,9 +191,24 @@ export class KeyComponent extends Component {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let hex_key = ''
|
||||||
|
if(show_key) {
|
||||||
|
if(model.status() == 'locked')
|
||||||
|
hex_key = html`<div class="hex-key">Unlock key first</div>`
|
||||||
|
else {
|
||||||
|
let key = sjcl.codec.hex.fromBits(model.key)
|
||||||
|
key = key.replace(/(....)/g, "$1 ").trim()
|
||||||
|
hex_key = html`<div class="hex-key">${key}</div>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let unlocked = model.status()=='unlocked'
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="status" onclick=${()=>this.toggle()}>${status}</div>
|
<div class="status" onclick=${()=>this.toggle()}>${status}</div>
|
||||||
<div class="description" onclick=${()=>this.toggle()}>${model.description}</div>
|
<div class="description" onclick=${()=>this.toggle()}>${model.description}</div>
|
||||||
|
<div class="view" onclick=${()=>this.toggleViewKey()}>${unlocked ? 'View key' : ''}</div>
|
||||||
|
${hex_key}
|
||||||
`
|
`
|
||||||
}//}}}
|
}//}}}
|
||||||
toggle() {//{{{
|
toggle() {//{{{
|
||||||
@ -108,6 +233,9 @@ export class KeyComponent extends Component {
|
|||||||
alert(err)
|
alert(err)
|
||||||
}
|
}
|
||||||
}//}}}
|
}//}}}
|
||||||
|
toggleViewKey() {//{{{
|
||||||
|
this.setState({ show_key: !this.state.show_key })
|
||||||
|
}//}}}
|
||||||
}
|
}
|
||||||
|
|
||||||
// vim: foldmethod=marker
|
// vim: foldmethod=marker
|
||||||
|
@ -2,11 +2,12 @@ import { h, Component, createRef } from 'preact'
|
|||||||
import htm from 'htm'
|
import htm from 'htm'
|
||||||
import { signal } from 'preact/signals'
|
import { signal } from 'preact/signals'
|
||||||
import { Keys, Key } from 'key'
|
import { Keys, Key } from 'key'
|
||||||
|
import Crypto from 'crypto'
|
||||||
const html = htm.bind(h)
|
const html = htm.bind(h)
|
||||||
|
|
||||||
export class NodeUI extends Component {
|
export class NodeUI extends Component {
|
||||||
constructor() {//{{{
|
constructor(props) {//{{{
|
||||||
super()
|
super(props)
|
||||||
this.menu = signal(false)
|
this.menu = signal(false)
|
||||||
this.node = signal(null)
|
this.node = signal(null)
|
||||||
this.nodeContent = createRef()
|
this.nodeContent = createRef()
|
||||||
@ -53,13 +54,19 @@ export class NodeUI extends Component {
|
|||||||
let page = ''
|
let page = ''
|
||||||
switch(this.page.value) {
|
switch(this.page.value) {
|
||||||
case 'node':
|
case 'node':
|
||||||
if(node.ID > 0)
|
if(node.ID > 0) {
|
||||||
|
let padlock = ''
|
||||||
|
if(node.CryptoKeyID > 0)
|
||||||
|
padlock = html`<img src="/images/${window._VERSION}/padlock-black.svg" style="height: 24px;" />`
|
||||||
page = html`
|
page = html`
|
||||||
${children.length > 0 ? html`<div class="child-nodes">${children}</div>` : html``}
|
${children.length > 0 ? html`<div class="child-nodes">${children}</div>` : html``}
|
||||||
<div class="node-name">${node.Name}</div>
|
<div class="node-name">
|
||||||
<${NodeContent} key=${node.ID} content=${node.Content} ref=${this.nodeContent} />
|
${node.Name} ${padlock}
|
||||||
|
</div>
|
||||||
|
<${NodeContent} key=${node.ID} node=${node} ref=${this.nodeContent} />
|
||||||
<${NodeFiles} node=${this.node.value} />
|
<${NodeFiles} node=${this.node.value} />
|
||||||
`
|
`
|
||||||
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'upload':
|
case 'upload':
|
||||||
@ -84,8 +91,8 @@ export class NodeUI extends Component {
|
|||||||
<header class="${modified}" onclick=${()=>this.saveNode()}>
|
<header class="${modified}" onclick=${()=>this.saveNode()}>
|
||||||
<div class="tree"><img src="/images/${window._VERSION}/tree.svg" onclick=${()=>document.getElementById('app').classList.toggle('toggle-tree')} /></div>
|
<div class="tree"><img src="/images/${window._VERSION}/tree.svg" onclick=${()=>document.getElementById('app').classList.toggle('toggle-tree')} /></div>
|
||||||
<div class="name">Notes</div>
|
<div class="name">Notes</div>
|
||||||
<div class="add" onclick=${evt=>this.createNode(evt)}>+</div>
|
<div class="add" onclick=${evt=>this.createNode(evt)}><img src="/images/${window._VERSION}/add.svg" /></div>
|
||||||
<div class="keys" onclick=${()=>this.showPage('keys')}><img src="/images/${window._VERSION}/padlock.svg" /></div>
|
<div class="keys" onclick=${evt=>{ evt.stopPropagation(); this.showPage('keys')}}><img src="/images/${window._VERSION}/padlock.svg" /></div>
|
||||||
<div class="menu" onclick=${evt=>this.showMenu(evt)}>☰</div>
|
<div class="menu" onclick=${evt=>this.showMenu(evt)}>☰</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -96,7 +103,11 @@ export class NodeUI extends Component {
|
|||||||
${page}
|
${page}
|
||||||
`
|
`
|
||||||
}//}}}
|
}//}}}
|
||||||
componentDidMount() {//{{{
|
async componentDidMount() {//{{{
|
||||||
|
// When rendered and fetching the node, keys could be needed in order to
|
||||||
|
// decrypt the content.
|
||||||
|
await this.retrieveKeys()
|
||||||
|
|
||||||
this.props.app.startNode.retrieve(node=>{
|
this.props.app.startNode.retrieve(node=>{
|
||||||
this.node.value = node
|
this.node.value = node
|
||||||
|
|
||||||
@ -177,6 +188,7 @@ export class NodeUI extends Component {
|
|||||||
node.retrieve(node=>{
|
node.retrieve(node=>{
|
||||||
this.props.app.nodeModified.value = false
|
this.props.app.nodeModified.value = false
|
||||||
this.node.value = node
|
this.node.value = node
|
||||||
|
this.showPage('node')
|
||||||
|
|
||||||
// Tree needs to know another node is selected, in order to render any
|
// Tree needs to know another node is selected, in order to render any
|
||||||
// previously selected node not selected.
|
// previously selected node not selected.
|
||||||
@ -218,6 +230,29 @@ export class NodeUI extends Component {
|
|||||||
})
|
})
|
||||||
}//}}}
|
}//}}}
|
||||||
|
|
||||||
|
async retrieveKeys() {//{{{
|
||||||
|
return new Promise((resolve, reject)=>{
|
||||||
|
this.props.app.request('/key/retrieve', {})
|
||||||
|
.then(res=>{
|
||||||
|
this.keys.value = res.Keys.map(keyData=>new Key(keyData, this.keyCounter))
|
||||||
|
resolve(this.keys.value)
|
||||||
|
})
|
||||||
|
.catch(reject)
|
||||||
|
})
|
||||||
|
}//}}}
|
||||||
|
keyCounter() {//{{{
|
||||||
|
return window._app.current.request('/key/counter', {})
|
||||||
|
.then(res=>BigInt(res.Counter))
|
||||||
|
.catch(window._app.current.responseError)
|
||||||
|
}//}}}
|
||||||
|
getKey(id) {//{{{
|
||||||
|
let keys = this.keys.value
|
||||||
|
for(let i = 0; i < keys.length; i++)
|
||||||
|
if(keys[i].ID == id)
|
||||||
|
return keys[i]
|
||||||
|
return null
|
||||||
|
}//}}}
|
||||||
|
|
||||||
showPage(pg) {//{{{
|
showPage(pg) {//{{{
|
||||||
this.page.value = pg
|
this.page.value = pg
|
||||||
}//}}}
|
}//}}}
|
||||||
@ -229,10 +264,18 @@ class NodeContent extends Component {
|
|||||||
this.contentDiv = createRef()
|
this.contentDiv = createRef()
|
||||||
this.state = {
|
this.state = {
|
||||||
modified: false,
|
modified: false,
|
||||||
//content: props.content,
|
|
||||||
}
|
}
|
||||||
}//}}}
|
}//}}}
|
||||||
render({ content }) {//{{{
|
render({ node }) {//{{{
|
||||||
|
let content = ''
|
||||||
|
try {
|
||||||
|
content = node.content()
|
||||||
|
} catch(err) {
|
||||||
|
return html`
|
||||||
|
<div id="node-content" class="node-content encrypted">${err.message}</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="grow-wrap">
|
<div class="grow-wrap">
|
||||||
<textarea id="node-content" class="node-content" ref=${this.contentDiv} oninput=${()=>this.contentChanged()} required rows=1>${content}</textarea>
|
<textarea id="node-content" class="node-content" ref=${this.contentDiv} oninput=${()=>this.contentChanged()} required rows=1>${content}</textarea>
|
||||||
@ -255,6 +298,18 @@ class NodeContent extends Component {
|
|||||||
if(textarea)
|
if(textarea)
|
||||||
textarea.parentNode.dataset.replicatedValue = textarea.value
|
textarea.parentNode.dataset.replicatedValue = textarea.value
|
||||||
}//}}}
|
}//}}}
|
||||||
|
unlock() {//{{{
|
||||||
|
let pass = prompt("Password")
|
||||||
|
if(!pass)
|
||||||
|
return
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.props.model.unlock(pass)
|
||||||
|
this.forceUpdate()
|
||||||
|
} catch(err) {
|
||||||
|
alert(err)
|
||||||
|
}
|
||||||
|
}//}}}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NodeFiles extends Component {
|
class NodeFiles extends Component {
|
||||||
@ -299,11 +354,13 @@ export class Node {
|
|||||||
this.ID = nodeID
|
this.ID = nodeID
|
||||||
this.ParentID = 0
|
this.ParentID = 0
|
||||||
this.UserID = 0
|
this.UserID = 0
|
||||||
|
this.CryptoKeyID = 0
|
||||||
this.Name = ''
|
this.Name = ''
|
||||||
this.Content = ''
|
this._content = ''
|
||||||
this.Children = []
|
this.Children = []
|
||||||
this.Crumbs = []
|
this.Crumbs = []
|
||||||
this.Files = []
|
this.Files = []
|
||||||
|
this._decrypted = false
|
||||||
this._expanded = false // start value for the TreeNode component,
|
this._expanded = false // start value for the TreeNode component,
|
||||||
// it doesn't control it afterwards.
|
// it doesn't control it afterwards.
|
||||||
// Used to expand the crumbs upon site loading.
|
// Used to expand the crumbs upon site loading.
|
||||||
@ -313,8 +370,9 @@ export class Node {
|
|||||||
.then(res=>{
|
.then(res=>{
|
||||||
this.ParentID = res.Node.ParentID
|
this.ParentID = res.Node.ParentID
|
||||||
this.UserID = res.Node.UserID
|
this.UserID = res.Node.UserID
|
||||||
|
this.CryptoKeyID = res.Node.CryptoKeyID
|
||||||
this.Name = res.Node.Name
|
this.Name = res.Node.Name
|
||||||
this.Content = res.Node.Content
|
this._content = res.Node.Content
|
||||||
this.Children = res.Node.Children
|
this.Children = res.Node.Children
|
||||||
this.Crumbs = res.Node.Crumbs
|
this.Crumbs = res.Node.Crumbs
|
||||||
this.Files = res.Node.Files
|
this.Files = res.Node.Files
|
||||||
@ -339,10 +397,18 @@ export class Node {
|
|||||||
})
|
})
|
||||||
.catch(this.app.responseError)
|
.catch(this.app.responseError)
|
||||||
}//}}}
|
}//}}}
|
||||||
save(content, callback) {//{{{
|
async save(content, callback) {//{{{
|
||||||
|
let update_content = content
|
||||||
|
/*
|
||||||
|
* XXX - fix encrypting when saving
|
||||||
|
if(this.CryptoKeyID != 0)
|
||||||
|
update_content = await this.#encrypt(content)
|
||||||
|
*/
|
||||||
|
|
||||||
this.app.request('/node/update', {
|
this.app.request('/node/update', {
|
||||||
NodeID: this.ID,
|
NodeID: this.ID,
|
||||||
Content: content,
|
Content: update_content,
|
||||||
|
CryptoKeyID: this.CryptoKeyID,
|
||||||
})
|
})
|
||||||
.then(callback)
|
.then(callback)
|
||||||
.catch(this.app.responseError)
|
.catch(this.app.responseError)
|
||||||
@ -387,6 +453,74 @@ export class Node {
|
|||||||
a.remove() //afterwards we remove the element again
|
a.remove() //afterwards we remove the element again
|
||||||
})
|
})
|
||||||
}//}}}
|
}//}}}
|
||||||
|
content() {//{{{
|
||||||
|
if(this.CryptoKeyID != 0 && !this._decrypted) {
|
||||||
|
this.#decrypt()
|
||||||
|
}
|
||||||
|
return this._content
|
||||||
|
}//}}}
|
||||||
|
|
||||||
|
async encrypt(obj_key) {//{{{
|
||||||
|
if(obj_key.ID != this.CryptoKeyID)
|
||||||
|
throw('Invalid key')
|
||||||
|
|
||||||
|
let crypto = new Crypto(obj_key.key)
|
||||||
|
this._decrypted = false
|
||||||
|
|
||||||
|
let counter = await obj_key.counter()
|
||||||
|
|
||||||
|
this.content = sjcl.codec.base64.fromBits(
|
||||||
|
crypto.encrypt(
|
||||||
|
sjcl.codec.utf8String.toBits(this.content),
|
||||||
|
counter,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}//}}}
|
||||||
|
#decrypt() {//{{{
|
||||||
|
let obj_key = this.app.nodeUI.current.getKey(this.CryptoKeyID)
|
||||||
|
if(obj_key === null || obj_key.ID != this.CryptoKeyID)
|
||||||
|
throw('Invalid key')
|
||||||
|
|
||||||
|
// Ask user to unlock key first
|
||||||
|
var pass = null
|
||||||
|
while(pass || obj_key.status() == 'locked') {
|
||||||
|
pass = prompt("Password")
|
||||||
|
if(!pass)
|
||||||
|
throw new Error(`Key "${obj_key.description}" is locked`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
obj_key.unlock(pass)
|
||||||
|
} catch(err) {
|
||||||
|
alert(err)
|
||||||
|
}
|
||||||
|
pass = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if(obj_key.status() == 'locked')
|
||||||
|
throw new Error(`Key "${obj_key.description}" is locked`)
|
||||||
|
|
||||||
|
let crypto = new Crypto(obj_key.key)
|
||||||
|
this._decrypted = true
|
||||||
|
this._content = sjcl.codec.utf8String.fromBits(
|
||||||
|
crypto.decrypt(this._content)
|
||||||
|
)
|
||||||
|
}//}}}
|
||||||
|
/*
|
||||||
|
async #encrypt(content) {//{{{
|
||||||
|
let obj_key = this.app.nodeUI.current.getKey(this.CryptoKeyID)
|
||||||
|
if(obj_key === null || obj_key.ID != this.CryptoKeyID)
|
||||||
|
throw('Invalid key')
|
||||||
|
|
||||||
|
if(obj_key.status() == 'locked')
|
||||||
|
throw new Error(`Key "${obj_key.description}" is locked`)
|
||||||
|
|
||||||
|
let crypto = new Crypto(obj_key.key)
|
||||||
|
let content_bits = sjcl.codec.utf8String.toBits(content)
|
||||||
|
let counter = await this.app.nodeUI.current.keyCounter()
|
||||||
|
return crypto.encrypt(content_bits, counter, true)
|
||||||
|
}//}}}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
class Menu extends Component {
|
class Menu extends Component {
|
||||||
@ -504,16 +638,82 @@ class UploadUI extends Component {
|
|||||||
class NodeProperties extends Component {
|
class NodeProperties extends Component {
|
||||||
constructor(props) {//{{{
|
constructor(props) {//{{{
|
||||||
super(props)
|
super(props)
|
||||||
|
this.props.nodeui.retrieveKeys()
|
||||||
|
this.selected_key_id = 0
|
||||||
}//}}}
|
}//}}}
|
||||||
render({ nodeui }) {//{{{
|
render({ nodeui }) {//{{{
|
||||||
|
let save = true
|
||||||
|
let keys = nodeui.keys.value
|
||||||
|
.sort((a, b)=>{
|
||||||
|
if(a.description < b.description) return -1
|
||||||
|
if(a.description > b.description) return 1
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
.map(key=>{
|
||||||
|
this.props.nodeui.keys.value.some(uikey=>{
|
||||||
|
if(uikey.ID == nodeui.node.value.ID) {
|
||||||
|
save = (uikey.status() == 'unlocked')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="key ${key.status()}">
|
||||||
|
<input type="radio" name="key" id="key-${key.ID}" checked=${nodeui.node.value.CryptoKeyID == key.ID} disabled=${key.status() == 'locked'} oninput=${()=>this.selected_key_id = key.ID} />
|
||||||
|
<label for="key-${key.ID}">${key.description}</label>
|
||||||
|
</div>`
|
||||||
|
})
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div id="blackout" onclick=${()=>nodeui.properties.value = false}></div>
|
|
||||||
<div id="properties">
|
<div id="properties">
|
||||||
<h1>Node properties</h1>
|
<h1>Note properties</h1>
|
||||||
<input type="checkbox" id="node-encrypted" /> <label for="node-encrypted">Encrypted</label>
|
|
||||||
|
These properties are only for this note.
|
||||||
|
|
||||||
|
<h2>Encryption</h2>
|
||||||
|
<div class="key">
|
||||||
|
<input type="radio" id="key-none" name="key" checked=${nodeui.node.value.CryptoKeyID == 0} oninput=${()=>this.selected_key_id = 0} />
|
||||||
|
<label for="key-none">None</label>
|
||||||
|
</div>
|
||||||
|
${keys}
|
||||||
|
|
||||||
|
${save ? html`<button style="margin-top: 32px" onclick=${()=>this.save()}>Save</button>` : ''}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
}//}}}
|
}//}}}
|
||||||
|
save() {//{{{
|
||||||
|
let nodeui = this.props.nodeui
|
||||||
|
let node = nodeui.node.value
|
||||||
|
|
||||||
|
// Find the actual key object used for encryption
|
||||||
|
let encrypt_key = nodeui.getKey(this.selected_key_id)
|
||||||
|
let decrypt_key = nodeui.getKey(node.CryptoKeyID)
|
||||||
|
|
||||||
|
if(decrypt_key && decrypt_key.status() == 'locked') {
|
||||||
|
alert("Decryption key is locked and can not be used.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if(encrypt_key && encrypt_key.status() == 'locked') {
|
||||||
|
alert("Key is locked and can not be used.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Currently not encrypted - encrypt with new key.
|
||||||
|
let crypto = new Crypto(selected_key.key)
|
||||||
|
if(node.CryptoKeyID == 0) {
|
||||||
|
let encrypted = crypto.encrypt(
|
||||||
|
sjcl.codec.utf8String.toBits(node.Content()),
|
||||||
|
1n,
|
||||||
|
)
|
||||||
|
console.log(encrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
crypto.encrypt(
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
}//}}}
|
||||||
}
|
}
|
||||||
|
|
||||||
// vim: foldmethod=marker
|
// vim: foldmethod=marker
|
||||||
|
@ -24,16 +24,17 @@ html, body {
|
|||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
font-size: 1em;
|
font-size: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
margin-top: 32px;
|
margin-top: 32px;
|
||||||
font-size: 0.9em;
|
font-size: 1.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
|
padding: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#blackout {
|
#blackout {
|
||||||
@ -90,7 +91,6 @@ button {
|
|||||||
|
|
||||||
input {
|
input {
|
||||||
border: 1px solid #000;
|
border: 1px solid #000;
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.files {
|
.files {
|
||||||
@ -330,6 +330,10 @@ header {
|
|||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#node-content.encrypted {
|
||||||
|
color: #a00;
|
||||||
|
}
|
||||||
|
|
||||||
.node-content {
|
.node-content {
|
||||||
grid-area: content;
|
grid-area: content;
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
@ -413,7 +417,6 @@ header {
|
|||||||
grid-template-columns: 1fr min-content;
|
grid-template-columns: 1fr min-content;
|
||||||
grid-gap: 8px 16px;
|
grid-gap: 8px 16px;
|
||||||
color: #444;
|
color: #444;
|
||||||
font-size: 0.85em;
|
|
||||||
|
|
||||||
.filename {
|
.filename {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@ -443,11 +446,10 @@ header {
|
|||||||
|
|
||||||
.key-list {
|
.key-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: min-content 1fr min-content;
|
grid-template-columns: min-content min-content min-content;
|
||||||
grid-gap: 12px 12px;
|
grid-gap: 12px 12px;
|
||||||
align-items: end;
|
align-items: end;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
font-size: 0.85em;
|
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -464,13 +466,16 @@ header {
|
|||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.view {
|
.view {
|
||||||
white-space: nowrap;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
margin-left: 16px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hex-key {
|
.hex-key {
|
||||||
@ -482,6 +487,39 @@ header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#key-create {
|
||||||
|
.fields {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-gap: 16px;
|
||||||
|
max-width: 48ex;
|
||||||
|
|
||||||
|
#key-description {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
font-size: 1em;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
#key-key {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
height: 4em;
|
||||||
|
resize: none;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#key-pass1, #key-pass2 {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.generate {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.create {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.layout-tree {// {{{
|
.layout-tree {// {{{
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
|
Loading…
Reference in New Issue
Block a user