2019-09-15 18:00:36 +00:00
'use strict' ;
2020-04-16 13:38:09 +00:00
'require baseclass' ;
2020-04-03 08:00:06 +00:00
'require view' ;
2019-10-02 18:01:27 +00:00
'require fs' ;
2019-11-03 17:03:01 +00:00
'require ui' ;
2019-09-15 18:00:36 +00:00
2020-04-16 13:38:09 +00:00
var isReadonlyView = ! L . hasViewPermission ( ) || null ;
var SSHPubkeyDecoder = baseclass . singleton ( {
2018-11-21 19:04:55 +00:00
lengthDecode : function ( s , off )
{
var l = ( s . charCodeAt ( off ++ ) << 24 ) |
( s . charCodeAt ( off ++ ) << 16 ) |
( s . charCodeAt ( off ++ ) << 8 ) |
s . charCodeAt ( off ++ ) ;
if ( l < 0 || ( off + l ) > s . length )
return - 1 ;
return l ;
} ,
decode : function ( s )
{
2020-12-23 14:31:58 +00:00
var parts = s . trim ( ) . match ( /^((?:(?:^|,)[^ =,]+(?:=(?:[^ ",]+|"(?:[^"\\]|\\.)*"))?)+ +)?(ssh-dss|ssh-rsa|ssh-ed25519|ecdsa-sha2-nistp[0-9]+) +([^ ]+)( +.*)?$/ ) ;
if ( ! parts )
2018-11-21 19:04:55 +00:00
return null ;
var key = null ;
2020-12-23 14:31:58 +00:00
try { key = atob ( parts [ 3 ] ) ; } catch ( e ) { }
2018-11-21 19:04:55 +00:00
if ( ! key )
return null ;
var off , len ;
off = 0 ;
len = this . lengthDecode ( key , off ) ;
if ( len <= 0 )
return null ;
var type = key . substr ( off + 4 , len ) ;
2020-12-23 14:31:58 +00:00
if ( type !== parts [ 2 ] )
2018-11-21 19:04:55 +00:00
return null ;
off += 4 + len ;
var len1 = off < key . length ? this . lengthDecode ( key , off ) : 0 ;
if ( len1 <= 0 )
return null ;
var curve = null ;
if ( type . indexOf ( 'ecdsa-sha2-' ) === 0 ) {
curve = key . substr ( off + 4 , len1 ) ;
if ( ! len1 || type . substr ( 11 ) !== curve )
return null ;
type = 'ecdsa-sha2' ;
curve = curve . replace ( /^nistp(\d+)$/ , 'NIST P-$1' ) ;
}
off += 4 + len1 ;
var len2 = off < key . length ? this . lengthDecode ( key , off ) : 0 ;
if ( len2 < 0 )
return null ;
if ( len1 & 1 )
len1 -- ;
if ( len2 & 1 )
len2 -- ;
2020-12-23 14:31:58 +00:00
var comment = ( parts [ 4 ] || '' ) . trim ( ) ,
fprint = parts [ 3 ] . length > 68 ? parts [ 3 ] . substr ( 0 , 33 ) + '…' + parts [ 3 ] . substr ( - 34 ) : parts [ 3 ] ;
var options = null ;
( parts [ 1 ] || '' ) . trim ( ) . replace ( /(?:^|,)([^ =,]+)(?:=(?:([^ ",]+)|"((?:[^"\\]|\\.)*)"))?/g , function ( m , k , p , q ) {
options = options || { } ;
if ( options . hasOwnProperty ( k ) )
options [ k ] += ',' + ( q || p || true ) ;
else
options [ k ] = ( q || p || true ) ;
} ) ;
2018-11-21 19:04:55 +00:00
switch ( type )
{
case 'ssh-rsa' :
2020-12-23 14:31:58 +00:00
return { type : 'RSA' , bits : len2 * 8 , comment : comment , options : options , fprint : fprint , src : s } ;
2018-11-21 19:04:55 +00:00
case 'ssh-dss' :
2020-12-23 14:31:58 +00:00
return { type : 'DSA' , bits : len1 * 8 , comment : comment , options : options , fprint : fprint , src : s } ;
2018-11-21 19:04:55 +00:00
case 'ssh-ed25519' :
2020-12-23 14:31:58 +00:00
return { type : 'ECDH' , curve : 'Curve25519' , comment : comment , options : options , fprint : fprint , src : s } ;
2018-11-21 19:04:55 +00:00
case 'ecdsa-sha2' :
2020-12-23 14:31:58 +00:00
return { type : 'ECDSA' , curve : curve , comment : comment , options : options , fprint : fprint , src : s } ;
2018-11-21 19:04:55 +00:00
default :
return null ;
}
}
2019-09-15 18:00:36 +00:00
} ) ;
2018-11-21 19:04:55 +00:00
2020-12-23 14:31:58 +00:00
function renderKeyItem ( pubkey ) {
return E ( 'div' , {
class : 'item' ,
click : isReadonlyView ? null : removeKey ,
'data-key' : pubkey . src
} , [
E ( 'strong' , pubkey . comment || _ ( 'Unnamed key' ) ) , E ( 'br' ) ,
E ( 'small' , [
'%s, %s' . format ( pubkey . type , pubkey . curve || _ ( '%d Bit' ) . format ( pubkey . bits ) ) ,
pubkey . options ? E ( [ ] , [
' / ' , _ ( 'Options:' ) , ' ' ,
E ( 'code' , Object . keys ( pubkey . options ) . sort ( ) . join ( ', ' ) )
] ) : '' ,
E ( 'br' ) , E ( 'code' , pubkey . fprint )
] )
] ) ;
}
2018-11-21 19:04:55 +00:00
function renderKeys ( keys ) {
2019-09-27 10:53:47 +00:00
var list = document . querySelector ( '.cbi-dynlist' ) ;
2018-11-21 19:04:55 +00:00
while ( ! matchesElem ( list . firstElementChild , '.add-item' ) )
list . removeChild ( list . firstElementChild ) ;
keys . forEach ( function ( key ) {
2019-09-15 18:00:36 +00:00
var pubkey = SSHPubkeyDecoder . decode ( key ) ;
2018-11-21 19:04:55 +00:00
if ( pubkey )
2020-12-23 14:31:58 +00:00
list . insertBefore ( renderKeyItem ( pubkey ) , list . lastElementChild ) ;
2018-11-21 19:04:55 +00:00
} ) ;
if ( list . firstElementChild === list . lastElementChild )
list . insertBefore ( E ( 'p' , _ ( 'No public keys present yet.' ) ) , list . lastElementChild ) ;
}
function saveKeys ( keys ) {
2019-10-21 06:43:07 +00:00
return fs . write ( '/etc/dropbear/authorized_keys' , keys . join ( '\n' ) + '\n' , 384 /* 0600 */ )
2019-09-15 18:00:36 +00:00
. then ( renderKeys . bind ( this , keys ) )
2019-11-03 17:03:01 +00:00
. catch ( function ( e ) { ui . addNotification ( null , E ( 'p' , e . message ) ) } )
. finally ( ui . hideModal ) ;
2018-11-21 19:04:55 +00:00
}
function addKey ( ev ) {
2019-09-15 18:00:36 +00:00
var list = findParent ( ev . target , '.cbi-dynlist' ) ,
2018-11-21 19:04:55 +00:00
input = list . querySelector ( 'input[type="text"]' ) ,
key = input . value . trim ( ) ,
2019-09-15 18:00:36 +00:00
pubkey = SSHPubkeyDecoder . decode ( key ) ,
2018-11-21 19:04:55 +00:00
keys = [ ] ;
if ( ! key . length )
return ;
list . querySelectorAll ( '.item' ) . forEach ( function ( item ) {
keys . push ( item . getAttribute ( 'data-key' ) ) ;
} ) ;
if ( keys . indexOf ( key ) !== - 1 ) {
2019-11-03 17:03:01 +00:00
ui . showModal ( _ ( 'Add key' ) , [
2018-11-21 19:04:55 +00:00
E ( 'div' , { class : 'alert-message warning' } , _ ( 'The given SSH public key has already been added.' ) ) ,
E ( 'div' , { class : 'right' } , E ( 'div' , { class : 'btn' , click : L . hideModal } , _ ( 'Close' ) ) )
] ) ;
}
else if ( ! pubkey ) {
2019-11-03 17:03:01 +00:00
ui . showModal ( _ ( 'Add key' ) , [
2018-11-21 19:04:55 +00:00
E ( 'div' , { class : 'alert-message warning' } , _ ( 'The given SSH public key is invalid. Please supply proper public RSA or ECDSA keys.' ) ) ,
E ( 'div' , { class : 'right' } , E ( 'div' , { class : 'btn' , click : L . hideModal } , _ ( 'Close' ) ) )
] ) ;
}
else {
keys . push ( key ) ;
input . value = '' ;
2019-09-15 18:00:36 +00:00
return saveKeys ( keys ) . then ( function ( ) {
2020-12-23 14:31:58 +00:00
var added = list . querySelector ( '[data-key="%s"]' . format ( key . replace ( /["\\]/g , '\\$&' ) ) ) ;
2019-09-15 18:00:36 +00:00
if ( added )
added . classList . add ( 'flash' ) ;
} ) ;
2018-11-21 19:04:55 +00:00
}
}
function removeKey ( ev ) {
var list = findParent ( ev . target , '.cbi-dynlist' ) ,
delkey = ev . target . getAttribute ( 'data-key' ) ,
keys = [ ] ;
list . querySelectorAll ( '.item' ) . forEach ( function ( item ) {
var key = item . getAttribute ( 'data-key' ) ;
if ( key !== delkey )
keys . push ( key ) ;
} ) ;
L . showModal ( _ ( 'Delete key' ) , [
E ( 'div' , _ ( 'Do you really want to delete the following SSH key?' ) ) ,
E ( 'pre' , delkey ) ,
E ( 'div' , { class : 'right' } , [
E ( 'div' , { class : 'btn' , click : L . hideModal } , _ ( 'Cancel' ) ) ,
' ' ,
2019-11-03 17:03:01 +00:00
E ( 'div' , { class : 'btn danger' , click : ui . createHandlerFn ( this , saveKeys , keys ) } , _ ( 'Delete key' ) ) ,
2018-11-21 19:04:55 +00:00
] )
] ) ;
}
function dragKey ( ev ) {
ev . stopPropagation ( ) ;
ev . preventDefault ( ) ;
ev . dataTransfer . dropEffect = 'copy' ;
}
function dropKey ( ev ) {
var file = ev . dataTransfer . files [ 0 ] ,
input = ev . currentTarget . querySelector ( 'input[type="text"]' ) ,
reader = new FileReader ( ) ;
if ( file ) {
reader . onload = function ( rev ) {
input . value = rev . target . result . trim ( ) ;
addKey ( ev ) ;
input . value = '' ;
} ;
reader . readAsText ( file ) ;
}
ev . stopPropagation ( ) ;
ev . preventDefault ( ) ;
}
2019-09-15 18:00:36 +00:00
function handleWindowDragDropIgnore ( ev ) {
ev . preventDefault ( )
}
2018-11-21 19:04:55 +00:00
2020-04-03 08:00:06 +00:00
return view . extend ( {
2019-09-15 18:00:36 +00:00
load : function ( ) {
2019-10-02 18:01:27 +00:00
return fs . lines ( '/etc/dropbear/authorized_keys' ) . then ( function ( lines ) {
2020-12-23 14:31:58 +00:00
return lines . map ( function ( line ) {
return SSHPubkeyDecoder . decode ( line ) ;
} ) . filter ( function ( line ) {
return line != null ;
2019-09-15 18:00:36 +00:00
} ) ;
} ) ;
} ,
render : function ( keys ) {
2020-04-16 13:38:09 +00:00
var list = E ( 'div' , {
'class' : 'cbi-dynlist' ,
'dragover' : isReadonlyView ? null : dragKey ,
'drop' : isReadonlyView ? null : dropKey
} , [
2019-09-15 18:00:36 +00:00
E ( 'div' , { 'class' : 'add-item' } , [
E ( 'input' , {
'class' : 'cbi-input-text' ,
'type' : 'text' ,
'placeholder' : _ ( 'Paste or drag SSH key file…' ) ,
2020-04-16 13:38:09 +00:00
'keydown' : function ( ev ) { if ( ev . keyCode === 13 ) addKey ( ev ) } ,
'disabled' : isReadonlyView
2019-09-15 18:00:36 +00:00
} ) ,
E ( 'button' , {
'class' : 'cbi-button' ,
2020-04-16 13:38:09 +00:00
'click' : ui . createHandlerFn ( this , addKey ) ,
'disabled' : isReadonlyView
2019-09-15 18:00:36 +00:00
} , _ ( 'Add key' ) )
] )
] ) ;
2020-12-23 14:31:58 +00:00
keys . forEach ( L . bind ( function ( pubkey ) {
list . insertBefore ( renderKeyItem ( pubkey ) , list . lastElementChild ) ;
2019-09-15 18:00:36 +00:00
} , this ) ) ;
if ( list . firstElementChild === list . lastElementChild )
list . insertBefore ( E ( 'p' , _ ( 'No public keys present yet.' ) ) , list . lastElementChild ) ;
window . addEventListener ( 'dragover' , handleWindowDragDropIgnore ) ;
window . addEventListener ( 'drop' , handleWindowDragDropIgnore ) ;
return E ( 'div' , { } , [
E ( 'h2' , _ ( 'SSH-Keys' ) ) ,
E ( 'div' , { 'class' : 'cbi-section-descr' } , _ ( 'Public keys allow for the passwordless SSH logins with a higher security compared to the use of plain passwords. In order to upload a new key to the device, paste an OpenSSH compatible public key line or drag a <code>.pub</code> file into the input field.' ) ) ,
E ( 'div' , { 'class' : 'cbi-section-node' } , list )
] ) ;
} ,
handleSaveApply : null ,
handleSave : null ,
handleReset : null
2018-11-21 19:04:55 +00:00
} ) ;