Search code examples
node.jsstringencryptionencodingelgamal

Converting ElGamal encryption from encrypting numbers to strings


I've have the following ElGamal encryption scheme

const forge = require('node-forge');
const bigInt = require("big-integer");

// Generates private and public keys
function keyPairGeneration(p, q, g) {
    var secretKey = bigInt.randBetween(2, q.minus(2));
    var publicKey = g.modPow(secretKey, p);
    const keys = {
        secret: secretKey,
        public: publicKey
    }
    return keys;
}

// Generates a proxy and a user key
function generateProxyKeys(secretKey) {
    const firstKey = bigInt.randBetween(1, secretKey);
    const secondKey = secretKey.minus(firstKey);
    const keys = {
        firstKey: firstKey,
        secondKey: secondKey
    }
    return keys;
}

// Re-encrypts 
function preEncrypt(p, q, g, m, publicKey) {
    const k = bigInt.randBetween(1, q.minus(1));
    const c1 = g.modPow(k, p);
    // g^x = publicKey
    // m.publicKey^k
    const c2 = bigInt(m).multiply(publicKey.modPow(k, p)).mod(p);
    const c = {
        c1: c1, 
        c2: c2
    }
    return c;
}

function preDecrypt(p, c1, c2, key) {
    // (mg^xr) / (g^rx1)
    var decrypt = c2.multiply(c1.modPow(key, p).modInv(p)).mod(p); 
    return decrypt;
}

Which works fine with numbers. However, I want to be able to use it to encrypt strings (btw, it's not a regular ElGamal, I don't think the difference is that relevant in this context but for more details see this question I asked)

I thought about converting the string to an integer, running the encryption, and converting back to a string whenever I needed it. I couldn't find a way of doing this in JS (there was this question posted here but the code didn't work). There is another similar question but it's in Java and the method mentioned there is not provided by the BigInt implementation in JS.

Is there any easy way of converting a string to a BigInt?


Solution

  • Arbitrarily long messages

    Asymmetric encryption should not be used to encrypt messages of arbitrary length, because it is much slower than symmetric encryption. So, we can use symmetric encryption for the actual message and asymmetric encryption for the key that encrypted the message.

    There are basically two ways for arbitrary sized messages:

    1. If prime p is big enough that it fits a common key size of a symmetric cipher such as AES, then you can simply generate a random AES key (128, 192 or 256 bit) and use an AES-derived scheme such as AES-GCM to encrypt your message. Afterwards, you decode a number from the AES key (use fromArray) to be used as m in your ElGamal-like encryption scheme. This is called hybrid encryption.

    2. Regardless how big prime p is, you can always generate a random m number in the range of 1 to p-1 and use that to produce your asymmetric ciphertext. Afterwards, you can take the previously generated m, encode it into a byte array (use toString(16) to produce a Hex-encoded string and then simply parse it as Hex for the hashing) and hash it with a cryptographic hash function such as SHA-256 to get your AES key. Then you can use the AES key to encrypt the message with a symmetric scheme like AES-GCM. This is called key encapsulation.

    The main remaining thing that you have to look out for is data format: How do you serialize the data for the asymmetric part and the symmetric part of the ciphertext? How do you read them back that you can always tell them apart? There are many possible solutions there.

    Short messages

    If the messages that you want to encrypt have a maximum size that is smaller than the prime that you use, then you don't need the two approaches above. You just need to take the byte representation of the message and convert it to a big integer. Something like this:

    var arr = Array.prototype.slice.call(Buffer.from("some message"), 0);
    var message = bigInt.fromArray(arr, 256);
    

    This is a big endian encoding.

    This makes only sense if your prime is big enough which it should be for security.