6

At first glance, I have the same problem as many. But my case a bit more complex.

Preconditions:
Project language: Java 11
Network Server: Orbiwise NS (https://eu.saas.orbiwise.com/)
Device: (STM32 + Wifi module) connection via Lorawan gateway to Orbiwise and using TCP socket via wifi.

Input data:
From TCP socket received byte array:

40 24 fa fa 01 c2 c5 25  03 06 01 43 a4 99 5a c1
85 71 0c 87 38 84 53 9a  80 6c 5a 14 da f8 ff 7c
21 83 8f 78 8e ec f2 7d  4e 4e 07  

(43 bytes)

On Orbiwise have corresponding to it uplink payload:

31 19 10 07 01 13 51 25  09 01 00 00 00 00 33 04
00 00 5A 00 00 00 EB 0D  00 00 64 EB 

(28 bytes)

Task:
Decrypt data from TCP socket to have decrypted payload in the same format as on Orbiwise

Approaches were used without positive result:

  1. https://github.com/jsubercaze/javalora
  2. https://github.com/huahang/crypto-utils/blob/master/crypto-utils/src/main/java/im/chic/utils/crypto/AesUtils.java
  3. https://github.com/matthiaszimmermann/ttn_decoder_java - core code for mine.

All project above was written a long time ago and not helped me. This one helped, but written on Node JS: https://github.com/anthonykirby/lora-packet

From LoraWan specification I got that used "AES/ECB/NoPadding" method and my current code looks like:

package org.thethingsnetwork.main.java.org.thethingsnetwork.util.security;

import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

public class Crypto {

    /**
     * Decrypts TTN data_raw payload to data according to the TTN REST API.
     * @param pld encrypted message payload from ttn mqtt message
     * @param K the TTN application key 
     * @param IV
     * @return decrypted payload
     * @throws Exception
     */
    public static byte[] decrypt(byte [] pld, byte [] K, byte [] IV) throws Exception {
        byte [] devAddr = getDevAddr(pld);
        byte [] frameCounter = getFrameCounter(pld);
        byte [] result = initializeResult(pld);
        byte [] Ai = new byte[16];
        byte [] Si = null;

        for(int i = 0; i < result.length; i += 16) {
            int blockSeqCnt = (i >> 4) + 1;

            computeAi(Ai, devAddr, frameCounter, blockSeqCnt);

            Si = encryptAES(Ai, K, IV);

            for(int j=0; j < 16 && i+j < result.length; j++) {
                result[i+j] ^= Si[j];
            }
        }
        return  result;
    }

    /**
     * Converts TTN payload data to data_plain according to the TTN REST API.
     * Decode a text using base 64 decoding. 
     * @param decryptedText
     * @return
     */
    public static String toPlainText(String decryptedText) {
        byte [] data = Base64.getDecoder().decode(decryptedText);
        StringBuffer plain = new StringBuffer();

        for(int i = 0; i < data.length; i++) {
            plain.append((char)data[i]);
        }

        return plain.toString();
    }


    public static byte [] getDevAddr(byte [] payload) {
        byte [] devAddr = new byte[4];
        System.arraycopy(payload, 1, devAddr, 0, 4);
        return devAddr;
    }

    public static byte [] getFrameCounter(byte [] payload) {
        byte [] frameCounter = new byte[2];
        System.arraycopy(payload, 6, frameCounter, 0, 2);
        return frameCounter;
    }

    public static byte [] initializeResult(byte [] payload) {
        byte [] result = new byte[payload.length - 13];

        for(int i = 0; i < result.length; i++) {
            result[i] = payload[i+9];
        }

        return result;
    }

    public static void computeAi(byte [] a, byte [] devAddr, byte [] frameCounter, int blockSeqCnt) {
        a[0]  = 0x01;
        a[1]  = 0x00;
        a[2]  = 0x00;
        a[3]  = 0x00;
        a[4]  = 0x00;
        a[5]  = 0;               // 0 for uplink frames 1 for downlink frames;
        a[6]  = devAddr[0];      // LSB devAddr 4 bytes
        a[7]  = devAddr[1];      // ..
        a[8]  = devAddr[2];      // ..
        a[9]  = devAddr[3];      // MSB
        a[10] = frameCounter[0]; // LSB framecounter
        a[11] = frameCounter[1]; // MSB framecounter
        a[12] = 0x00;            // Frame counter upper Bytes
        a[13] = 0x00;
        a[14] = 0x00;
        a[15] = (byte)blockSeqCnt;  // block sequence counter 1,2,3...
    }

    /**
     * AES encrpytion.
     */
    public static byte[] encryptAES(byte [] data, byte [] key, byte [] iv) throws Exception {
        Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding", "SunJCE");
        SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
        cipher.init(Cipher.DECRYPT_MODE, keySpec);
        return cipher.doFinal(data);
    }

}

And second class - test:

package org.thethingsnetwork.util.security;

import org.apache.commons.codec.DecoderException;
import org.junit.Assert;
import org.junit.Test;
import org.thethingsnetwork.main.java.org.thethingsnetwork.util.security.Crypto;
import org.apache.commons.codec.binary.Hex;

public class CryptoTest {

private byte [] SEMTECH_DEFAULT_KEY = Hex.decodeHex("2E12E8BD30FE2FB2D8DE609747D2569F".toCharArray());

    public static final byte [] IV = new byte [] {
            0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
            0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0
    };  //  initialization vector – IV

    // example data from a ttn message
    public static final byte[] TEXT_RAW   = new byte[] {0x40, 0x24, (byte) 0xfa, (byte) 0xfa, 0x01, (byte) 0xc2,
            (byte) 0xc5, 0x25, 0x03, 0x06, 0x01, 0x43, (byte) 0xa4, (byte) 0x99, 0x5a, (byte) 0xc1, (byte) 0x85, 0x71,
            0x0c, (byte) 0x87, 0x38, (byte) 0x84, 0x53, (byte) 0x9a, (byte) 0x80, 0x6c, 0x5a, 0x14, (byte) 0xda,
            (byte) 0xf8, (byte) 0xff, 0x7c, 0x21, (byte) 0x83, (byte) 0x8f, 0x78, (byte) 0x8e, (byte) 0xec, (byte) 0xf2,
            0x7d, 0x4e, 0x4e, 0x07};

    public CryptoTest() throws DecoderException {
    }

    @Test
    public void testDecoder() throws Exception {
        byte[] decryptedText = Crypto.decrypt(TEXT_RAW, SEMTECH_DEFAULT_KEY, IV);

        printTheByteToString(decryptedText);
    }

    /**
     * Method for prin in command line byte array. For debug necessary
     * @param b - input byte array
     */
    private void printTheByteToString (byte[] b) {
        for (byte val : b) {
            System.out.print(String.format("%02x ", val));
        }
    }
}

I've checked the code a hundred times but no positive result - the decrypted message is different from on Orbiwise.

I've checked the data using NodeJs project (https://github.com/anthonykirby/lora-packet) and all is correct. But can't resolve the task using my Java code.
enter image description here

Can some body help me in this situation? Thanks a lot in advance!

Maarten Bodewes
  • 90,524
  • 13
  • 150
  • 263
FlySpot
  • 73
  • 5
  • OK, final comment, as this is going to take too much time. I'd try and replicate the output of that nice JS library. That way you can be relatively certain that you have the same values. And yes, that probably means that you are required to do a bit more parsing. – Maarten Bodewes Dec 28 '19 at 12:06

2 Answers2

2

Your ciphertext offset is off by 2 (as there are 3 option bytes instead of 1, no doubt). Printing out intermediate results or performing full parsing of the header bytes should show you that, which is why I mentioned it in the comments. Note that increasing the offset of 9 by 2 may also affect the ciphertext size, as the end of the ciphertext is fixed.

Furthermore, you are using Cipher.DECRYPT_MODE while the protocol only uses the cipher in forward mode. Counter mode (used by CCM and CCM* ciphers) only uses the cipher in the forward mode as they generate a key stream that is then XOR-ed with the plaintext stream or ciphertext stream to encrypt / decrypt respectively. The "decryption" part in the protocol only is about performing the final XOR of the generated key stream to the ciphertext rather than the plaintext.

Of course, ECB mode - used to simply implement a single block encrypt in this case - doesn't require an IV, so that part of the code is spurious.

Maarten Bodewes
  • 90,524
  • 13
  • 150
  • 263
  • Sorry for my mistakes in code, I'm not so good programmer.. IV - was from previous realization and it outdated part. All other I didn't got: 1. "ciphertext offset is off by 2" - I don't use offset For my example: 40 24 fa fa 01 c2 c5 25 03 06 01 43 a4 99 5a c1 85 71 0c 87 38 84 53 9a 80 6c 5a 14 da f8 ff 7c 21 83 8f 78 8e ec f2 7d 4e 4e 07 40 //MHDR 24 fa fa 01 //DevAddr c2 //FCtrl c5 25 //FCnt 03 //FPort 06 01 43 a4 99 5a c1 85 71 0c 87 38 84 53 9a 80 6c 5a 14 da f8 ff 7c 21 83 8f 78 8e ec f2 //Payload 7d 4e 4e 07 // MIC – FlySpot Dec 28 '19 at 13:12
  • 2. Cipher.DECRYPT_MODE - for decryption and in Lora specification wrote: "The encryption scheme used is based on the generic algorithm described in IEEE 6 802.15.4/2006 Annex B [IEEE802154] using AES with a key length of 128 bits. Encryption and decryption of the payload is done by truncating (pld | pad16) xor S to the first len(pld) octets." – FlySpot Dec 28 '19 at 13:12
  • From this point I really bewildered which 3 changes I need do for the code work correct? – FlySpot Dec 28 '19 at 13:14
  • 1
    Yeah, ignore that last part. Just XOR'ing as you do up to the end of the plaintext is more efficient and absolutely identical. The encryption / decryption part is *just* using XOR. The generation of what is called the *key stream* is using a form of counter mode. See Wikipedia counter mode to understand. – Maarten Bodewes Dec 28 '19 at 13:14
  • 1
    1. add 2 to the offset for the ciphertext (`result` in your code - ugh). That would be 9 to 11. 2. remove 2 from the size (I think) of the ciphertext length. 3. change `DECRYPT_MODE` to `ENCRYPT_MODE`. That's all folks. Print out the hex of the ciphertext if you are not convinced. – Maarten Bodewes Dec 28 '19 at 13:16
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/204983/discussion-between-flyspot-and-maarten-reinstate-monica). – FlySpot Dec 28 '19 at 13:22
1

