@futoin/security
Version:
FutoIn Security Concept reference implementation
323 lines (277 loc) • 9.31 kB
JavaScript
;
/**
* @file
*
* Copyright 2018 FutoIn Project (https://futoin.org)
* Copyright 2018 Andrey Galkin <andrey@futoin.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const assert = require( 'assert' );
const Errors = require( 'futoin-asyncsteps/Errors' );
const {
MANAGE_FACE,
SVKEY_FACE,
CLIENT_TOKEN_COOKIE,
} = require( './main' );
module.exports = new class {
checkUser( as, ccm, user, source = null ) {
as.isAsyncSteps();
assert( user );
const manage = ccm.iface( MANAGE_FACE );
as.add(
( as ) => manage.getUserInfo( as, user ),
( as, err ) => {
if ( err === 'UnknownUser' ) {
as.error( Errors.SecurityError, `Invalid user or password: ${user}` );
}
}
);
as.add( ( as, info ) => {
if ( !info.is_enabled ) {
as.error( Errors.SecurityError, `User is not enabled: ${user}` );
}
if ( source ) {
// TODO: check fingerprints
}
as.success( info );
} );
}
parseMACAlgo( as, algo ) {
let macf;
let hash;
switch ( algo ) {
case 'HMAC-MD5':
case 'HMD5':
macf = 'HMAC';
hash = 'MD5';
break;
case 'HMAC-GOST3411-256':
case 'HG256':
macf = 'HMAC';
hash = 'md_gost12_256';
break;
case 'HMAC-GOST3411-512':
case 'HG512':
macf = 'HMAC';
hash = 'md_gost12_512';
break;
case 'HMAC-SHA-224':
case 'HS224':
macf = 'HMAC';
hash = 'SHA224';
break;
case 'HMAC-SHA-256':
case 'HS256':
macf = 'HMAC';
hash = 'SHA256';
break;
case 'HMAC-SHA-384':
case 'HS384':
macf = 'HMAC';
hash = 'SHA384';
break;
case 'HMAC-SHA-512':
case 'HS512':
macf = 'HMAC';
hash = 'SHA512';
break;
case 'KMAC-128':
macf = 'KMAC';
hash = '128';
break;
case 'KMAC-256':
macf = 'KMAC';
hash = '256';
break;
default:
as.error( Errors.InvokerError, `Unknown hash type: ${algo}` );
}
return { macf, hash };
}
parseKDS( as, kds ) {
let kdf;
let kdf_hash;
switch ( kds ) {
case 'HKDF256':
kdf = 'HKDF';
kdf_hash = 'SHA-256';
break;
case 'HKDF512':
kdf = 'HKDF';
kdf_hash = 'SHA-512';
break;
default:
as.error( Errors.InvokerError, `Unknown KDS: ${kds}` );
}
return { kdf, kdf_hash };
}
purpose2usage( as, purpose ) {
switch ( purpose ) {
case 'MAC': return [ 'sign', 'shared', 'temp' ];
case 'ENC': return [ 'encrypt', 'shared', 'temp' ];
case 'EXPOSED': return [ 'encrypt', 'sign', 'shared', 'temp' ];
default: as.error( Errors.Invoker, `Unknown key purpose: ${purpose}` );
}
}
ensureDerivedKey( as, ccm, purpose, { msid, type, kds, salt, prm, forbid_derive=false } ) {
assert( salt, 'Empty salt' );
assert( msid, 'Empty Master Secret ID' );
assert( kds, 'Empty Key Derivation Strategy' );
assert( type, 'Empty Key type' );
const { kdf, kdf_hash } = this.parseKDS( as, kds );
const kdf_salt = `${salt}:${purpose}`;
const svkey = ccm.iface( SVKEY_FACE );
const drv_ext_id = `${msid}:DRV:${kds}:${type}:${kdf_salt}:${prm}`;
// Try to get already derived key
as.add(
( as ) => {
svkey.extKeyInfo( as, drv_ext_id );
// Fast path result
as.add( ( as, key_info ) => {
const { local_id, global_id } = key_info.params;
assert( local_id );
assert( global_id );
const auth_info = { local_id, global_id };
as.success( { auth_info, dsid: key_info.id } );
} );
},
( as, err ) => {
if ( ( err !== 'UnknownKeyID' ) || forbid_derive ) {
return;
}
// Check if master key exists
svkey.keyInfo( as, msid );
as.add( ( as, mkey_info ) => {
const { local_id, bits } = mkey_info.params;
assert( local_id );
// ensure user is enabled
this.checkUser( as, ccm, local_id );
as.add( ( as, { global_id, ds_max } ) => {
// NOTE: make sure to use actual Global ID
assert( global_id );
// Check if too many derived keys
svkey.listKeys( as, `${msid}:DRV:` );
as.add( ( as, keys ) => {
if ( ( keys.length + 1 ) >= ds_max ) {
as.error( 'SecurityError', 'Too many derived keys' );
}
} );
// Derive key
svkey.deriveKey(
as,
drv_ext_id,
this.purpose2usage( as, purpose ),
type,
{
bits,
local_id,
global_id,
},
msid,
kdf,
kdf_hash,
Buffer.from( kdf_salt ),
{
info: prm,
}
);
// Slow path result
as.add( ( as, dsid ) => {
const auth_info = { local_id, global_id };
as.success( { auth_info, dsid } );
} );
} );
} );
}
);
}
encryptKey( as, ccm, { key_id, emode }, { salt, msid, type, kds, prm } ) {
this.ensureDerivedKey(
as, ccm, 'ENC',
{ salt, msid, type, kds, prm }
);
as.add( ( as, tmp ) => {
const dsid = tmp.dsid;
const svkey = ccm.iface( SVKEY_FACE );
svkey.encryptedKey(
as,
key_id,
dsid,
emode
);
as.add( ( as, ekey ) => {
svkey.wipeKey( as, dsid );
as.successStep( ekey );
} );
} );
}
injectEncryptedMACKey(
as, ccm,
{ ext_id, emode, ekey, local_id, global_id },
{ salt, msid, type, kds, prm }
) {
this.ensureDerivedKey(
as, ccm, 'ENC',
{ salt, msid, type, kds, prm }
);
as.add( ( as, tmp ) => {
const dsid = tmp.dsid;
const svkey = ccm.iface( SVKEY_FACE );
svkey.injectEncryptedKey(
as,
ext_id,
this.purpose2usage( as, 'MAC' ),
'HMAC',
{ local_id, global_id },
ekey,
dsid,
emode
);
as.add( ( as, ekey ) => {
svkey.wipeKey( as, dsid );
as.successStep( ekey );
} );
} );
}
reqinfo2source( reqinfo ) {
const res = {};
const channel = reqinfo.channel();
const chan_type = channel.type();
if ( chan_type === 'HTTP' ) {
// User Agent
const headers = channel.getRequestHeaders();
const ua = headers['user-agent'];
if ( ua ) {
res.user_agent = ua;
}
// Client Token
if ( 'cookie' in headers ) {
// Enable heavy processing only for requests with cookie set
const client_token = channel.getCookie( CLIENT_TOKEN_COOKIE );
if ( client_token ) {
res.client_token = client_token;
}
}
}
// Source IP
const { host } = reqinfo.info.CLIENT_ADDR;
if ( host ) {
res.source_ip = host;
}
// TODO:
// x509
// ssh_pubkey
return res;
}
};