2
\$\begingroup\$

I'm a 14year old and still learning. I know everyone says "don't roll your own crypto", however I decided I must make my own cipher in javascript as soon as I have enough knowledge.

I started simple with simple ciphers like caesar and vigenère.
But then learned about binary-based ciphers.
I even created my mutation of the aes MCS

But this was just copied aes. Now I created a smaller version of the previous crypto > MiniCiph.js.
I tried to make it compatible with all browsers.
It includes crc32 key-derivation function, encodings (Latin1, Hex, Base64, Utf8 and words↔bytes), null-padding and the new cipher Sentece (because it is word-based).
It is a binary 32bit-words - based cipher I designed.
It takes a 128bit(4 words) block, 128bit key and produces 128bit output.
It is still a bit similar to AES, but just a bit.

Sentece algorithm explanation:

input text gets converted to bytes grouped by four to 32bit words
words get grouped by 4 into 128bit blocks and goes through the encryption round below 7 times:
1) Permute words every word get xored with 0xc9a74e5b, and goes through bluring step for bytes and gets converted to an entirely different word
2) Rotate words every word gets rotated by (position * 8 - 5) bits:
1st->by 3 bits, 2dn->by 11 bits, 3rd->by 19 bits, 4th->by 27bits
3) Xor with key every word from the block gets xored with the responding word of the key
4) Permute block the whole block goes through the bluring step for words for every word of the key to have impact on every word of the block
+ Bluring step expects 4 input integers. Every int (except the 1st one) gets xored with the previous one, their order reverse and the same process is done again, ints in the original order are returned

I thought it would be much faster since it is simpler and word-based, and it is, but when it comes to input data larger than 60kB, it is suddenly very slow.

I wanted to ask why is it takes less than a second to encrypt 30kB file,
but it takes nearly 7 times more to encrypt a 80kB file?
Is it secure?
And any other suggestions about the code/performance/usage.
Also I'm not putting the file-encryption script here (it is done with FileReader)

Sentence.js

//Sentence.js v1.3 by FranaTrtur
//Copyright (c) 2020 František Artur Čech

var Sentence = {};

Sentence._SubWord = function(word, forwards){
    forwards = typeof forwards == "undefined" ? true : !!forwards;
    if(forwards)
        word ^= 3383184987;
    var bytes = [word >>> 24, word << 8 >>> 24, word << 16 >>> 24, word << 24 >>> 24];
    var rot3 = function(byte){return (byte << 3 | byte >> 5) << 24 >>> 24;};
    if(forwards){
        bytes[1] ^= bytes[0];
        bytes[2] ^= bytes[1];
        bytes[3] ^= bytes[2];
        bytes[2] ^= rot3(bytes[3]);
        bytes[1] ^= rot3(bytes[2]);
        bytes[0] ^= rot3(bytes[1]);
    }
    else{
        bytes[0] ^= rot3(bytes[1]);
        bytes[1] ^= rot3(bytes[2]);
        bytes[2] ^= rot3(bytes[3]);
        bytes[3] ^= bytes[2];
        bytes[2] ^= bytes[1];
        bytes[1] ^= bytes[0];
    }
    var fin = bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3];
    return forwards ? fin : fin ^ 3383184987;
};

Sentence._DoExpandKey = function(key){
    var rot21 = function(word){return word << 21 | word >>> 11;};
    var rotWord = Sentence._SubWord(rot21(key[3]), true);
    var word0 = rotWord ^ key[0] ^ 452984832;
    var word1 = word0 ^ key[1];
    var word2 = word1 ^ key[2];
    var word3 = word2 ^ key[3];
    return [word0, word1, word2, word3];
};

Sentence._DoCryptBlock = function(block, key, rounds){
    rounds = typeof rounds == "number" ? rounds : 7;
    var rot = function(word, shift){return word << shift | word >>> (32 - shift);};
    var temp = block.slice(0),
        tempkey = key.slice(0);
    for(var rnd = 0; rnd < rounds; rnd++){
        temp[0] = rot(Sentence._SubWord(temp[0], true), 3);
        temp[1] = rot(Sentence._SubWord(temp[1], true), 11);
        temp[2] = rot(Sentence._SubWord(temp[2], true), 19);
        temp[3] = rot(Sentence._SubWord(temp[3], true), 27);
        temp[0] ^= tempkey[0];
        temp[1] ^= temp[0] ^ tempkey[1];
        temp[2] ^= temp[1] ^ tempkey[2];
        temp[3] ^= temp[2] ^ tempkey[3];
        temp[2] ^= temp[3];
        temp[1] ^= temp[2];
        temp[0] ^= temp[1];
        tempkey = Sentence._DoExpandKey(tempkey);
    }
    return temp;
};