Big thank to Maarten in solving the issue.
as result - correct code looks like:

import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

public class Crypto {

    /**
     * Decrypts TTN data_raw payload to data according to the TTN REST API.
     * @param pld encrypted message payload from ttn mqtt message
     * @param K the TTN application key
     * @return decrypted payload
     * @throws Exception
     */
    public static byte[] decrypt(byte [] pld, byte [] K) throws Exception {
        byte [] devAddr = getDevAddr(pld);
        byte [] frameCounter = getFrameCounter(pld);
        byte [] result = initializeResult(pld);
        byte [] Ai = new byte[16];
        byte [] Si = null;

        for(int i = 0; i < result.length; i += 16) {
            int blockSeqCnt = (i >> 4) + 1;

            computeAi(Ai, devAddr, frameCounter, blockSeqCnt);
            Si = encryptAES(Ai, K);

            for(int j=0; j < 16 && i+j < result.length; j++) {
                result[i+j] ^= Si[j];
            }
        }
        return  result;
    }

    public static byte [] getDevAddr(byte [] payload) {
        byte [] devAddr = new byte[4];
        System.arraycopy(payload, 1, devAddr, 0, 4);
        return devAddr;
    }

    public static byte [] getFrameCounter(byte [] payload) {
        byte [] frameCounter = new byte[2];
        System.arraycopy(payload, 6, frameCounter, 0, 2);
        return frameCounter;
    }

