UI improvment + languages support + crypto v3 + support of parented

window
This commit is contained in:
devingfx 2018-07-08 23:31:21 +02:00
parent f28ee0a943
commit fe7439a509
2 changed files with 319 additions and 44 deletions

View File

@ -2,4 +2,30 @@
mini web app to use WebCrypto
<a href="javascript: win=window.open('','','titlebar=no,location=no'); fetch('https'+'://git.p2p.legal/dig/kriptopter/raw/master/kriptopter').then(res=>res.text()).then(app=>win.document.write(app) ))">bookmarklet</a>
## Install
```
git clone https//git.p2p.legal/dig/kriptopter
```
## Usage
### Standalone
Open the file `kriptopter` in a browser.
_(Tested only under Iron/Chromium 63.0.3300.0, please do the test in other browser and submit an issue, with a minimum stack trace, if you want to help )_
### Bookmarklet
To compile the app to be used as `javascrip:` url in a bookmark, some characters need to be escaped,
you can use `kompilo.js` with nodejs:
```javascript
node kompilo
```
it will generate a `kriptoptlet` file containing the text to copy/paste in a new bookmark.
<a href="javascript: win=window.open('','','titlebar=no,location=no'); fetch('https'+'://git.p2p.legal/dig/kriptopter/raw/master/kriptopter').then(res=>res.text()).then(app=>win.document.write(app) ))">bookmarklet online</a>
## Roadmap

View File

