first commit
This commit is contained in:
commit
ff482ebe23
251 files changed
+19126
No files matched your search
@@ -0,0 +1,35 @@
|
||||
'use strict';
|
||||
// Application-layer policy encryption over authenticated HTTPS. No TLS replacement.
|
||||
const c=require('node:crypto');
|
||||
const VERSION='safetynet-policy-mlkem1024-mldsa87-v1';
|
||||
const MAX=3*1024*1024;
|
||||
const fail=()=>{throw Error('Protected management channel refused');};
|
||||
const record=value=>{if(!value||typeof value!=='object'||Array.isArray(value))fail();return value;};
|
||||
function exact(value,keys){record(value);if(Object.keys(value).sort().join(',')!==keys.slice().sort().join(','))fail();}
|
||||
const encode=value=>value.toString('base64url');
|
||||
function decode(value,max=MAX){if(typeof value!=='string'||value.length>Math.ceil(max*4/3)||!value.length||!/^[A-Za-z0-9_-]+$/.test(value))fail();const bytes=Buffer.from(value,'base64url');if(bytes.length>max||encode(bytes)!==value)fail();return bytes;}
|
||||
function publicKey(pem,type){if(typeof pem!=='string'||pem.length>20000)fail();const key=c.createPublicKey(pem);if(key.asymmetricKeyType!==type)fail();return key;}
|
||||
const bytes=value=>Buffer.from(JSON.stringify(value));
|
||||
function signed(payload,edPrivate,pqPrivate){const data=bytes(payload);return {...payload,edSignature:encode(c.sign(null,data,edPrivate)),pqSignature:encode(c.sign(null,data,pqPrivate))};}
|
||||
function verified(value,keys,edPublic,pqPublic){exact(value,[...keys,'edSignature','pqSignature']);const payload=Object.fromEntries(keys.map(key=>[key,value[key]]));const data=bytes(payload);if(!c.verify(null,data,publicKey(edPublic,'ed25519'),decode(value.edSignature,64))||!c.verify(null,data,publicKey(pqPublic,'ml-dsa-87'),decode(value.pqSignature,5000)))fail();return payload;}
|
||||
function identity(edPrivate,pqPrivate,pqPublic){publicKey(pqPublic,'ml-dsa-87');return signed({version:VERSION,signatureKey:pqPublic},edPrivate,pqPrivate);}
|
||||
function verifyIdentity(value,edPublic,pin){record(value);if(value.version!==VERSION||pin&&value.signatureKey!==pin)fail();verified(value,['version','signatureKey'],edPublic,pin||value.signatureKey);return value.signatureKey;}
|
||||
function challenge(deviceId,edPrivate,pqPrivate,now=Date.now()){
|
||||
const pair=c.generateKeyPairSync('ml-kem-1024');
|
||||
const bundle=signed({version:VERSION,deviceId,id:encode(c.randomBytes(32)),expiresAt:now+60000,kemKey:pair.publicKey.export({type:'spki',format:'pem'})},edPrivate,pqPrivate);
|
||||
return {bundle,privateKey:pair.privateKey};
|
||||
}
|
||||
function verifyChallenge(value,deviceId,edPublic,pqPublic,now=Date.now()){
|
||||
const result=verified(value,['version','deviceId','id','expiresAt','kemKey'],edPublic,pqPublic);
|
||||
if(result.version!==VERSION||result.deviceId!==deviceId||decode(result.id,32).length!==32||!Number.isSafeInteger(result.expiresAt)||result.expiresAt<=now||result.expiresAt>now+65000)fail();
|
||||
publicKey(result.kemKey,'ml-kem-1024');return result;
|
||||
}
|
||||
function derive(shared,bundle,direction){const salt=c.createHash('sha384').update(bytes(bundle)).digest();return Buffer.from(c.hkdfSync('sha384',shared,salt,Buffer.from(VERSION+':'+direction),32));}
|
||||
const aad=(bundle,direction)=>bytes({version:VERSION,id:bundle.id,deviceId:bundle.deviceId,direction});
|
||||
function seal(key,value,bundle,direction){const input=bytes(value);if(input.length>MAX)fail();const iv=c.randomBytes(12),cipher=c.createCipheriv('aes-256-gcm',key,iv);cipher.setAAD(aad(bundle,direction));try{return {iv:encode(iv),ciphertext:encode(Buffer.concat([cipher.update(input),cipher.final()])),tag:encode(cipher.getAuthTag())};}finally{input.fill(0);}}
|
||||
function open(key,value,bundle,direction){exact(value,['iv','ciphertext','tag']);const iv=decode(value.iv,12),tag=decode(value.tag,16);if(iv.length!==12||tag.length!==16)fail();const cipher=c.createDecipheriv('aes-256-gcm',key,iv);cipher.setAAD(aad(bundle,direction));cipher.setAuthTag(tag);let plaintext;try{plaintext=Buffer.concat([cipher.update(decode(value.ciphertext)),cipher.final()]);return JSON.parse(plaintext.toString('utf8'));}finally{plaintext?.fill(0);}}
|
||||
function prepare(bundle){const {sharedKey,ciphertext}=c.encapsulate(publicKey(bundle.kemKey,'ml-kem-1024'));const requestKey=derive(sharedKey,bundle,'request'),responseKey=derive(sharedKey,bundle,'response');sharedKey.fill(0);const requestId=encode(c.randomBytes(32));try{return {request:{version:VERSION,id:bundle.id,deviceId:bundle.deviceId,kemCiphertext:encode(ciphertext),encrypted:seal(requestKey,{purpose:'policy',requestId},bundle,'request')},responseKey,requestId};}finally{requestKey.fill(0);}}
|
||||
function receive(request,bundle,privateKey){exact(request,['version','id','deviceId','kemCiphertext','encrypted']);if(request.version!==VERSION||request.id!==bundle.id||request.deviceId!==bundle.deviceId||bundle.expiresAt<=Date.now())fail();const ciphertext=decode(request.kemCiphertext,1568);if(ciphertext.length!==1568)fail();const shared=c.decapsulate(privateKey,ciphertext),requestKey=derive(shared,bundle,'request'),responseKey=derive(shared,bundle,'response');shared.fill(0);try{const body=open(requestKey,request.encrypted,bundle,'request');exact(body,['purpose','requestId']);if(body.purpose!=='policy'||decode(body.requestId,32).length!==32)fail();return {responseKey,requestId:body.requestId};}catch(error){responseKey.fill(0);throw error;}finally{requestKey.fill(0);}}
|
||||
function response(policy,bundle,context){return {version:VERSION,id:bundle.id,encrypted:seal(context.responseKey,{requestId:context.requestId,policy},bundle,'response')};}
|
||||
function finish(value,bundle,context){exact(value,['version','id','encrypted']);if(value.version!==VERSION||value.id!==bundle.id)fail();const body=open(context.responseKey,value.encrypted,bundle,'response');exact(body,['requestId','policy']);if(body.requestId!==context.requestId)fail();return body.policy;}
|
||||
module.exports={VERSION,identity,verifyIdentity,challenge,verifyChallenge,prepare,receive,response,finish};
|
||||
Reference in new issue
Block a user