UNPKG

basic_simple_elgamal

Version:

This is a cypher engine which uses ElGamal cryptosystem internally. besides cryptography, it provide some group functionality such as add 2 members or select a random group member and so on.

645 lines (573 loc) 23.3 kB
/** * ElGamal Module * @module basic_simple_elgamal */ const bigInteger = require('big-integer'); const bigIntManager = require('./bigIntManager'); const debug = require('debug'); const https = require('https'); /** * To access offline groups in offline mode: */ const fs = require('fs/promises'); /* Below groups are just selected in offline mode and if you are in a browser: */ const defaultGroup2048Bit = require('./offlineGroups/2048/0'); const defaultGroup3072Bit = require('./offlineGroups/3072/0'); const defaultGroup4096Bit = require('./offlineGroups/4096/0'); const defaultGroup8192Bit = require('./offlineGroups/8192/0') const log = debug('app::elgamal'); const securityLevels = ['HIGH', 'LOW', 'MEDIUM'] /** * @typedef {'HIGH'|'LOW'|'MEDIUM'} securityLevel */ /** * @typedef {Object} cipherText (refer to ElGamal Schema for more information). * @property {bigInteger.BigInteger} c1 - This is half the shared secret: c1 = g^r * @property {bigInteger.BigInteger} c2 - This is encrypted message: c2 = y^r . m */ /** * @typedef {8192|4096|3072|2048} allowedLengthes */ /** * @typedef {Object} Engine the object containing essential information to build the ElGamal * cryptoengine again * @property {bigInteger.BigInteger} p - The modulus of underlying group and determine the whole Cyclic group * @property {bigInteger.BigInteger} g - The generator of underlying group. * @property {bigInteger.BigInteger} y - The public key which is your public key and others can use it to * encrypt messages for you. * @property {bigInteger.BigInteger} [x] - The private key(decryption key) which is strongly recommended to don't export it * @property {bigInteger.BigInteger} [r] - The secret key which is used in last encryption to build * the cipherText.c1 * @property {securityLevel} [security] - The engine security level. */ class ElGamal{ /** * Initialize the ElGamal Engine by giving private key, public key, modulus, group order, group generator. * @param {string|bigInteger.BigInteger} [p] modulus of multiplicative group * @param {string|bigInteger.BigInteger} [g] generator of the group * @param {string|bigInteger.BigInteger} [y] public key(encryption key), * NOTE: this is not your public key but public key of receiver. * @param {string|bigInteger.BigInteger} [x] private key(decryption key) * @throws Will throw an error if any of passed parameters is an invalid big integer! */ constructor(p, g, y, x){ /** * @property {securityLevel} [securityLevel='HIGH'] - determines the security level of the engine. * if it's set to 'HIGH' then you should use safe prime numbers as underlying group's modulus, * else if it's set to 'MEDIUM' then you should use prime numbers as modulus, * else it's set to 'LOW' then there is no condition but on other hands there is no guarantee * that the engine operates correctly. * @type {securityLevel} */ this.securityLevel = 'HIGH'; /** * @property {boolean} [isSecure=false] - this property indicates whether engine is at expected * secure level or not. If it's false then you will be unable to use the engines functionality. * to Change this value to True, please call setSecurityLevel() method. */ this.isSecure = false; /** * @property {bigInteger.BigInteger} g - The generator of underlying multiplicative group. */ try{ if(typeof g === 'string') this.g = bigInteger(g); else if(g instanceof bigInteger) this.g = g; else this.g = undefined; }catch(err){ log('Error when initializing generator:', err); throw new Error('Error: generator is not a valid big integer!'); } /** * @property {bigInteger.BigInteger} p - The modulus of underlying multiplicative group. */ try{ if(typeof p === 'string'){ this.p = bigInteger(p); }else if(p instanceof bigInteger) this.p = p; else this.p = undefined; }catch(err){ log('Error when initializing modulus:', err); throw new Error('Error: modulus is not a valid big integer'); } /** * @property {bigInteger.BigInteger} x - The private key of the ElGamal cryptosystem. */ try{ if(typeof x === 'string'){ this.x = bigInteger(x); }else if(x instanceof bigInteger) this.x = x; else this.x = undefined; }catch(err){ log('Error when initializing private key: ', err); throw new Error(`Private key is not a valid big integer`); } /** * @property {bigInteger.BigInteger} y - The public key of ElGamal encryption * @private */ try{ if(typeof y === 'string') this.y = bigInteger(y); else if(y instanceof bigInteger) this.y = y; else this.y = undefined; }catch(err){ log('Error when initializing public key: ', err); throw new Error(`Public key is not a vlid big integer`); } /** * @property {bigInteger.BigInteger} q - The group order */ if(this.p) this.q = this.p.minus(1).divide(2); } /** * Check the security of engine regarding the configured security level * @returns {boolean} true if the engine is at given security level, otherwise false * @throws Will throw an error if the engine is not initialized correctly. */ checkSecurity(){ if(this.securityLevel === 'HIGH'){ if(this.q === undefined) if(this.p === undefined) throw new Error('Engine is not initialized') else this.q = this.p.minus(1).divide(2); else if(this.q.multiply(2).add(1).compare(this.p)) throw new Error('Inconsistent Group Order and Group Modulus'); if(! this.p.isProbablePrime()) return false; else if(! this.q.isProbablePrime()) return false; }else if(this.securityLevel === 'MEDIUM'){ if(this.p === undefined) throw new Error('Engine is not initialized!'); if(! this.p.isProbablePrime()) return false; }else log('Warning: Low level security detected'); this.isSecure = true; return true; } /** * * @param {securityLevel} level * @throws Will throw an error if level is not one of allowed values */ setSecurityLevel(level){ if(securityLevels.indexOf(level.toUpperCase()) === -1) throw new Error('Invalid security level'); else this.securityLevel = level; } /** * it's better to choose the lengthes which are divided evenly by 8. * By calling this method, the engine will create a thread to find a safe prime number and * then initialize engine using it. * @async * @param {number} lengthOfOrder indicate the length of Modulus of Group in bit */ async initialize(lengthOfOrder = 4096){ this.q = undefined; do{ this.q = await bigIntManager.getPrime(lengthOfOrder-1); log(this.q, typeof this.q); this.p = this.q.shiftLeft(1).add(1); log('one prime number produced:', this.p.bitLength()); }while(!this.p.isPrime()); //produce generator: do{ let exponent = await bigIntManager.getInRange(this.p,3); this.g = bigInteger[2].modPow(exponent, this.p); }while( this.g.modPow(this.q,this.p).notEquals(1) || this.g.modPow(2,this.p).equals(1) || this.p.prev().remainder(this.g).equals(0) || this.p.prev().remainder(this.g.modInv(this.p)).equals(0) ); //Produce other parameters: await this.fillIn(); } /** * This is an internale method and you shouldn't call it directly! * Gets modulus and group order then tries to initialize ElGamal Engine. * Also p and q should be in Safe and Sophie Germain primes form. * @param {string|number} p The modulus of group, Should be prime. * @param {string|number} q The order of group, Should be prime. * @throws Will throw an error if p or q are not prime. * @throws Will throw an error if provided p and q aren't Safe and Sophie Germain primes. */ async initializeUsingModulusGroup(p, q){ if(typeof p === 'number') p = `p` if(typeof q === 'number') q = `q` this.p = bigInteger(p); this.q = bigInteger(q); //check Validity of primes: if(this.q.multiply(2).add(1).compareTo(this.p)) throw new Error(`The received primes are not in Safe form: p = ${p}, q = ${q}`); if(!this.p.isProbablePrime()) throw new Error('P is not prime: '+ p); if(!this.q.isProbablePrime()) throw new Error('q is not prime: ' + q); //produce generator: do{ let exponent = await bigIntManager.getInRange(this.p,3); this.g = bigInteger[2].modPow(exponent, this.p); }while( this.g.modPow(this.q, this.p).notEquals(1) || this.g.modPow(2,this.p).equals(1) || this.p.prev().remainder(this.g).equals(0) || this.p.prev().remainder(this.g.modInv(this.p)).equals(0) ); //Fill in the empty parameters: await this.fillIn(); } /** * it's better to choose the lengthes which are divided evenly by 8. * This method tries to connect to a remote DB and get the underlying group information * then initialize the engine. * Unlike initialize() method which creates a thread and has an enormous cpu usage, this method * don't have any cpu usage but it's need a network connection! * @async * @param {allowedLengthes} lengthOfOrder indicate the length of Modulus Of Group in bit * @throws Will throw an error if there is no internet connection or received Information are * inconsistent. */ async initializeRemotely(lengthOfOrder = 4096){ return new Promise((resolve, reject)=>{ https.get(`https://2ton.com.au/getprimes/random/${lengthOfOrder}`, (res)=>{ res.on('data', async (data)=>{ let readableData = data.toString('utf8'); let primes = JSON.parse(readableData); try{ await this.initializeUsingModulusGroup( primes.p.base10, primes.q.base10 ) }catch(err){ log(err) return reject(err) } resolve(true); }) } ).on('error', async (err)=>{ log(`Error: Unable to fetch group information from remote server!`) log(err) log(` Server Address:https://2ton.com.au/getprimes/random/${lengthOfOrder}`) log(`Redirecting this error to console and using offline groups...`) console.log(`Unable to connect to server: ${err}`); //Check if we live in browser or not: if(global?.performance?.nodeTiming?.name){ //We are in Node.js, so we can use a random group info easily. let groupIndex = Math.floor(Math.random() * 100) /** * We load a random group and keep it inside memory because we may * choose it later too: */ let group = require(`./offlineGroups/${lengthOfOrder}/${groupIndex}`); try{ await this.initializeUsingModulusGroup( group.p, group.q ) }catch(err){ log(err); return reject(err); } resolve(true) }else{ /* * We are in a browser, so we don't have access to file system and we should * use default groups: */ if(lengthOfOrder == 2048) await this.initializeUsingModulusGroup( defaultGroup2048Bit.p, defaultGroup2048Bit.q ) else if(lengthOfOrder == 3072) await this.initializeUsingModulusGroup( defaultGroup3072Bit.p, defaultGroup3072Bit.q ) else if(lengthOfOrder == 4096) await this.initializeUsingModulusGroup( defaultGroup4096Bit.p, defaultGroup4096Bit.q ) else if(lengthOfOrder == 8192) await this.initializeUsingModulusGroup( defaultGroup8192Bit.p, defaultGroup8192Bit.q ) resolve(true); } }) }) } /** * Fill in the empty parameters of ElGamal. * The engine should at least has the generator and modulus initialized. * Don't use this method directly. * @async * @throws Will throw an Error if one of Generator or Modulus is not provided. */ async fillIn(){ //produce privateKey: this.x = await bigIntManager.getInRange( this.p.prev(), 2 ); //calculate public key: if(this.y === undefined) this.y = this.g.modPow(this.x, this.p); } /** * Encrypt the given message under ElGamal cryptosystem schema. By default this use your own * public key unless you change it by setting publicKey. * @param {number|bigInteger.BigInteger} message - The message which you want to encrypt it. please note due to the ElGamal schema * it's must be a member of underlying group. * @returns {Promise<cipherText>} - The resulted cipher text which you can decrypt it by using decrypt() method. * @throws Will throw an Error if the message is not a number */ async encrypt(message){ if(! this.isSecure){ throw new Error(`Engine is not secure to use, please make sure you call checkSecurity() method before using engine.`); } if(! this.isReady()){ throw new Error(`Engine is not initialized correctly!`); } const tempPrivateKey = await bigIntManager.getInRange(this.p.prev(), 1); /** * @property {bigInteger.BigInteger} lastEncryptionKey keeps the last used encryption key. */ this.lastEncryptionKey = tempPrivateKey; let msgBI; if(typeof message === 'string'){ throw new Error('Not implemented yet! the message should of type number'); }else if(typeof message === 'number') msgBI = bigInteger(message); else if(message instanceof bigInteger) msgBI = message; else throw new Error('Not supported message type'); log(this.y.modPow(tempPrivateKey, this.p).toString()); return { c1 : this.g.modPow(tempPrivateKey, this.p), c2 : this.y.modPow(tempPrivateKey, this.p).multiply(msgBI).remainder(this.p) }; } /** * * @param {cipherText} cipherPair - The result of encrypt() method. * @returns {Promise<bigInteger.BigInteger>} - The decrypted message which is a big Integer. The message is a member of * underlying Cyclic Group. */ async decrypt(cipherPair){ if(! this.isSecure){ throw new Error(`Engine is not secure to use, please make sure you call checkSecurity() method before using engine.`); } if(! this.isReady()){ throw new Error(`Engine is not initialized correctly!`); } const sharedSecret = cipherPair.c1.modPow(this.x, this.p); const reverseSecret = sharedSecret.modInv(this.p); const plainText = reverseSecret.multiply(cipherPair.c2).mod(this.p); return plainText; } /** * Choose one of the underlying group members randomly! * @returns {Promise<bigInteger.BigInteger>} One of group members which is selected randomly. */ async randomGropuMember(){ let exponenet = await bigIntManager.getInRange(this.p, 3); return this.g.modPow(exponenet, this.p); } /** * calculate: generator^exponent mod modulus. It works independent of ElGamal, it means you * can use it even if ElGamal fails security conditions. * This method is not part of elgamal but part of basic group functionality! it's provided * here to reduce the complexity of dependencies. * @param {bigInteger.BigInteger|string} exponent - The exponent to calculate its modular exponentiation * regarding generator * @returns {bigInteger.BigInteger} The resulted modular exponentiation */ power(exponent){ if(typeof exponent === 'string') exponent = bigInteger(exponent); return this.g.modPow(exponent, this.p); } /** * calculate: a + b mod modulus. It works independent of ElGamal, it means you can use it * even if ElGamal fails security conditions. * This method is not part of elgamal but part of basic group functionality! it's provided * here to reduce the complexity of dependencies. * @param {bigInteger.BigInteger|string} a - The first number. * @param {bigInteger.BigInteger|string} b - The second number to be added with a. * @returns {bigInteger.BigInteger} - The resulted modular addition. */ add(a, b){ if(typeof a === 'string') a = bigInteger(a); if(typeof b === 'string') b = bigInteger(b); return a.add(b).mod(this.p); } /** * calculate: a * b mod modulus. It works independent of ElGamal, it means you can use it * even if ElGamal fails security conditions. * This method is not part of elgamal but part of basic group functionality! it's provided * here to reduce the complexity of dependencies. * @param {bigInteger.BigInteger|string} a - The first number * @param {bigInteger.BigInteger|string} b - The second number to be multiplying in a * @returns {bigInteger.BigInteger} - The resulted modular multiplication. */ multiply(a, b){ if(typeof a === 'string') a = bigInteger(a); if(typeof b === 'string') b = bigInteger(b); return a.multiply(b).mod(this.p); } /** * @param {bigInteger.BigInteger|string} _g Generator */ set generator(_g){ if(typeof _g === 'string') _g = bigInteger(_g); this.g = _g; } /** * @type {string} */ get generator(){ return this.g.toString(); } /** * @param {bigInteger.BigInteger|string} _q Order of inner cyclic group */ set groupOrder(_q){ if(typeof _q === 'string') _q = bigInteger(_q); this.q = _q; } /** * @type {string} */ get groupOrder(){ return this.q.toString(); } /** * @param {bigInteger.BigInteger|string} _p Modulus of Multiplicative group */ set modulus(_p){ if(typeof _p === 'string') _p = bigInteger(_p); this.p = _p; } /** * @type {string} */ get modulus(){ return this.p.toString(); } /** * This is not ElGamal public key but the public secret. * @param {bigInteger.BigInteger|string} _y public key */ set publicKey(_y){ if(typeof _y === 'string') _y = bigInteger(_y); this.y = _y; } /** * @type {string} */ get publicKey(){ return this.y.toString(); } /** * We strongly advise to avoid calling this method! * @param {bigInteger.BigInteger|string} _x private key * */ set privateKey(_x){ if(typeof _x === 'string') _x = bigInteger(_x); this.x = _x; } /** * Will return the private key if and only if the securityLevel is 'LOW'. * We strongly advise to avoid calling this method, * because this method return the flat private key and not * ElGamal private key * @type {string?} * @throws Will throw an Error if securityLevel is not 'LOW' */ get privateKey(){ if(this.securityLevel === 'LOW') return this.x.toString(); else throw new Error('Violating security policies. First set the security level to \'LOW\''); } /** * This method checks if engine is initialized or not. It doesn't check engine security. * @returns {boolean} - True if all parameters of engine are available and false otherwise. */ isReady(){ if(this.p && this.g && this.y && this.x){ return true; } return false; } /** * This method will export the ElGamal engine so that you can build it again completely. * @param {boolean} deep if true then some last secret key will be revealed and removed from * ElGamal for security sake. * @returns {Engine} The exported ElGamal engine, you can import it easily by calling import() method */ export(deep){ let r = undefined; if(deep){ r = this.lastEncryptionKey; this.lastEncryptionKey = undefined; } return { r, security: this.securityLevel, ...(this.securityLevel === 'LOW'? {x: this.x} : undefined), g: this.g, p: this.p, y: this.g.modPow(this.x, this.p) }; } /** * This method will import ElGamal engine based on passed info. * @param {Engine} engine - The ElGamal info which is exported using export() method. */ import(engine){ this.isSecure = false; this.g = engine.g; this.p = engine.p; this.y = engine.p; this.securityLevel = engine.security; this.x = engine.x; } } /** * ElGamal Engine */ module.exports = ElGamal;