Reputation: 73
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:
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.
Can some body help me in this situation? Thanks a lot in advance!
Upvotes: 6
Views: 1211
Reputation: 73
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));
}
}
}
Upvotes: 1
Reputation: 94038
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.
Upvotes: 2