Search code examples
javascriptcryptographyrsahuawei-mobile-services

Reverse engineer RSA algorithm used by HUAWEI Router in Java/Kotlin


I'm trying control my huawei router using its web api, but some things are RSA encrypted. I have the public key, but I am struggling to find the algorithm they use to encrypt things.

The code they use is below:


function doRSAEncrypt(encstring) {
    if (encstring == '') {
        return '';
    }

    if (typeof (g_moduleswitch.encrypt_enabled) == 'undefined' || g_moduleswitch.encrypt_enabled != 1) {
        return encstring;
    }

    if (g_encPublickey.e == '') {
        if (true == g_scarm_login) {
            var pubkeyArray = getPubkey();
            g_encPublickey.e = pubkeyArray[1];
            g_encPublickey.n = pubkeyArray[0];
        } else {
            getEncpubkey();
        }
    }
    var rsa = new RSAKey();
    rsa.setPublic(g_encPublickey.n, g_encPublickey.e);
    encstring = base64_encode(encstring);
    var num = encstring.length / 245;
    var restotal = '';
    for (i = 0; i < num; i++) {
        var encdata = encstring.substr(i * 245, 245);
        var res = rsa.encrypt(encdata);
        restotal += res;
    }
    if (restotal.length % 256 != 0) {
        restotal = doRSAEncrypt(encstring);
    }
    return restotal;
}




function parseBigInt(str, r) {
    return new BigInteger(str, r);
}

// PKCS#1 (type 2, random) pad input string s to n bytes, and return a bigint
function pkcs1pad2(s, n) {
    if (n < s.length + 11) { // TODO: fix for utf-8                                                           alert("Message too long for RSA");
        return null;
    }
    var ba = new Array();
    var i = s.length - 1;
    while (i >= 0 && n > 0) {
        var c = s.charCodeAt(i--);
        if (c < 128) { // encode using utf-8            
            ba[--n] = c;
        } else if ((c > 127) && (c < 2048)) {
            ba[--n] = (c & 63) | 128;
            ba[--n] = (c >> 6) | 192;
        } else {
            ba[--n] = (c & 63) | 128;
            ba[--n] = ((c >> 6) & 63) | 128;
            ba[--n] = (c >> 12) | 224;
        }
    }
    ba[--n] = 0;
    var rng = new SecureRandom();
    var x = new Array();
    while (n > 2) { // random non-zero pad        
        x[0] = 0;
        while (x[0] == 0)
            rng.nextBytes(x);
        ba[--n] = x[0];
    }
    ba[--n] = 2;
    ba[--n] = 0;
    return new BigInteger(ba);
}

// "empty" RSA key constructor
function RSAKey() {
    this.n = null;
    this.e = 0;
    this.d = null;
    this.p = null;
    this.q = null;
    this.dmp1 = null;
    this.dmq1 = null;
    this.coeff = null;
}

// Set the public key fields N and e from hex strings
function RSASetPublic(N, E) {
    if (N != null && E != null && N.length > 0 && E.length > 0) {
        this.n = parseBigInt(N, 16);
        this.e = parseInt(E, 16);
    } else alert("Invalid RSA public key");
}

// Perform raw public operation on "x": return x^e (mod n)
function RSADoPublic(x) {
    return x.modPowInt(this.e, this.n);
}

// Return the PKCS#1 RSA PKCS#1 RSA encryption of "text" as an even-length hex string
function RSAEncrypt(text) {
    var m = pkcs1pad2(text, (this.n.bitLength() + 7) >> 3);
    if (m == null)
        return null;
    var c = this.doPublic(m);
    if (c == null)
        return null;
    var h = c.toString(16);
    if ((h.length & 1) == 0)
        return h;
    else        
        return "0" + h;
}

// Return the PKCS#1 RSA encryption of "text" as a Base64-encoded string
function RSAEncryptB64(text) {
    var h = this.encrypt(text);
    if (h)
        return hex2b64(h);
    else        
        return null;
}

// protectedRSAKey.prototype.doPublic = RSADoPublic;

// publicRSAKey.prototype.setPublic = RSASetPublic;
RSAKey.prototype.encrypt = RSAEncrypt;
RSAKey.prototype.encrypt_b64 = RSAEncryptB64;



function base64_encode(input) {
    _keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
    var output = "";
    var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
    var i = 0;
    input = _utf8_encode(input);
    while (i < input.length) {
        chr1 = input.charCodeAt(i++);
        chr2 = input.charCodeAt(i++);
        chr3 = input.charCodeAt(i++);
        enc1 = chr1 >> 2;
        enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
        enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
        enc4 = chr3 & 63;
        if (isNaN(chr2)) {
            enc3 = enc4 = 64;
        } else if (isNaN(chr3)) {
            enc4 = 64;
        }
        output = output + this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) + this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
    }
    return output;
}



function _utf8_encode(string) {
    string = string.replace(/\r\n/g, "\n");
    var utftext = "";
    for (var n = 0; n < string.length; n++) {
        var c = string.charCodeAt(n);
        if (c < 128) {
            utftext += String.fromCharCode(c);
        } else if ((c > 127) && (c < 2048)) {
            utftext += String.fromCharCode((c >> 6) | 192);
            utftext += String.fromCharCode((c & 63) | 128);
        } else {
            utftext += String.fromCharCode((c >> 12) | 224);
            utftext += String.fromCharCode(((c >> 6) & 63) | 128);
            utftext += String.fromCharCode((c & 63) | 128);
        }
    }
    return utftext;
}