    public static byte [] initializeResult(byte [] payload) {
        byte [] result = new byte[payload.length - 15];

        for(int i = 0; i < result.length; i++) {
            result[i] = payload[i+11];
        }

        return result;
    }

    public static void computeAi(byte [] a, byte [] devAddr, byte [] frameCounter, int blockSeqCnt) {
        a[0]  = 0x01;
        a[1]  = 0x00;
        a[2]  = 0x00;
        a[3]  = 0x00;
        a[4]  = 0x00;
        a[5]  = 0;               // 0 for uplink frames 1 for downlink frames;
        a[6]  = devAddr[0];      // LSB devAddr 4 bytes
        a[7]  = devAddr[1];      // ..
        a[8]  = devAddr[2];      // ..
        a[9]  = devAddr[3];      // MSB
        a[10] = frameCounter[0]; // LSB framecounter
        a[11] = frameCounter[1]; // MSB framecounter
        a[12] = 0x00;            // Frame counter upper Bytes
        a[13] = 0x00;
        a[14] = 0x00;
        a[15] = (byte)blockSeqCnt;  // block sequence counter 1,2,3...
    }

    /**
     * AES encrpytion.
     * @param data
     * @param key
     * @return
     * @throws Exception
     */
    public static byte[] encryptAES(byte [] data, byte [] key) throws Exception {
        Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding", "SunJCE");
        SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
        cipher.init(Cipher.ENCRYPT_MODE, keySpec);
        return cipher.doFinal(data);
    }

}