Sentence._DeCryptBlock = function(block, key, rounds){
    rounds = typeof rounds == "number" ? rounds : 7;
    var rot = function(word, shift){return word << shift | word >>> (32 - shift);};
    var schedule = [key];
    for(var rn = 1; rn < rounds; rn++)
        schedule.unshift(Sentence._DoExpandKey(schedule[0]));
    var temp = block.slice(0),
        tempkey;
    for(var rnd = 0; rnd < rounds; rnd++){
        tempkey = schedule[rnd];
        temp[0] ^= temp[1];
        temp[1] ^= temp[2];
        temp[2] ^= temp[3];
        temp[3] ^= temp[2] ^ tempkey[3];
        temp[2] ^= temp[1] ^ tempkey[2];
        temp[1] ^= temp[0] ^ tempkey[1];
        temp[0] ^= tempkey[0];
        temp[0] = Sentence._SubWord(rot(temp[0], 32 - 3), false);
        temp[1] = Sentence._SubWord(rot(temp[1], 32 - 11), false);
        temp[2] = Sentence._SubWord(rot(temp[2], 32 - 19), false);
        temp[3] = Sentence._SubWord(rot(temp[3], 32 - 27), false);
    }
    return temp;
};

Sentence._DoXorBlocks = function(block1, block2){
    return [
        block1[0] ^ block2[0],
        block1[1] ^ block2[1],
        block1[2] ^ block2[2],
        block1[3] ^ block2[3]
    ];
};

Sentence._RandWords = function(quantity){
    quantity = typeof quantity == "number" ? quantity : 4;
    var words = [];
    for(var wwr = 0; wwr < quantity; wwr++)
        words[wwr] = Math.floor(Math.random() * 4294967296) << 0;
    return words;
};

Sentence._DoPad = function(bytes){
    while(bytes.length % 16 != 0)
        bytes.push(0);
    return bytes;
};
Sentence._UnPad = function(bytes){
    while(bytes[bytes.length - 1] === 0)
        bytes.pop();
    return bytes;
};

Sentence._DoFormat = function(words, splitby){
    splitby = typeof splitby == "number" ? splitby : 4;
    var blocks = [];
    for(var st = 0; st < words.length; st += splitby)
        blocks.push(words.slice(st, st + splitby));
    return blocks;
};
Sentence._DeFormat = function(blocks){
    var words = [];
    for(var block = 0; block < blocks.length; block++)
        words = words.concat(blocks[block]);
    return words;
};

Sentence.CFB = {
    encrypt: function(data, key, rounds, iv){
        var blocks = Sentence._DoFormat(data);
        var init = [iv];
        for(var state = 0; state < blocks.length; state++){
            init[state] = Sentence._DoCryptBlock(init[state], key, rounds);
            blocks[state] = Sentence._DoXorBlocks(init[state], blocks[state]);
            init[state + 1] = blocks[state];
        }
        return Sentence._DeFormat(blocks);
    },
    decrypt: function(data, key, rounds, iv){
        var blocks = Sentence._DoFormat(data);
        var init = [iv].concat(blocks);
        for(var state = 0; state < blocks.length; state++){
            init[state] = Sentence._DoCryptBlock(init[state], key, rounds);
            blocks[state] = Sentence._DoXorBlocks(init[state + 1], init[state]);
        }
        return Sentence._DeFormat(blocks);
    }
};