For modulus d5eeead43ba5133e06cce6703b713db54331141d2707b8701a532173904b4e3bfca4bf73cdb7c56a640319299a083c780fa39d0fdc50aca6e0ea5d39c605cf90b88b33ed71126eea437fcd383576b11276df99425807e4c43bde60fcef38a11a6cbfb327377240b42dcf9e3d3abc1f37e62ca7efebfa247e879adeea9a395ed889916e91fd83199539dd9063f6fc306b106245b630f13ffea18eae7a486316b2c27b551214fd202993581276dfc407047f3f2da3e44161590b4cf5e12eab81633396b0eb17e487b3a12dd4a2a87726b487309801e0984ca222706127018e917ddbad3e9d9a7107e3cb54a9dad49ae7ba77252547758c2a3cf8c2c56e17b13591 and exponent 010001 (both in hex) the string test123test yields 59a5a4fe4056612c9892ea2d5108c1e348b2fb70a85a1f0c7b112bfddf058f969e8fc5e797875f63b75eb59160c1df77c7d23dbe481905226d2001f32b4eee59ec795bd113c3606096f8cbdab8ada6d6df00f1621635b7dec60ba1814208a39a3aba2ea527bda2ea522f6b65273525e7fe03478a154904debb11b7523c50432cda45a3638a6b65f768acbcc3ef1ea5c11235e39343042ea570bf220bbad05973e5c6d58af3e64c0ad183b946252c801567fa11029bdc667d2144413a5f943813ef8daf2318079204ec615e68a871e29556cd43495e3ea0a84adbe6b2dbd7e4a8d3be78bdf64a61db1a8c1dd9683499ce9241dbd6ea5bee0325944d01a17d5199 using their encryption.

I tried various RSA modes from Bouncycastle in Java like

  • RSA/ECB/OAEPWITHMD5ANDMGF1PADDING
  • RSA/ECB/OAEPWITHSHA1ANDMGF1PADDING
  • RSA/ECB/OAEPWITHSHA-256ANDMGF1PADDING
  • RSA/ECB/OAEPWITHSHA-384ANDMGF1PADDING
  • RSA/ECB/OAEPWITHSHA-512ANDMGF1PADDING
  • RSA/ECB/OAEPWithSHA-1AndMGF1Padding
  • RSA/ECB/PKCS1Padding

but none of them yielded the same result as the Huawei encrypt function did


Solution

  • import java.io.ByteArrayOutputStream
    import java.math.BigInteger
    import java.security.SecureRandom
    import java.util.*
    import kotlin.math.ceil
    import kotlin.math.min
    
    class RSAEncrypt(modulo: String, exponent: String) {
        private val modulo = BigInteger(modulo, 16)
        private val exponent = BigInteger(exponent, 16)
    
        fun doRSAEncrypt(encstring: String): String {
            val base64String = Base64.getEncoder().encodeToString(encstring.toByteArray())
            val num = base64String.length / 245.0
            var restotal = ""
            for (i in 0 until ceil(num).toInt()) {
                val encryptedData = base64String.substring(i * 245, min(245, base64String.length))
                val res = this.encrypt(encryptedData);
                restotal += res;
            }
    
            if (restotal.length % 256 !== 0) {
                restotal = doRSAEncrypt(base64String)
            }
    
            return restotal
        }
    
        private fun pkcs1pad2(s: String, n: Int): BigInteger? {
            if (n < s.length + 11) { // TODO: fix for utf-8
                return null
            }
    
            var n = n
            val ba = MutableList(n) { 0 }
            var i = s.length - 1;
            while (i >= 0 && n > 0) {
                val c = Character.codePointAt(s, i--)
                if (c < 128) { // encode using utf-8
                    ba[--n] = c;
                } else if ((c > 127) && (c < 2048)) {
                    ba[--n] = (c and 63) or 128;
                    ba[--n] = (c shr 6) or 192;
                } else {
                    ba[--n] = (c and 63) or 128;
                    ba[--n] = ((c shr 6) and 63) or 128;
                    ba[--n] = (c shr 12) or 224;
                }
            }
            ba[--n] = 0;
            val rng = SecureRandom()
            val x = ByteArray(n) { 0 }
            while (n > 2) {
                x[0] = 0;
                while (x[0].toInt() == 0) {
                    rng.nextBytes(x)
                }
                ba[--n] = x[0].toInt()
            }
            ba[--n] = 2;
            ba[--n] = 0;
    
            val baos = ByteArrayOutputStream()
            for (item in ba) {
                if (item < -128 || item > 127) {
                    throw Exception("You code is shit")
                }
    
                baos.write(item)
            }
    
            return BigInteger(baos.toByteArray())
        }
    
        private fun doPublic(x: BigInteger): BigInteger {
            return x.modPow(this.exponent, this.modulo)
        }
    
        private fun encrypt(text: String): String? {
            val maxLength = (this.modulo.bitLength() + 7) shr 3
            val m = pkcs1pad2(text, maxLength) ?: return null
    
            val c = this.doPublic(m)
    
            var h = c.toString(16);
            val length = h.length;
    
            // fix zero before result
            for (i in 0 until maxLength * 2 - length) {
                h = "0$h";
            }
    
            return h
        }
    }
    
    

    Implemented in Kotlin