@ -1,10 +1,30 @@
<title eo>Kriptopter</title>
<title en>Cryptopter</title>
<title fr>Cryptoptère</title>
<style lang="eo,en,fr">
[_lang_] { display: none; }
:root[lang=_lang_] [_lang_] { display: inherit; }
:root[lang=_lang_] [_lang_\:title]:hover::after { display: inherit }
/* [_lang_\:title]:after {
display: none;
position: absolute;
background: grey;
border: 1px solid black;
color: white;
padding: .1em .3em;
margin-left: -5%;
}
:root[lang=_lang_] [_lang_\:title]:after { content: attr(_lang_\:title) } */
</style>
<style>
body{
display: grid;
grid-template: "menu menu menu" 2em
"main main main" auto
"left middle right" 2em
/ 6em auto 6em;
"left middle right" 3em
/ 7em auto 7em;
height: 100%;
grid-gap: 1em;
margin: 0;
@ -36,14 +56,31 @@
height: 2em;
margin: 0;
padding: 0;
padding-left: 1px;
}
menu > * {
margin-left: -1px;
}
menu > button {
font-size: 1em;
height: 2em;
width: 2em;
margin-left: -1px;
}
menu > button {
font-size: 1em;
height: 2em;
width: 2em;
}
menu > button > i {
width: 1em;
}
menu > button > .fa-caret-up {
position: absolute;
top: 0.75em;
}
menu > button > .fa-caret-down {
position: absolute;
top: 2.2em;
}
[select-get], [select-set], [point], [page] { display: none; }
.parented [select-get], .parented [select-set], .parented [point], .parented [page] { display: inherit; }
input[filename] { flex: 1; padding: 0 .5em; }
@ -59,16 +96,50 @@
pass i { position: absolute; top: .5em; right: .5em;}
pass input.clear { filter: blur(0) }
i.fa-file {
position: relative;
}
i.fa-file.crypted {
font-size: 1.5em;
color: rebeccapurple;
}
i.fa-file i.fa-key {
position: absolute;
font-size: 0.6em;
top: .5em;
left: -.5em;
text-shadow: 0 0 1px black, 0 0 1px black, 0 0 1px black, 0 0 1px black, 0 0 1px black, 0 0 1px black, 0 0 1px black, 0 0 1px black, 0 0 1px black, 0 0 1px black, 0 0 1px black, 0 0 1px black, 0 0 1px black;
transform: rotate(-50deg) scaleX(-1);
font-size: 0.7em;
}
i.fa-key {
color: lightseagreen
}
i.fa-key.pub {
color: lawngreen
}
i.fa-key.priv {
color: orangered
}
ui-toast {
position: fixed;
text-align: center;
background: lightblue;
background: white;
top: 50%;
left: 50%;
width: 20em;
margin: -1em -10em;
width: 60em;
margin: -1em -30em;
height: 2em;
box-shadow: 0 0.1em 1em darkgrey;
border: 1px solid darkgrey;
line-height: 2em;
}
ui-toast[error] {
color: red;
border-color: red;
}
</style>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/solid.css" integrity="sha384-TbilV5Lbhlwdyc4RuIV/JhD8NR+BfMrvz4BL5QFa2we1hQu6wvREr3v6XSRfCTRp" crossorigin="anonymous">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/regular.css" integrity="sha384-avJt9MoJH2rB4PKRsJRHZv7yiFZn8LrnXuzvmZoD3fh1aL6aM6s0BBcnCvBe6XSD" crossorigin="anonymous">
@ -76,38 +147,64 @@
<menu>
<button disabled select title="Select text on the page"
onclick="">
<i class="fas fa-align-center"></i>
<button select-get title="Get selected text on the page"
onclick="$text.value = getParentSelection()">
<i class="fas fa-align-left"></i>
<i class="fas fa-caret-down"></i>
</button>
<button disabled point title="Point the element in the page"
onclick="">
<!-- <button point title="Point the element in the page (not available yet)"
disabled onclick="">
<i class="fas fa-mouse-pointer"></i>
</button>
</button> -->
<button page title="Load the whole page content"
onclick="$text.value = opener.document.body.textContent">
<i class="far fa-file"></i>
<i class="fas fa-caret-down"></i>
</button>
<button open title="Load a file from disk"
onclick="$`[type=file]`.click()">
<i class="far fa-folder-open"></i>
<input type="file" onchange="read(this.files[0]).then( s=> $text.value = s ); $filename.value = this.files[0].name"/>
</button>
<input filename type="text"/>
<button save title="Save to disk"
onclick="save($text.value,'cryptor.txt').catch(bubbleOn(this))">
<input filename type="text" eo:placeHolder="Filo nami" en:placeHolder="File name" fr:placeHolder="Nom du fichier"/>
<button save en:title="Save to disk" fr:title="Enregistrer sur le disque" eo:title="Sovargi al disko"
onclick="save($text.value,$filename.value||'krypta.krr').catch(bubbleOn(this))">
<i class="far fa-save"></i>
</button>
<button select-set en:title="Replace selected text on the page" fr:title="Remplacer la séléction" eo:title="Sovargi al disko"
onclick="setParentSelection( $text.value )">
<i class="fas fa-align-left"></i>
<i class="fas fa-caret-up"></i>
</button>
</menu>
<ui-data></ui-data>
<ui-data bin></ui-data>
<button encrypt onclick="encrypt2($text.value).then(res=>$text.value=res,bubbleOn(this))">encrypt</button>
<button encrypt onclick="encrypt3($text.value).then(res=>$text.value=res,bubbleOn(this))">
<i class="far fa-file crypted" style="font-size: 1.5em">
<i class="fas fa-key" style=""></i>
</i>
<lang eo>cifri</lang>
<lang en>encrypt</lang>
<lang fr>chiffrer</lang>
</button>
<!-- <button sign onclick="sign($text.value).then(res=>$text.value=res,bubbleOn(this))"> -->
<!-- <i class="fas fa-file-contract"></i></button> -->
<pass>
<input type="text" placeholder="Enter a password"/>
<i class="far fa-eye-slash" onmouseover="$pass.classList.add('clear');this.classList.remove('fa-eye-slash');this.classList.add('fa-eye')"
onmouseout="$pass.classList.remove('clear');this.classList.add('fa-eye-slash');this.classList.remove('fa-eye')"></i>
</pass>
<button decrypt onclick="decrypt($text.value).catch(bubbleOn(this))">decrypt</button>
<button decrypt onclick="decrypt3($text.value).then(res=>$text.value=res,bubbleOn(this))">
<i class="fas fa-file crypted" style="font-size: 1.5em">
<i class="fas fa-key" style=""></i>
</i>
<lang eo>decifri</lang>
<lang en>decrypt</lang>
<lang fr>déchiffrer</lang>
</button>
<template ui-data>
<style>
@ -135,14 +232,13 @@
<script ui>
const _s = (ss, ...pp)=> ss.map((s,i)=> s + (pp[i]||'')).join('')
, _a = iterable=> iterable.length > 1 ? iterable : iterable[0]
, Q = (ss, ...pp)=> Array.from( document.querySelectorAll( _s(ss, ...pp)) )
, Q = (ss, ...pp)=> [...document.querySelectorAll( _s(ss, ...pp))]
, T = document.createElement('template')
, DOM = (ss, ...pp)=> ( T.innerHTML = _s(ss, ...pp), document.adoptNode(T.content).childNodes )
, $ = (ss, ...pp)=> _a( ~_s(ss, ...pp).indexOf('<') ? DOM(ss, ...pp) : Q(ss, ...pp) )
, notify = (html,duration)=> $`body`.add( $`<ui-toast ${duration?`duration="${duration}"`:''}>${html}</ui-toast>` )
, notify = (html,attrs)=> $`body`.add( $`<ui-toast ${attrs ? attrs : ''}>${html}</ui-toast>` )
, bubbleOn = el=> e=> el.dispatchEvent( new ErrorEvent('Error',{bubbles:true, error:e}) )
, parseAlgo = str=> ({ name: str.split(' ')[0], iv: Uint8Array.from(str.split(' ').slice(1)) })
, pulse = color=> (document.body.style.background = color) && setTimeout(o=> document.body.style.background = 'lightgrey', 1000)
Node.prototype.add = function( ...els )
@ -155,9 +251,14 @@ Node.prototype.$ = function(ss, ...pp)
return _a(
~_s(ss, ...pp).indexOf('<')
? this.add(...DOM(ss, ...pp))
: Array.from( this.querySelectorAll( _s(ss, ...pp)) )
: [...this.querySelectorAll( _s(ss, ...pp))]
)
}
Node.prototype.on = function(ss, ...pp)
{
let types = _s( [].concat(ss), ...pp ).split(/[,\s]/)
return (...args)=> ( types.map( type=> this.addEventListener(type,...args)), this )
}
class UI extends HTMLElement {
static get is(){ return 'ui' + this.name.replace(/[A-Z]/g, L=> '-'+L.toLocaleLowerCase()) }
@ -171,7 +272,7 @@ class UI extends HTMLElement {
}
class Toast extends UI {
static get duration(){ return this._duration || 5000000 }
static get duration(){ return this._duration || 10000 }
static set duration( v ){ this._duration = v }
connectedCallback()
{
@ -188,17 +289,30 @@ class Data extends UI {
// changes=> console.log(changes.filter(m=>~['bin','255','utf8','base64','object'].indexOf(m.attributeName)) )
// )
// .observe( this, { attributes: true } )
this.shadowRoot.append( ...Data.observedAttributes.map( type=> $`<button ${type}>${type}</button>` ) )
this.shadowRoot.$`button`.map( button=> button.onclick = e=> {
[...this.attributes].map( o=> o.ownerElement.removeAttributeNode(o) )
this.setAttribute( e.target.innerText, '' )
})
this.shadowRoot.append(
...Data.observedAttributes
.map( type=> $`<button ${type}>${type}</button>`
.on`click`( e=> this.type = e.target.innerText )
)
)
// this.shadowRoot.$`button`.map( button=> button.onclick = e=> this.type = e.target.innerText )
this.shadowRoot.$`textarea`.on`change`( e=> this._value = CC[this.type+'2buffer'](e.target.value) )
}
get value(){ return this.shadowRoot.$`textarea`.value }
set value( v ){ return this.shadowRoot.$`textarea`.value = v }
get type(){ return this.attributes[0].name }
set type( v ){
if( this.type == v ) return;
[...this.attributes].map( o=> o.ownerElement.removeAttributeNode(o) )
this.setAttribute( v, '' )
}
get value(){ return this._value }
set value( v ){ this._value = v; this.shadowRoot.$`textarea`.value = CC['buffer2'+this.type](v) }
attributeChangedCallback(name, oldValue, newValue)
{
console.log( arguments )
if( this.shadowRoot.$`textarea`.value )
this.shadowRoot.$`textarea`.value = CC['buffer2'+this.type]( this._value )
}
}
@ -206,19 +320,63 @@ class Data extends UI {
;[ Toast, Data ]
.map( klass=> customElements.define(klass.is, klass) )
document.body.addEventListener('Error', e=> pulse('red') && (notify(e.error.message).style.color = 'red') )
// document.body.addEventListener('Error', e=> pulse('red') && (notify(e.error.message).style.color = 'red') )
$`body`.on`Error`( e=>
pulse('red') && notify(
`<b>${e.error.code||''} ${e.error.name}</b>: ${e.error.message}`
, 'error')
)
// lang support
Object.defineProperty( window, 'lang', {
get: ()=> $`:root`.getAttribute('lang')
, set: v=> $`:root`.setAttribute( 'lang', v )
})
new MutationObserver( changes=>
changes.map( ch=> ch.attributeName == 'lang' && $`:root`.dispatchEvent(new Event('lang')) )
)
.observe( $`:root`, { attributes: true } )
const lgAttReg = new RegExp( '^('+$`style[lang]`.attributes.lang.value.split(',').join('|')+'):')
$`:root`.on`lang`( e=>
$`*`.map( node=> [...node.attributes]
.filter( att=> lgAttReg.test(att.name) )
)
.filter( arr=> arr.length )
.map( atts=> atts.filter(att=>new RegExp('^'+lang+':').test(att.name))
.map(att=> att.ownerElement.setAttribute(att.name.split(':').pop(),att.value) ))
)
// compile lang styles
Q`style[lang]`.map( style=>
style.innerHTML = [...style.sheet.rules]
.map( r=> style.attributes.lang.value.split(',')
.map( lg=> r.cssText.replace(/_lang_/g, lg) )
.join('\n')
)
.join('\n')
)
lang = navigator.language
</script>
<script app>
const algo = { name: "AES-GCM", iv: Uint8Array.from([120,1,248,135,62,71,87,156,92,67,155,37]) }
, algos = [ "AES-GCM 120 1 248 135 62 71 87 156 92 67 155 37" ]
, parseAlgo = str=> ({ name: str.split(' ')[0], iv: Uint8Array.from(str.split(' ').slice(1)) })
, algos = [ "AES-GCM 120 1 248 135 62 71 87 156 92 67 155 37", 'AES-CBC' ]
//, algo = parseAlgo(algos[0])
, $pass = $`pass input`
, $text = $`ui-data`
, $filename = $`[filename]`
opener && document.documentElement.classtList.add('parented')
// sel=opener.document.getSelection().type=='None'||'Range'
const getParentSelection = ()=> opener && opener.document.getSelection().toString()
const setParentSelection = str=> opener && opener.document.activeElement.setRangeText( str )
let key
const ivLen = 16 // the IV is always 16 bytes
$pass.onchange = async e=>
key = await crypto.subtle.importKey(
'raw'
@ -226,6 +384,22 @@ $pass.onchange = async e=>
, algo, false, ['encrypt','decrypt']
)
const joinIvAndData = (iv, data)=> {
var buf = new Uint8Array(iv.length + data.length)
Array.prototype.forEach.call(iv, (byte, i)=> buf[i] = byte )
Array.prototype.forEach.call(data, (byte, i)=> buf[ivLen + i] = byte )
return buf
}
const separateIvFromData = buf=> Array.prototype.reduce.call( new Uint8Array(buf),
( res, byte, i )=>
i < ivLen
? ( res.iv[i] = byte, res )
: ( res.data[i - ivLen] = byte, res )
, { iv: new Uint8Array(ivLen), data: new Uint8Array(buf.byteLength - ivLen) } )
const encrypt = ()=> {
return crypto.subtle.digest( 'SHA-256', new TextEncoder().encode($pass.value) )
.then( pwHash=> crypto.subtle.importKey('raw', pwHash, algo, false, ['encrypt']) )
@ -239,11 +413,26 @@ const encrypt = ()=> {
const encrypt2 = str=>
crypto.subtle.encrypt( algo, key, new TextEncoder().encode(str) )
//.then( ctBuffer=> $text.value = new Uint8Array(ctBuffer).toString().replace(/,/g,'O') )
.then( ctBuffer=> uint2String(new Uint8Array(ctBuffer)) )
//.then( ctBuffer=> $text.value = new TextDecoder().decode(new Uint8Array(ctBuffer)) )
// .then( ctBuffer=> uint2String(new Uint8Array(ctBuffer)) )
.then( ctBuffer=> $text.value = new TextDecoder().decode(new Uint8Array(ctBuffer)) )
//.catch( e=> pulse('red') )
const encrypt3 = ( data, iv = crypto.getRandomValues(new Uint8Array(ivLen)) )=>
crypto.subtle.encrypt( {name: 'AES-GCM', iv}, key, data )
// .then( encrypted=> bufferToBinaryString( joinIvAndData(iv, new Uint8Array(encrypted)).buffer ) )
.then( encrypted=> joinIvAndData(iv, new Uint8Array(encrypted)).buffer )
// var base64 = Unibabel.bufferToBase64(ciphered)
// .replace(/\-/g, '+')
// .replace(/_/g, '\/')
// ;
// while (base64.length % 4) {
// base64 += '=';
// }
// return base64;
// })
const decrypt = ()=> {
return crypto.subtle.digest( 'SHA-256', new TextEncoder().encode($pass.value) )
@ -256,6 +445,26 @@ const decrypt = ()=> {
//.catch( e=> pulse('red') )
}
const decrypt3 = buf=> {
let { iv, data } = separateIvFromData( buf )
return crypto.subtle.decrypt(
{ name: 'AES-GCM', iv }
, key
, data
)/*.then(function (decrypted) {
var base64 = bufferToUtf8(new Uint8Array(decrypted))
// .replace(/\-/g, '+')
// .replace(/_/g, '\/')
// ;
// while (base64.length % 4) {
// base64 += '=';
// }
return base64
})*/
}
var lastSaved
const save = ( content, filename )=> {
let link = document.createElement('a')
@ -295,13 +504,53 @@ const makeTextFile = text=> {
const save2 = (file, content, type)=> window.open( "data:application/octet-stream," + encodeURIComponent(content), file )
const uint2String = uintArray=> String.fromCharCode.apply( null, uintArray )
//// conversion ////
const string2Uint = string=> new Uint8Array(
string.split('')
const uint2string = uintArray=> String.fromCharCode.apply( null, uintArray )
const string2uint = string=> new Uint8Array(
btoa( string ).split('')
.map( l=>l.charCodeAt(0) )
)
const buffer2bin = buf=> Array.prototype.map.call( new Uint8Array(buf), ch=> String.fromCharCode(ch) ).join('')
const buffer2base64 = arr=> btoa( buffer2bin(arr) )
const bin2buffer = binstr=> new Uint8Array(
Array.prototype.map.call( new Uint8Array( binstr.length ), (ch, i)=> binstr.charCodeAt(i) )
)
const base642buffer = base64=> bin2buffer( atob(base64) )
const bin2utf8 = binstr=> decodeURIComponent(
binstr.replace(/./g, c=>
`%${c.charCodeAt(0) < 16 ? '0' : ''}${c.charCodeAt(0).toString(16).toUpperCase()}`
)
)
const buffer2utf8 = buf=> bin2utf8( buffer2bin(buf) )
const utf82bin = str=> encodeURIComponent(str)
// replaces any uri escape sequence, such as %0A,
// with binary escape, such as 0x0A
.replace(/%([0-9A-F]{2})/g, (s,$1)=> String.fromCharCode(parseInt($1, 16)) )
const utf82buffer = str=> bin2buffer(utf82bin(str))
const CC = {
uint2string
, string2uint
, buffer2bin
, buffer2base64
, buffer2utf8
, bin2buffer
, base642buffer
, bin2utf8
, utf82bin
, utf82buffer
}
/*const uintToString = (uintArray)=> {
var encodedString = String.fromCharCode.apply(null, uintArray),