mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-09-07 23:16:09 +02:00
391 lines
12 KiB
Java
391 lines
12 KiB
Java
/* Copyright (C) 2024 Damien Gaignon, Martin.JM, Vitalii Tomin
|
|
|
|
This file is part of Gadgetbridge.
|
|
|
|
Gadgetbridge is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published
|
|
by the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Gadgetbridge is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|
|
|
/* TLV parsing and serialisation thanks to https://github.com/yihleego/tlv */
|
|
package nodomain.freeyourgadget.gadgetbridge.devices.huawei;
|
|
|
|
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants.CryptoTags;
|
|
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.ByteOrder;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.List;
|
|
import java.util.Objects;
|
|
|
|
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCrypto.CryptoException;
|
|
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket.ParamsProvider;
|
|
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
|
|
|
public class HuaweiTLV {
|
|
@Override
|
|
public boolean equals(Object o) {
|
|
if (this == o) return true;
|
|
if (o == null || getClass() != o.getClass()) return false;
|
|
HuaweiTLV huaweiTLV = (HuaweiTLV) o;
|
|
return Objects.equals(valueMap, huaweiTLV.valueMap);
|
|
}
|
|
|
|
public static class TLV {
|
|
private final byte tag;
|
|
private final byte[] value;
|
|
|
|
public TLV(byte tag, byte[] value) {
|
|
this.tag = tag;
|
|
this.value = value;
|
|
}
|
|
|
|
public byte getTag() {
|
|
return tag;
|
|
}
|
|
|
|
public byte[] getValue() {
|
|
return value;
|
|
}
|
|
|
|
public int length() {
|
|
return 1 + VarInt.getVarIntSize(value.length) + value.length;
|
|
}
|
|
|
|
public byte[] serialize() {
|
|
return ByteBuffer.allocate(this.length())
|
|
.put(tag)
|
|
.put(VarInt.putVarIntValue(value.length))
|
|
.put(value)
|
|
.array();
|
|
}
|
|
|
|
public String toString() {
|
|
return "{tag: " + Integer.toHexString(tag & 0xFF) + " - Value: " + StringUtils.bytesToHex(value) + "} - ";
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object o) {
|
|
if (this == o) return true;
|
|
if (o == null || getClass() != o.getClass()) return false;
|
|
TLV tlv = (TLV) o;
|
|
return tag == tlv.tag && Arrays.equals(value, tlv.value);
|
|
}
|
|
}
|
|
|
|
private static final Logger LOG = LoggerFactory.getLogger(HuaweiTLV.class);
|
|
|
|
protected List<TLV> valueMap;
|
|
|
|
public HuaweiTLV() {
|
|
this.valueMap = new ArrayList<>();
|
|
}
|
|
|
|
public int length() {
|
|
int length = 0;
|
|
for (TLV tlv : valueMap)
|
|
length += tlv.length();
|
|
return length;
|
|
}
|
|
|
|
/**
|
|
* Parse byte buffer into this HuaweiTLV
|
|
* @param buffer The buffer to parse
|
|
* @param offset The offset to start parsing at
|
|
* @param length The length to parse
|
|
* @return The HuaweiTLV object itself
|
|
* @throws ArrayIndexOutOfBoundsException There are two general cases in which this exception
|
|
* can be thrown:
|
|
* 1. offset + length is greater than the buffer length
|
|
* 2. The buffer is malformed which causes an element size to be larger than the remaining
|
|
* buffer length
|
|
*/
|
|
public HuaweiTLV parse(byte[] buffer, int offset, int length) {
|
|
if (buffer == null)
|
|
return null;
|
|
int parsed = 0;
|
|
while (parsed < length) {
|
|
// Tag is 1 byte
|
|
byte tag = buffer[offset + parsed];
|
|
parsed += 1;
|
|
// It seems that there can be an extra null byte at the end of something encrypted
|
|
// If that happens we ignore it
|
|
if (parsed == length && tag == 0)
|
|
break;
|
|
// Size is a VarInt >= 1 byte
|
|
VarInt varInt = new VarInt(buffer, offset + parsed);
|
|
int size = varInt.dValue;
|
|
parsed += varInt.size;
|
|
byte[] value = new byte[size];
|
|
System.arraycopy(buffer, offset + parsed, value, 0, size);
|
|
put(tag, value);
|
|
parsed += size;
|
|
}
|
|
LOG.debug("Parsed TLV: " + this);
|
|
return this;
|
|
}
|
|
|
|
public HuaweiTLV parse(byte[] buffer) {
|
|
if (buffer == null) {
|
|
return null;
|
|
}
|
|
return parse(buffer, 0, buffer.length);
|
|
}
|
|
|
|
public byte[] serialize() {
|
|
int length = this.length();
|
|
if (length == 0)
|
|
return new byte[0];
|
|
ByteBuffer buffer = ByteBuffer.allocate(length);
|
|
for (TLV entry : valueMap)
|
|
buffer.put(entry.serialize());
|
|
LOG.debug("Serialized TLV: " + this);
|
|
return buffer.array();
|
|
}
|
|
|
|
public HuaweiTLV put(int tag) {
|
|
byte[] value = new byte[0];
|
|
valueMap.add(new TLV((byte)tag, value));
|
|
return this;
|
|
}
|
|
|
|
public HuaweiTLV put(int tag, byte[] value) {
|
|
if (value == null) {
|
|
return this;
|
|
}
|
|
valueMap.add(new TLV((byte)tag, value));
|
|
return this;
|
|
}
|
|
|
|
public HuaweiTLV put(int tag, byte value) {
|
|
return put(tag, new byte[]{value});
|
|
}
|
|
|
|
public HuaweiTLV put(int tag, boolean value) {
|
|
return put(tag, new byte[]{value ? (byte) 1 : (byte) 0});
|
|
}
|
|
|
|
public HuaweiTLV put(int tag, Long value) {
|
|
return put(tag, ByteBuffer.allocate(8).putLong(value).array());
|
|
}
|
|
|
|
public HuaweiTLV put(int tag, Double value) {
|
|
return put(tag, ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putDouble(value).array());
|
|
}
|
|
|
|
public HuaweiTLV put(int tag, int value) {
|
|
return put(tag, ByteBuffer.allocate(4).putInt(value).array());
|
|
}
|
|
|
|
public HuaweiTLV put(int tag, short value) {
|
|
return put(tag, ByteBuffer.allocate(2).putShort(value).array());
|
|
}
|
|
|
|
public HuaweiTLV put(int tag, String value) {
|
|
if (value == null) {
|
|
return this;
|
|
}
|
|
return put(tag, value.getBytes(StandardCharsets.UTF_8));
|
|
}
|
|
|
|
public HuaweiTLV put(int tag, HuaweiTLV value) {
|
|
if (value == null) {
|
|
return this;
|
|
}
|
|
return put(tag, value.serialize());
|
|
}
|
|
|
|
public List<TLV> get() {
|
|
return this.valueMap;
|
|
}
|
|
|
|
public byte[] getBytes(int tag) throws HuaweiPacket.MissingTagException {
|
|
for (TLV item : valueMap)
|
|
if (item.getTag() == (byte) tag)
|
|
return item.getValue();
|
|
throw new HuaweiPacket.MissingTagException(tag);
|
|
}
|
|
|
|
public Byte getByte(int tag) throws HuaweiPacket.MissingTagException {
|
|
return getBytes(tag)[0];
|
|
}
|
|
|
|
public Boolean getBoolean(int tag) throws HuaweiPacket.MissingTagException {
|
|
return getBytes(tag)[0] == 1;
|
|
}
|
|
|
|
public Integer getInteger(int tag) throws HuaweiPacket.MissingTagException {
|
|
return ByteBuffer.wrap(getBytes(tag)).getInt();
|
|
}
|
|
|
|
public Short getShort(int tag) throws HuaweiPacket.MissingTagException {
|
|
return ByteBuffer.wrap(getBytes(tag)).getShort();
|
|
}
|
|
|
|
public String getString(int tag) throws HuaweiPacket.MissingTagException {
|
|
return new String(getBytes(tag), StandardCharsets.UTF_8);
|
|
}
|
|
|
|
public HuaweiTLV getObject(int tag) throws HuaweiPacket.MissingTagException {
|
|
byte[] bytes = getBytes(tag);
|
|
return new HuaweiTLV().parse(bytes, 0, bytes.length);
|
|
}
|
|
|
|
public List<HuaweiTLV> getObjects(int tag) {
|
|
List<HuaweiTLV> returnValue = new ArrayList<>();
|
|
for (TLV tlv : valueMap) {
|
|
if (tlv.getTag() == (byte) tag)
|
|
returnValue.add(new HuaweiTLV().parse(tlv.getValue()));
|
|
}
|
|
return returnValue;
|
|
}
|
|
|
|
public boolean contains(int tag) {
|
|
for (TLV item : valueMap)
|
|
if (item.getTag() == (byte) tag)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Removes the last element that was added with the specified tag
|
|
* @param tag The tag of the element that should be removed
|
|
* @return The value contained in the removed tag
|
|
*/
|
|
public byte[] remove(int tag) {
|
|
TLV foundItem = null;
|
|
for (TLV item : valueMap)
|
|
if (item.getTag() == (byte) tag)
|
|
foundItem = item;
|
|
if (foundItem != null) {
|
|
valueMap.remove(foundItem);
|
|
return foundItem.getValue();
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get string representation of HuaweiTLV, "Empty" when no elements are present
|
|
* @return String
|
|
*/
|
|
public String toString() {
|
|
if (valueMap.isEmpty())
|
|
return "Empty";
|
|
|
|
StringBuilder msg = new StringBuilder();
|
|
for (TLV entry : valueMap)
|
|
msg.append(entry.toString());
|
|
return msg.substring(0, msg.length() - 3);
|
|
}
|
|
|
|
public HuaweiTLV encrypt(ParamsProvider paramsProvider) throws CryptoException {
|
|
byte[] serializedTLV = serialize();
|
|
byte[] key = paramsProvider.getSecretKey();
|
|
byte[] nonce = paramsProvider.getIv();
|
|
byte[] encryptedTLV = HuaweiCrypto.encrypt(
|
|
paramsProvider.getEncryptMethod() == 0x01 || paramsProvider.getDeviceSupportType() == 0x04,
|
|
serializedTLV,
|
|
key,
|
|
nonce);
|
|
return new HuaweiTLV()
|
|
.put(CryptoTags.encryption, (byte) 0x01)
|
|
.put(CryptoTags.initVector, nonce)
|
|
.put(CryptoTags.cipherText, encryptedTLV);
|
|
}
|
|
|
|
public void decrypt(ParamsProvider paramsProvider) throws CryptoException, HuaweiPacket.MissingTagException {
|
|
byte[] key = paramsProvider.getSecretKey();
|
|
byte[] decryptedTLV = HuaweiCrypto.decrypt(
|
|
paramsProvider.getEncryptMethod() == 0x01 || paramsProvider.getDeviceSupportType() == 0x04,
|
|
getBytes(CryptoTags.cipherText),
|
|
key,
|
|
getBytes(CryptoTags.initVector));
|
|
this.valueMap = new ArrayList<>();
|
|
parse(decryptedTLV);
|
|
}
|
|
}
|
|
|
|
final class VarInt {
|
|
int dValue; // Decoded value of the VarInt
|
|
int size; // Size of the encoded value
|
|
byte[] eValue; // Encoded value of the VarInt
|
|
|
|
public VarInt(byte[] src, int offset) {
|
|
this.dValue = getVarIntValue(src, offset);
|
|
this.eValue = putVarIntValue(this.dValue);
|
|
this.size = this.eValue.length;
|
|
}
|
|
|
|
public String toString() {
|
|
return "VarInt(dValue: " + this.dValue + ", size: " + this.size + ", eValue: " + StringUtils.bytesToHex(this.eValue) + ")";
|
|
}
|
|
|
|
/**
|
|
* Returns the size of the encoded input value.
|
|
*
|
|
* @param value the integer to be measured
|
|
* @return the encoding size of the input value
|
|
*/
|
|
public static int getVarIntSize(int value) {
|
|
int result = 0;
|
|
do {
|
|
result++;
|
|
value >>>= 7;
|
|
} while (value != 0);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Decode a byte array of a variable-length encoding from start,
|
|
* 7 bits per byte.
|
|
* Return the decoded value in int.
|
|
*
|
|
* @param src the byte array to get the var int from
|
|
* @return the decoded value in int
|
|
*/
|
|
public static int getVarIntValue(byte[] src, int offset) {
|
|
int result = 0;
|
|
int b;
|
|
while (true) {
|
|
b = src[offset];
|
|
result += (b & 0x7F);
|
|
if ((b & 0x80) == 0) { break; }
|
|
result <<= 7;
|
|
offset++;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Encode an integer in a variable-length encoding, 7 bits per byte.
|
|
* Return the encoded value in byte[]
|
|
*
|
|
* @param value the int value to encode
|
|
* @return the encoded value in byte[]
|
|
*/
|
|
public static byte[] putVarIntValue(int value) {
|
|
int size = getVarIntSize(value);
|
|
byte[] result = new byte[size];
|
|
result[size - 1] = (byte)(value & 0x7F);
|
|
for (int offset = size - 2; offset >= 0; offset--) {
|
|
value >>>= 7;
|
|
result[offset] = (byte)((value & 0x7F) | 0x80);
|
|
}
|
|
return result;
|
|
}
|
|
}
|