And test class:

import org.apache.commons.codec.DecoderException;
import org.junit.Assert;
import org.junit.Test;
import org.security.Crypto;
import org.apache.commons.codec.binary.Hex;

public class CryptoTest {

private byte [] SEMTECH_DEFAULT_KEY = Hex.decodeHex("2E12E8BD30FE2FB2D8DE609747D2569F".toCharArray());

    // example data from a ttn message
    public static final byte[] TEXT_RAW   = new byte[] {0x40, 0x24, (byte) 0xfa, (byte) 0xfa, 0x01, (byte) 0xc2,
            (byte) 0xc5, 0x25, 0x03, 0x06, 0x01, 0x43, (byte) 0xa4, (byte) 0x99, 0x5a, (byte) 0xc1, (byte) 0x85, 0x71,
            0x0c, (byte) 0x87, 0x38, (byte) 0x84, 0x53, (byte) 0x9a, (byte) 0x80, 0x6c, 0x5a, 0x14, (byte) 0xda,
            (byte) 0xf8, (byte) 0xff, 0x7c, 0x21, (byte) 0x83, (byte) 0x8f, 0x78, (byte) 0x8e, (byte) 0xec, (byte) 0xf2,
            0x7d, 0x4e, 0x4e, 0x07};
    public static final String TEXT       = "31 19 10 07 01 13 51 25 09 01 00 00 00 00 33 04 00 00 5a 00 00 00 eb 0d 00 00 64 eb";

    public CryptoTest() throws DecoderException {
    }

    @Test
    public void testDecoder() throws Exception {
        byte[] decryptedText = Crypto.decrypt(TEXT_RAW, SEMTECH_DEFAULT_KEY);

        printTheByteToString(decryptedText);
        Assert.assertEquals("decrypted text does not match", TEXT, decryptedText);
    }

    /**
     * Method for prin in command line byte array. For debug necessary
     * @param b - input byte array
     */
    private void printTheByteToString (byte[] b) {
        for (byte val : b) {
            System.out.print(String.format("%02x ", val));
        }
    }
}
FlySpot
  • 73
  • 5