Teams PQC addons fully working

This commit is contained in:
itscutaru 2025-07-28 11:24:36 +03:00
parent 2d58d692fb
commit 71f4e69be0
15 changed files with 156 additions and 38 deletions

View File

@ -1032,7 +1032,7 @@ HK.onChannelMessage = function (Env, Server, channel, msgStruct, cb) {
}));
}).nThen(function (w) {
// if there's no validateKey present skip to the next block
if (!(metadata && metadata.validateKey)) { return; }
if (!(metadata && metadata.validateKey && metadata.dsaPublic)) { return; }
// trim the checkpoint indicator off the message if it's present
let signedMsg = (isCp) ? msgStruct[4].replace(CHECKPOINT_PATTERN, '') : msgStruct[4];
@ -1045,7 +1045,7 @@ HK.onChannelMessage = function (Env, Server, channel, msgStruct, cb) {
*/
var proceed = w();
Env.queueValidation(channel.id, function (next) {
Env.validateMessage(signedMsg, metadata.validateKey, function (err) {
Env.validateMessage(signedMsg, metadata.validateKey, metadata.dsaPublic, function (err) {
// always go on to the next item in the queue regardless of the outcome
next();
@ -1056,7 +1056,8 @@ HK.onChannelMessage = function (Env, Server, channel, msgStruct, cb) {
// we log this case, but not others for some reason
Log.info("HK_SIGNED_MESSAGE_REJECTED", {
channel: channel.id,
validateKey: metadata.validayKey,
validateKey: metadata.validateKey,
dsaPublic: metadata.dsaPublic,
message: signedMsg,
});
}

View File

@ -18,6 +18,7 @@ const Nacl = require('tweetnacl/nacl-fast');
const Eviction = require("../eviction");
const CPCrypto = require('../crypto');
const plugins = require("../plugin-manager");
const ml_dsa = require('@noble/post-quantum/ml-dsa');
const Env = {
Log: {},
@ -783,8 +784,52 @@ COMMANDS.INLINE = function (data, cb) {
} catch (e) {
return void cb("E_BADKEY");
}
// validate the message
//const validated = Nacl.sign.open(signedMsg, validateKey);
// Hybrid signature detection: [classical sig (64 bytes)] + [PQC sig length (4 bytes)] + [PQC sig] + [message]
if (signedMsg.length > 64 + 4) {
try {
// Parse hybrid signature
const classicalSig = signedMsg.slice(0, 64);
const pqSigLen = new DataView(signedMsg.buffer, signedMsg.byteOffset + 64, 4).getUint32(0, false);
const pqSig = signedMsg.slice(64 + 4, 64 + 4 + pqSigLen);
const message = signedMsg.slice(64 + 4 + pqSigLen);
// Classical verification
const classicalVerified = Nacl.sign.detached.verify(message, classicalSig, validateKey);
// PQC verification: get PQC public key from metadata if available
let pqcVerified = false;
let pqcPublicKey;
if (data.dsaPublic) {
try {
pqcPublicKey = Util.decodeBase64(data.dsaPublic);
} catch (e) {
return void cb("E_BAD_PQ_KEY");
}
} else {
// If not provided, fail (could be extended to fetch from metadata)
return void cb("E_NO_PQ_KEY");
}
try {
pqcVerified = ml_dsa.ml_dsa44.internal.verify(
pqcPublicKey,
message,
pqSig
);
} catch (e) {
pqcVerified = false;
}
if (!classicalVerified || !pqcVerified) {
return void cb("FAILED");
}
return cb();
} catch (e) {
return void cb("E_HYBRID_SIG_PARSE");
}
}
// Fallback: classical only
const validated = Env.crypto.open(signedMsg, validateKey);
if (!validated) {
return void cb("FAILED");

View File

@ -555,12 +555,14 @@ Workers.initialize = function (Env, config, _cb) {
};
// Synchronous crypto functions
Env.validateMessage = function (signedMsg, key, cb) {
sendCommand({
msg: signedMsg,
key: key,
Env.validateMessage = function (opts, cb) {
const cmd = {
msg: opts.msg,
key: opts.key,
command: 'INLINE',
}, cb);
};
if (opts.dsaPublic) cmd.dsaPublic = opts.dsaPublic;
sendCommand(cmd, cb);
};
Env.checkSignature = function (signedMsg, signature, publicKey, cb) {

View File

@ -74,7 +74,7 @@ const factory = (Util, ApiConfig = {}, ServerCommand, Nacl, Crypto) => {
var sign = keys.sign;
var pqSignPair = keys.pqSignPair;
// Basic format with classical keys
// Basic format with classical and pqc keys
var result = {
edPrivate: Util.encodeBase64(sign.secretKey),
edPublic: Util.encodeBase64(sign.publicKey),
@ -101,10 +101,12 @@ const factory = (Util, ApiConfig = {}, ServerCommand, Nacl, Crypto) => {
};
// (uint8Array block) => payload object
// src/common/outer/login-block.js
Block.decrypt = function (u8_content, keys) {
// version is currently ignored since there is only one
var nonce = u8_content.subarray(1, 1 + Crypto.CryptoAgility.secretboxNonceLength());
var box = u8_content.subarray(1 + Crypto.CryptoAgility.secretboxNonceLength());
var nonceLength = Crypto.CryptoAgility.secretboxNonceLength();
var nonce = u8_content.subarray(1, 1 + nonceLength);
var box = u8_content.subarray(1 + nonceLength);
var plaintext = Crypto.CryptoAgility.secretboxOpen(box, nonce, keys.symmetric);
try {

View File

@ -586,7 +586,9 @@ const factory = (Sortify, UserObject, ProxyManager,
color: Store.getUserColor(),
notifications: Util.find(proxy, ['mailboxes', 'notifications', 'channel']),
curvePublic: proxy.curvePublic,
kemPublic: proxy.kemPublic,
edPublic: proxy.edPublic,
dsaPublic: proxy.dsaPublic,
netfluxId: store?.network?.webChannels?.[0]?.myID,
badge: Util.find(proxy, ['profile', 'badge'])
},
@ -595,6 +597,8 @@ const factory = (Sortify, UserObject, ProxyManager,
clientId: clientId,
edPublic: proxy.edPublic,
edPrivate: proxy.edPrivate,
dsaPublic: proxy.dsaPublic,
dsaPrivate: proxy.dsaPrivate,
friends: proxy.friends || {},
settings: proxy.settings || NEW_USER_SETTINGS,
thumbnails: disableThumbnails === false,

View File

@ -12,7 +12,7 @@ var factory = function (Util, Cred, Nacl, Crypto) {
Invite.generateKeys = function () {
var ed = Crypto.CryptoAgility.signKeyPair();
var curve = Crypto.CryptoAgility.curveKeyPair();
var kem = Crypto.PQC.ml_dsa.ml_kem512().keygen();
var kem = Crypto.PQC.ml_kem.ml_kem512.keygen();
var dsa = Crypto.PQC.ml_dsa.ml_dsa44.keygen();
return {
edPublic: encode64(ed.publicKey),

View File

@ -5,15 +5,35 @@
const factory = (Crypto, Hash, Util, Constants, Realtime) => {
var Msg = {};
/*
* CRYPTOGRAPHIC IDENTITY NOTE:
*
* Throughout this module and the entire CryptPad application, we continue to use
* curve25519 public keys (curvePublic) as the primary identifier for users despite
* having post-quantum cryptography (PQC) keys (kemPublic) available.
*
* 1. Legacy compatibility: A complete migration to PQC for identification would require
* changing all existing user relationships and channels
*
* 2. System integration: The curvePublic key is deeply integrated into friend relationships,
* channel management, history keeper, and other core components
*
* 3. Risk management: Partially migrating identity systems can lead to inconsistencies
* across the application, potentially creating security vulnerabilities
*
* A future coordinated migration will be necessary to fully replace curve25519 with PQC,
* which will require careful planning to ensure all components are updated simultaneously.
*/
var createData = Msg.createData = function (proxy, hash) {
var data = {
channel: hash || Hash.createChannelId(),
displayName: proxy['cryptpad.username'],
profile: proxy.profile && proxy.profile.view,
edPublic: proxy.edPublic,
curvePublic: proxy.curvePublic,
curvePublic: proxy.curvePublic, // Still used as primary ID throughout the system
dsaPublic: proxy.dsaPublic,
kemPublic: proxy.kemPublic,
kemPublic: proxy.kemPublic, // PQC key available but not used as primary ID yet
notifications: Util.find(proxy, ['mailboxes', 'notifications', 'channel']),
avatar: proxy.profile && proxy.profile.avatar,
badge: proxy.profile && proxy.profile.badge,
@ -23,6 +43,8 @@ const factory = (Crypto, Hash, Util, Constants, Realtime) => {
return data;
};
// Friend lookup still uses curvePublic as the key - changing this would require
// migrating all existing friendship relationships and updating all related code
var getFriend = Msg.getFriend = function (proxy, pubkey) {
if (!pubkey) { return; }
if (pubkey === proxy.curvePublic) {

View File

@ -730,10 +730,6 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
var ciphertext = crypto.encrypt(Sortify(msg));
var id = getMessageId(ciphertext);
//console.log("Sending with id [%s]", id, msg);
//console.log();
response.expect(id, function (err, state) {
if (err) { return void cb(err); }
cb(void 0, state, id);
@ -879,8 +875,19 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
w.abort();
return void cb("NO_VALIDATE_KEY");
}
if (!config.keys.teamKemPublic && metadata.kemPublic) {
config.keys.teamKemPublic = metadata.kemPublic;
}
if (!config.keys.teamDsaPublic && metadata.dsaPublic) {
config.keys.teamDsaPublic = metadata.dsaPublic;
}
if (!config.keys.teamKemPublic && !config.keys.teamDsaPublic) {
w.abort();
return void cb("NO_PQC_KEYS");
}
try {
// Crypto.Team.createEncryptor now supports PQC keys
crypto = Crypto.Team.createEncryptor(config.keys);
} catch (err) {
w.abort();
@ -903,6 +910,7 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
crypto: crypto,
validateKey: config.keys.teamEdPublic,
dsaValidateKey: config.keys.teamDsaPublic, // PQC validation key for ML-DSA
owners: config.owners,

View File

@ -132,7 +132,10 @@ proxy.mailboxes = {
}
var text = JSON.stringify(obj);
var ciphertext = crypto.encrypt(text, user.curvePublic);
var ciphertext = crypto.encrypt(text, user.curvePublic, user.kemPublic);
// const bytes = Crypto.CryptoAgility.decodeBase64(ciphertext);
// if (bytes[0] !== 1) throw new Error("PQC not used for encryption");
// If we've sent this message to one of our teams' mailbox, we may want to "dismiss" it
// automatically

View File

@ -13,6 +13,26 @@ const factory = (Crypto, Hash, Util, Realtime, Messaging,
Messages = data.Messages;
};
/*
* CRYPTOGRAPHIC IDENTITY NOTE:
*
* Throughout this module and the entire CryptPad application, we continue to use
* curve25519 public keys (curvePublic) as the primary identifier for users despite
* having post-quantum cryptography (PQC) keys (kemPublic) available.
*
* 1. Legacy compatibility: A complete migration to PQC for identification would require
* changing all existing user relationships and channels
*
* 2. System integration: The curvePublic key is deeply integrated into friend relationships,
* channel management, history keeper, and other core components
*
* 3. Risk management: Partially migrating identity systems can lead to inconsistencies
* across the application, potentially creating security vulnerabilities
*
* A future coordinated migration will be necessary to fully replace curve25519 with PQC,
* which will require careful planning to ensure all components are updated simultaneously.
*/
var Types = {
message: 'MSG',
unfriend: 'UNFRIEND',

View File

@ -448,7 +448,9 @@ const factory = (Util, Hash, Constants, Realtime, ProxyManager,
// Roster keys
var myKeys = {
curvePublic: ctx.store.proxy.curvePublic,
curvePrivate: ctx.store.proxy.curvePrivate
curvePrivate: ctx.store.proxy.curvePrivate,
kemPublic: ctx.store.proxy.kemPublic,
kemPrivate: ctx.store.proxy.kemPrivate,
};
var rosterData = keys.roster || {};
var rosterKeys = rosterData.edit ? Crypto.Team.deriveMemberKeys(rosterData.edit, myKeys)
@ -470,6 +472,11 @@ const factory = (Util, Hash, Constants, Realtime, ProxyManager,
if (k && !rosterKeys.teamEdPublic) {
rosterKeys.teamEdPublic = k;
}
// Also get DSA public key from cache if available
var dsaPublic = obj && obj.dsaPublic;
if (dsaPublic && !rosterKeys.teamDsaPublic) {
rosterKeys.teamDsaPublic = dsaPublic;
}
if (!c) {
waitFor.abort();
cb({error: 'NOCACHE'});
@ -653,14 +660,18 @@ const factory = (Util, Hash, Constants, Realtime, ProxyManager,
var hash = Hash.createRandomHash('team', password);
var secret = Hash.getSecrets('team', hash, password);
var roHash = Hash.getViewHashFromKeys(secret);
var keyPair = Crypto.CryptoAgility.signKeyPair(); // keyPair.secretKey , keyPair.publicKey
var keyPair = Crypto.CryptoAgility.signKeyPair(); // ed25519
var curvePair = Crypto.CryptoAgility.curveKeyPair(); // Curve25519
var curvePair = Crypto.CryptoAgility.curveKeyPair(); // curvePair.secretKey, curvePair.publicKey
var kemPair = Crypto.PQC.ml_kem.ml_kem512.keygen();
var dsaPair = Crypto.PQC.ml_dsa.ml_dsa44.keygen();
var rosterSeed = Crypto.Team.createSeed();
var rosterKeys = Crypto.Team.deriveMemberKeys(rosterSeed, {
curvePublic: ctx.store.proxy.curvePublic,
curvePrivate: ctx.store.proxy.curvePrivate
curvePrivate: ctx.store.proxy.curvePrivate,
kemPublic: ctx.store.proxy.kemPublic,
kemPrivate: ctx.store.proxy.kemPrivate,
});
var roster;
@ -680,7 +691,6 @@ const factory = (Util, Hash, Constants, Realtime, ProxyManager,
owners: [ctx.store.proxy.edPublic]
};
nThen(function (waitFor) {
// Initialize the roster
Roster.create({
network: ctx.store.network || ctx.store.networkPromise,
channel: rosterKeys.channel, //sharedConfig.rosterChannel,
@ -693,7 +703,6 @@ const factory = (Util, Hash, Constants, Realtime, ProxyManager,
}, waitFor(function (err, _roster) {
if (err) {
waitFor.abort();
console.error(err);
return void cb({error: 'ROSTER_ERROR'});
}
roster = _roster;
@ -707,7 +716,6 @@ const factory = (Util, Hash, Constants, Realtime, ProxyManager,
}));
}));
// Add yourself as owner of the chat channel
var crypto = Crypto.createEncryptor(chatSecret.keys);
var chatCfg = {
network: ctx.store.network,
@ -750,19 +758,22 @@ const factory = (Util, Hash, Constants, Realtime, ProxyManager,
var proxy = lm.proxy;
proxy.version = 2; // No migration needed
proxy.on('ready', function () {
// Store keys in our drive
var keys = {
mailbox: {
channel: Hash.createChannelId(),
viewed: [],
keys: {
curvePrivate: Util.encodeBase64(curvePair.secretKey),
curvePublic: Util.encodeBase64(curvePair.publicKey)
curvePublic: Util.encodeBase64(curvePair.publicKey),
kemPrivate: Util.encodeBase64(kemPair.secretKey),
kemPublic: Util.encodeBase64(kemPair.publicKey),
}
},
drive: {
edPrivate: Util.encodeBase64(keyPair.secretKey),
edPublic: Util.encodeBase64(keyPair.publicKey)
edPublic: Util.encodeBase64(keyPair.publicKey),
dsaPrivate: Util.encodeBase64(dsaPair.secretKey),
dsaPublic: Util.encodeBase64(dsaPair.publicKey),
},
chat: {
edit: chatHashes.editHash,
@ -783,7 +794,6 @@ const factory = (Util, Hash, Constants, Realtime, ProxyManager,
roHash: roHash,
password: password,
keys: keys,
//members: membersHashes.editHash,
metadata: {
name: data.name
}
@ -802,13 +812,11 @@ const factory = (Util, Hash, Constants, Realtime, ProxyManager,
cb();
});
}).on('error', function (info) {
if (info && typeof (info.loaded) !== "undefined" && !info.loaded) {
if (info && typeof (info.loaded) !== "undefined" && !info.loaded) {
cb({error:'ECONNECT'});
}
if (info && info.error) {
if (info.error === "EDELETED") {
closeTeam(ctx, id);
}
if (info && info.error === "EDELETED") {
closeTeam(ctx, id);
}
});
});

View File

@ -53,7 +53,7 @@ var factory = function (Util, Nacl, Scrypt, Crypto) {
'/components/tweetnacl/nacl-fast.min.js',
'/components/scrypt-async/scrypt-async.min.js',
'/components/chainpad-crypto/crypto.js'
], function (Util) {
], function (Util, nacl, Scrypt, Crypto) {
return factory(Util, window.nacl, window.scrypt, Crypto);
});
}

View File

@ -114,6 +114,7 @@ define([
var res = Utils.Crypto.Mailbox.openOwnSecretLetter(messages[0].msg, {
validateKey: data.validateKey,
ephemeral_private: Util.decodeBase64(answer.curvePrivate),
ephemeral_kem_private: Util.decodeBase64(answer.kemPrivate),
my_private: Util.decodeBase64(finalKeys.curvePrivate),
their_public: Util.decodeBase64(data.publicKey)
});

View File

@ -326,6 +326,7 @@ define([
channel: d.channel,
crypto: crypto,
validateKey: rosterKeys.teamEdPublic,
dsaValidateKey: rosterKeys.teamDsaPublic, // Add PQC validation key for ML-DSA
Cache: Cache,
noChainPad: true,
onCacheReady: onCacheReady,

View File

@ -355,6 +355,7 @@ define([
toolbar: APP.toolbar,
APP: driveAPP,
edPublic: APP.teamEdPublic,
dsaPublic: APP.teamDsaPublic,
editKey: teamData.secondaryKey
});
APP.drive = drive;