Bubble Encrypt > Xano Decrpyt

Options
Hi community

I need the assistance of someone who understand encrypting and decrypting better than me

I have been collaborating with Lindsay Smith (https://www.knowcode.tech) whom has developed a bubble plugin AES 256 encrypt tool - and has now modified it to work with the XANO IV (nonce) set to 12 instead of 16.

However - I’m still having trouble making it work. Here is my logic so far.

It appears the bubble plug-in encrypts the string in a HEX format, but also SALTS the response so it’s different every time.

In XANO - I can take the HEX string and convert to binary using the HEX2BIN filter - however I’m stuck at how to interpret the SALT portion.

Is anyone familiar with decrypting salted strings in XANO and how to filter it to work?

Comments

  • Ray Deck
    Ray Deck Trusted Xano Expert ✭✭✭
    Options
    The nocode part of Xano wants the IV and key. One can derive the key from the salt and a known passphrase, and the IV is often part of the payload. 

    Bubble plugins are usually written in javascript - and Xano supports NodeJS (with the crypto module) too! So subject to how this is built, we could take it apart again with a lambda even if the simpler nocode interface doesn't handle it.

    Those are broad observations, but basically it's hard to know how to proceed without seeing the way the payload was built. Is there a github for the plugin?
  • Sam Bevis
    Options
     thanks for responding.

    Ill paste the code below, however formatting is pretty terrible on this "circle" platform. The link to view the code in bubble is here (click actions on left and scroll down): 
    ENCRYPT TEXT:
    function(properties, context) {

        const crypto = require('crypto');

        const algorithm = 'aes-256-gcm';
        let   ivLength = 16;
       // const saltLength = 64;
        const saltLength = 0;
        
        const tagLength = 16;

        let iv;

        
        if (properties.iv_length) {
            ivLength = properties.iv_length;
        }
        
        if (properties.initialisationVector) {
            const encoder = new TextEncoder();
    iv = encoder.encode(properties.initialisationVector);
        }
        else {
            iv = crypto.randomBytes(ivLength);
        }
        
        let debug = 'IV: ' + iv;
        debug += '\nivLength: ' + ivLength
        
        const tagPosition = saltLength + ivLength;
        const encryptedPosition = tagPosition + tagLength;

        
        function Cryptr(secret) {
            if (!secret || typeof secret !== 'string') {
                throw new Error('Cryptr: secret must be a non-0-length string');
            }

            function getKey(salt) {
                return crypto.pbkdf2Sync(secret, salt, 100000, 32, 'sha512');
            }

            this.encrypt = function encrypt(value) {
                if (value == null) {
                    throw new Error('value must not be null or undefined');
                }
                
                let salt = '';
                if (saltLength.length !== 0) {
                    salt = crypto.randomBytes(saltLength);
                }

                const key = getKey(salt);

                const cipher = crypto.createCipheriv(algorithm, key, iv);
                const encrypted = Buffer.concat([cipher.update(String(value), 'utf8'), cipher.final()]);

                const tag = cipher.getAuthTag();

                return Buffer.concat([salt, iv, tag, encrypted]).toString('hex');
            };

        }

        try{
            const cryptr = new Cryptr(context.keys.key); 
            const encryptedString = cryptr.encrypt(properties.input_string);
            return {"output_string": encryptedString, "debug": debug};
        } catch (err) {
            
            debug += ' Error:' + err;
            return {"error": true, "debug": debug}
        }
    }

    ________________________________________________________________________________

    DECRYPT TEXT:

    function(properties, context) {

        const crypto = require('crypto');

        const algorithm = 'aes-256-gcm';
        let ivLength = 16;
        const saltLength = 64;
        let tagLength = 16;
        
        if (properties.iv_length){
              ivLength = properties.iv_length; 
        }
        
        const tagPosition = saltLength + ivLength;
        const encryptedPosition = tagPosition + tagLength;
        let debug = 'IV length: ' + ivLength;


        function Cryptr(secret) {
            if (!secret || typeof secret !== 'string') {
                throw new Error('Cryptr: secret must be a non-0-length string');
            }

            function getKey(salt) {
                return crypto.pbkdf2Sync(secret, salt, 100000, 32, 'sha512');
            }

            this.encrypt = function encrypt(value) {
                if (value == null) {
                    throw new Error('value must not be null or undefined');
                }

                const iv = crypto.randomBytes(ivLength);
                const salt = crypto.randomBytes(saltLength);

                const key = getKey(salt);

                const cipher = crypto.createCipheriv(algorithm, key, iv);
                const encrypted = Buffer.concat([cipher.update(String(value), 'utf8'), cipher.final()]);

                const tag = cipher.getAuthTag();

                return Buffer.concat([salt, iv, tag, encrypted]).toString('hex');
            };

            this.decrypt = function decrypt(value) {
                if (value == null) {
                    throw new Error('value must not be null or undefined');
                }

                const stringValue = Buffer.from(String(value), 'hex');

                const salt = stringValue.slice(0, saltLength);
                const iv = stringValue.slice(saltLength, tagPosition);
                const tag = stringValue.slice(tagPosition, encryptedPosition);
                const encrypted = stringValue.slice(encryptedPosition);

                const key = getKey(salt);

                const decipher = crypto.createDecipheriv(algorithm, key, iv);

                decipher.setAuthTag(tag);

                return decipher.update(encrypted) + decipher.final('utf8');
            };
        }

        
        try{
            const cryptr = new Cryptr(context.keys.key); 
            const decryptedString = cryptr.decrypt(properties.input_string);
            return {"output_string": decryptedString, "debug": debug};
        } catch (err) {
                    
            debug += ' Error:' + err;
            return {"error": true, "debug": debug}
        }
    }
  • Ray Deck
    Ray Deck Trusted Xano Expert ✭✭✭
    Options
    This is great! Super helpful.

    The salt is a value that you can feed to a password-based key derivation frunction - along with your passphrase - to generate the actual key for decoding the encrypted string. Xano doesn't - afaik - support that through the nocode interface.

    But! A Xano lambda exposes the same nodejs crypto module that the bubble editor uses, so you can use that great JS code above directly in Xano. Replace mentions of properties and context with $var, and you can use variables defined in the function stack. At the end of your function, cleartext pops out.

    This makes the actual implementation a bit more ceremony, but entirely doable, and with the crypto concerns wrapped up in a lambda, looks more like secure legos.

    Let me know if I can help better!

    I also run the State Change PRO group, where we discuss sharp-end-of-the-stick questions like this in office hours every day.