Sentence.CTR = {
    encrypt: function(data, key, rounds, iv, cnt){
        var blocks = Sentence._DoFormat(data);
        var increment = function(cnt){
            cnt.reverse();
            for(var i = 0; i < cnt.length; i++){
                if(cnt[i] >= 4294967295)
                    cnt[i] = 0;
                else{
                    cnt[i]++;
                    break;
                }
            }
            return cnt.reverse();
        };
        var counter = Array.isArray(cnt) ? cnt : [0, 0];
        var init;
        for(var state = 0; state < blocks.length; state++){
            init = Sentence._DoXorBlocks(iv, counter);
            init = Sentence._DoCryptBlock(init, key, rounds);
            blocks[state] = Sentence._DoXorBlocks(init, blocks[state]);
            increment(counter);
        }
        return Sentence._DeFormat(blocks);
    },
    decrypt: function(data, key, rounds, iv, cnt){
        return Sentence.CTR.encrypt(data, key, rounds, iv, cnt);
    }
};

Sentence.CBC = {
    encrypt: function(data, key, rounds, iv){
        var blocks = Sentence._DoFormat(data);
        var init = [iv];
        for(var state = 0; state < blocks.length; state++){
            blocks[state] = Sentence._DoXorBlocks(blocks[state], init[state]);
            blocks[state] = Sentence._DoCryptBlock(blocks[state], key, rounds);
            init[state + 1] = blocks[state];
        }
        return Sentence._DeFormat(blocks);
    },
    decrypt: function(data, key, rounds, iv){
        var blocks = Sentence._DoFormat(data);
        var init = [iv].concat(Sentence._DoFormat(data));
        for(var state = 0; state < blocks.length; state++){
            blocks[state] = Sentence._DeCryptBlock(blocks[state], key, rounds);
            blocks[state] = Sentence._DoXorBlocks(blocks[state], init[state]);
        }
        return Sentence._DeFormat(blocks);
    }
};

Sentence.Enc = {};
Sentence.Enc.Latin1 = {
    parse: function(string){
        var bytes = [];
        for(var chr = 0; chr < string.length; chr++)
            bytes.push(string.charCodeAt(chr));
        return bytes;
    },
    stringify: function(bytes){
        var string = "";
        for(var byte = 0; byte < bytes.length; byte++)
            string += String.fromCharCode(bytes[byte]);
        return string;
    }
};

Sentence.Enc.Base64 = {
    parse: function(string){
        return Sentence.Enc.Latin1.parse(atob(string));
    },
    stringify: function(bytes){
        return btoa(Sentence.Enc.Latin1.stringify(bytes));
    }
};

Sentence.Enc.Utf8 = {
    parse: function(string){
        return Sentence.Enc.Latin1.parse(unescape(encodeURIComponent(string)));
    },
    stringify: function(bytes){
        return decodeURIComponent(escape(Sentence.Enc.Latin1.stringify(bytes)));
    }
};

Sentence.Enc.Hex = {
    parse: function(string){
        var bytes = [];
        for(var hxcode = 0; hxcode < string.length; hxcode += 2)
            bytes.push(parseInt(string.charAt(hxcode) + string.charAt(hxcode + 1), 16));
        return bytes;
    },
    stringify: function(bytes){
        var string = "";
        for(var byte = 0; byte < bytes.length; byte++)
            string += ("00" + bytes[byte].toString(16)).substr(-2);
        return string;
    }
};

Sentence.Enc.Words = {
    to: function(bytes){
        var words = [];
        for(var byte = 0; byte < bytes.length; byte += 4)
            words.push(bytes[byte] << 24 | bytes[byte + 1] << 16 | bytes[byte + 2] << 8 | bytes[byte + 3]);
        return words;
    },
    from: function(words){
        var bytes = [];
        for(var word = 0; word < words.length; word++)
            bytes = bytes.concat([words[word] >>> 24, words[word] << 8 >>> 24, words[word] << 16 >>> 24, words[word] << 24 >>> 24]);
        return bytes;
    }
};

Sentence.DeriveKey = function(string, salt){
    var crc32 = function(str){
        for(var temp, tab = [], idx = 0; idx < 256; idx++){
            temp = idx;
            for(var bb = 0; bb < 8; bb++)
                temp = 1 & temp ? 3988292384 ^ temp >>> 1 : temp >>> 1;
            tab[idx] = temp;
        }
        for(var crcint = -1, ix = 0; ix < str.length; ix++)
            crcint = crcint >>> 8 ^ tab[255 & (crcint ^ str.charCodeAt(ix))];
        return -1 ^ crcint;
    };
    var key = [];
    var saltstr = Array.isArray(salt) ? Sentence.Enc.Latin1.stringify(Sentence.Enc.Words.from(salt)) : "ªU";
    key[0] = crc32(string.split("").reverse().join("") + saltstr + string);
    key[1] = crc32(key[0] + string + saltstr.split("").reverse().join(""));
    key[2] = crc32(key[0] + string.split("").reverse().join("") + key[1] + saltstr);
    key[3] = crc32(key[2] + saltstr.split("").reverse().join("") + key[1] + string + key[0] + saltstr);
    return key;
};

