mirror of
https://github.com/cryptpad/cryptpad.git
synced 2026-09-13 18:45:43 +05:00
Teams PQC addons fully working
This commit is contained in:
parent
2d58d692fb
commit
71f4e69be0
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@ -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)
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -355,6 +355,7 @@ define([
|
||||
toolbar: APP.toolbar,
|
||||
APP: driveAPP,
|
||||
edPublic: APP.teamEdPublic,
|
||||
dsaPublic: APP.teamDsaPublic,
|
||||
editKey: teamData.secondaryKey
|
||||
});
|
||||
APP.drive = drive;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user