Sentence.encrypt = function(string, keyword, mode, dosalt){
    mode = typeof mode == "object" ? mode : Sentence.CBC;
    dosalt = typeof dosalt == "undefined" ? true : !!dosalt;
    var salt = dosalt ? Sentence._RandWords(2) : false;
    var data = Sentence.Enc.Words.to(Sentence._DoPad(Sentence.Enc.Utf8.parse(string)));
    var key = Sentence.DeriveKey(keyword, salt);
    var iv = Sentence._DoCryptBlock(Sentence._DoXorBlocks(key, salt), Sentence._DoExpandKey(key));
    var encrypted = mode.encrypt(data, key, 7, iv);
    return {
        mode: mode,
        words: encrypted,
        salt: salt,
        key: key,
        iv: iv,
        toBytes: function(){
            return Sentence.Enc.Words.from(this.words);
        },
        toString: function(enc, dosalt){
            enc = typeof enc == "object" ? enc : Sentence.Enc.Base64;
            dosalt = this.salt == false ? false : typeof dosalt == "undefined" ? true : !!dosalt;
            var data = [];
            if(dosalt)
                data = Sentence.Enc.Latin1.parse("Salted__").concat(Sentence.Enc.Words.from(this.salt));
            data = data.concat(this.toBytes());
            return enc.stringify(data);
        }
    };
};

Sentence.decrypt = function(strOrObj, keyword, mode){
    mode = typeof mode == "object" ? mode : Sentence.CBC;
    if(typeof strOrObj == "string"){
        var parsed = Sentence.Enc.Base64.parse(strOrObj);
        var salt = Sentence.Enc.Latin1.stringify(parsed.slice(0, 8)) === "Salted__" ? Sentence.Enc.Words.to(parsed.slice(8, 16)) : false;
        var data = salt ? Sentence.Enc.Words.to(parsed.slice(16)) : parsed;
    }
    else if(typeof strOrObj == "object"){
        var salt = strOrObj.salt;
        var data = strOrObj.words;
    }
    var key = Sentence.DeriveKey(keyword, salt);
    var iv = Sentence._DoCryptBlock(Sentence._DoXorBlocks(key, salt), Sentence._DoExpandKey(key));
    var decrypted = mode.decrypt(data, key, 7, iv);
    decrypted = Sentence._UnPad(Sentence.Enc.Words.from(decrypted));
    return {
        mode: mode, 
        bytes: decrypted,
        salt: salt,
        key: key,
        iv: iv,
        toWords: function(){
            return Sentence.Enc.Words.to(this.bytes);
        },
        toString: function(enc){
            enc = typeof enc == "object" ? enc : Sentence.Enc.Utf8;
            return enc.stringify(this.bytes);
        }
    };
};

Usage example

var encrypted = MiniCiph.encrypt("string", "password");
var decrypted = MiniCiph.decrypt(encrypted, "password");

Sorry for possible language errors, english is not my native language.

\$\endgroup\$
  • 1
    \$\begingroup\$ There are a lot of loops in your code, but since you don't want to show us the actual code doing the file-encryption I'm not sure we can help you. After all, it may still go wrong there. \$\endgroup\$ – Mast Jan 2 '20 at 14:53
  • \$\begingroup\$ The file is provided as a string like: "data:image/jpeg;base64,/9j/4AAQSkZJRgAB..." \$\endgroup\$ – Franartur Čech Jan 2 '20 at 15:36
  • \$\begingroup\$ Your cipher itself should not rely on being secret itself; using the hiding of the scheme is against Kerckhoffs principle. Make it as public as possible so everybody can analyze it. \$\endgroup\$ – Maarten Bodewes Jan 13 '20 at 15:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.