mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-09-09 15:56:36 +02:00
271 lines
13 KiB
Java
271 lines
13 KiB
Java
/* Copyright (C) 2024 Damien Gaignon, Martin.JM
|
|
|
|
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/>. */
|
|
package nodomain.freeyourgadget.gadgetbridge.devices.huawei;
|
|
|
|
import nodomain.freeyourgadget.gadgetbridge.util.CryptoUtils;
|
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
|
|
|
import java.io.UnsupportedEncodingException;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.security.InvalidAlgorithmParameterException;
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.security.InvalidKeyException;
|
|
import java.security.SecureRandom;
|
|
import java.security.spec.InvalidKeySpecException;
|
|
import java.util.Arrays;
|
|
|
|
import javax.crypto.BadPaddingException;
|
|
import javax.crypto.IllegalBlockSizeException;
|
|
import javax.crypto.NoSuchPaddingException;
|
|
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
public class HuaweiCrypto {
|
|
private static final Logger LOG = LoggerFactory.getLogger(HuaweiCrypto.class);
|
|
|
|
public static class CryptoException extends Exception {
|
|
CryptoException(Exception e) {
|
|
super(e);
|
|
}
|
|
}
|
|
|
|
public static final byte[] SECRET_KEY_1_v1 = new byte[]{ 0x6F, 0x75, 0x6A, 0x79,
|
|
0x6D, 0x77, 0x71, 0x34,
|
|
0x63, 0x6C, 0x76, 0x39,
|
|
0x33, 0x37, 0x38, 0x79};
|
|
public static final byte[] SECRET_KEY_2_v1 = new byte[]{ 0x62, 0x31, 0x30, 0x6A,
|
|
0x67, 0x66, 0x64, 0x39,
|
|
0x79, 0x37, 0x76, 0x73,
|
|
0x75, 0x64, 0x61, 0x39};
|
|
public static final byte[] SECRET_KEY_1_v23 = new byte[]{ 0x55, 0x53, (byte)0x86, (byte)0xFC,
|
|
0x63, 0x20, 0x07, (byte)0xAA,
|
|
(byte)0x86, 0x49, 0x35, 0x22,
|
|
(byte)0xB8, 0x6A, (byte)0xE2, 0x5C};
|
|
public static final byte[] SECRET_KEY_2_v23 = new byte[]{ 0x33, 0x07, (byte)0x9B, (byte)0xC5,
|
|
0x7A, (byte)0x88, 0x6D, 0x3C,
|
|
(byte)0xF5, 0x61, 0x37, 0x09,
|
|
0x6F, 0x22, (byte)0x80, 0x00};
|
|
|
|
public static final byte[] DIGEST_SECRET_v1 = new byte[]{ 0x70, (byte)0xFB, 0x6C, 0x24,
|
|
0x03, 0x5F, (byte)0xDB, 0x55,
|
|
0x2F, 0x38, (byte)0x89, (byte)0x8A,
|
|
(byte) 0xEE, (byte)0xDE, 0x3F, 0x69};
|
|
public static final byte[] DIGEST_SECRET_v2 = new byte[]{ (byte)0x93, (byte)0xAC, (byte)0xDE, (byte)0xF7,
|
|
0x6A, (byte)0xCB, 0x09, (byte)0x85,
|
|
0x7D, (byte)0xBF, (byte)0xE5, 0x26,
|
|
0x1A, (byte)0xAB, (byte)0xCD, 0x78};
|
|
public static final byte[] DIGEST_SECRET_v3 = new byte[]{ (byte)0x9C, 0x27, 0x63, (byte)0xA9,
|
|
(byte)0xCC, (byte)0xE1, 0x34, 0x76,
|
|
0x6D, (byte)0xE3, (byte)0xFF, 0x61,
|
|
0x18, 0x20, 0x05, 0x53};
|
|
|
|
public static final byte[] MESSAGE_RESPONSE = new byte[]{0x01, 0x10};
|
|
public static final byte[] MESSAGE_CHALLENGE = new byte[]{0x01, 0x00};
|
|
|
|
public static final long ENCRYPTION_COUNTER_MAX = 0xFFFFFFFF;
|
|
|
|
protected int authVersion;
|
|
protected int deviceSupportType;
|
|
protected byte authAlgo;
|
|
|
|
public HuaweiCrypto(int authVersion) {
|
|
this.authVersion = authVersion;
|
|
}
|
|
|
|
public HuaweiCrypto(int authVersion, byte authAlgo, int deviceSupportType) {
|
|
this(authVersion);
|
|
this.deviceSupportType = deviceSupportType;
|
|
this.authAlgo = authAlgo;
|
|
}
|
|
|
|
public static byte[] generateNonce() {
|
|
// While technically not a nonce, we need it to be random and rely on the length for the chance of repitition to be small
|
|
byte[] returnValue = new byte[16];
|
|
(new SecureRandom()).nextBytes(returnValue);
|
|
return returnValue;
|
|
}
|
|
|
|
private byte[] getDigestSecret() {
|
|
byte[] digest;
|
|
if (authVersion == 1) {
|
|
digest = DIGEST_SECRET_v1.clone();
|
|
} else if (authVersion == 2) {
|
|
digest = DIGEST_SECRET_v2.clone();
|
|
} else {
|
|
digest = DIGEST_SECRET_v3.clone();
|
|
}
|
|
return digest;
|
|
}
|
|
public byte[] computeDigest(byte[] message, byte[] nonce) throws NoSuchAlgorithmException, InvalidKeyException {
|
|
byte[] digestSecret = getDigestSecret();
|
|
byte[] msgToDigest = ByteBuffer.allocate(16 + message.length)
|
|
.put(digestSecret)
|
|
.put(message)
|
|
.array();
|
|
byte[] digestStep1 = CryptoUtils.calcHmacSha256(msgToDigest, nonce);
|
|
byte[] challenge = ByteBuffer.allocate(0x40)
|
|
.put(CryptoUtils.calcHmacSha256(digestStep1, nonce))
|
|
.put(digestStep1)
|
|
.array();
|
|
return challenge;
|
|
}
|
|
|
|
public byte[] computeDigestHiChainLite(byte[] message, byte[] key, byte[] nonce) throws NoSuchAlgorithmException, InvalidKeyException, InvalidKeySpecException, UnsupportedEncodingException {
|
|
byte[] digestStep1;
|
|
byte[] hashKey = CryptoUtils.digest(GB.hexdump(key).getBytes("UTF-8"));
|
|
byte[] digestSecret = getDigestSecret();
|
|
for (int i = 0; i < digestSecret.length; i++) {
|
|
digestSecret[i] = (byte) (((0xFF & hashKey[i]) ^ (digestSecret[i] & 0xFF)) & 0xFF);
|
|
}
|
|
byte[] msgToDigest = ByteBuffer.allocate(18)
|
|
.put(digestSecret)
|
|
.put(message)
|
|
.array();
|
|
if (authAlgo == 0x01) {
|
|
digestStep1 = CryptoUtils.pbkdf2Sha256(GB.hexdump(msgToDigest), GB.hexdump(nonce), 0x3e8, 0x100);
|
|
} else {
|
|
digestStep1 = CryptoUtils.calcHmacSha256(msgToDigest, nonce);
|
|
}
|
|
byte[] challenge = ByteBuffer.allocate(0x40)
|
|
.put(CryptoUtils.calcHmacSha256(digestStep1, nonce))
|
|
.put(digestStep1)
|
|
.array();
|
|
return challenge;
|
|
}
|
|
|
|
public byte[] digestChallenge(byte[] secretKey, byte[] nonce) throws NoSuchAlgorithmException, InvalidKeyException, InvalidKeySpecException, UnsupportedEncodingException {
|
|
if (deviceSupportType == 0x02) {
|
|
if (secretKey == null)
|
|
return null;
|
|
if (authVersion == 0x02) {
|
|
byte[] key = ByteBuffer.allocate(18)
|
|
.put(secretKey)
|
|
.put(MESSAGE_CHALLENGE)
|
|
.array();
|
|
byte[] challenge = ByteBuffer.allocate(0x40)
|
|
.put(CryptoUtils.calcHmacSha256(key, nonce))
|
|
.put(key)
|
|
.array();
|
|
return challenge;
|
|
}
|
|
return computeDigestHiChainLite(MESSAGE_CHALLENGE, secretKey, nonce);
|
|
}
|
|
return computeDigest(MESSAGE_CHALLENGE, nonce);
|
|
}
|
|
|
|
public byte[] digestResponse(byte[] secretKey, byte[] nonce) throws NoSuchAlgorithmException, InvalidKeyException, InvalidKeySpecException, UnsupportedEncodingException {
|
|
if (deviceSupportType == 0x02) {
|
|
if (secretKey == null)
|
|
return null;
|
|
if (authVersion == 0x02) {
|
|
byte[] key = ByteBuffer.allocate(18)
|
|
.put(secretKey)
|
|
.put(MESSAGE_RESPONSE)
|
|
.array();
|
|
byte[] challenge = ByteBuffer.allocate(0x40)
|
|
.put(CryptoUtils.calcHmacSha256(key, nonce))
|
|
.put(key)
|
|
.array();
|
|
return challenge;
|
|
}
|
|
return computeDigestHiChainLite(MESSAGE_RESPONSE, secretKey, nonce);
|
|
}
|
|
return computeDigest(MESSAGE_RESPONSE, nonce);
|
|
}
|
|
|
|
public static ByteBuffer initializationVector(long counter) {
|
|
if (counter == ENCRYPTION_COUNTER_MAX) {
|
|
counter = 1;
|
|
} else {
|
|
counter += 1;
|
|
}
|
|
ByteBuffer ivCounter = ByteBuffer.allocate(16);
|
|
ivCounter.put(generateNonce(), 0, 12);
|
|
ivCounter.put(ByteBuffer.allocate(8).putLong(counter).array(), 4, 4);
|
|
ivCounter.rewind();
|
|
return ivCounter;
|
|
}
|
|
|
|
public byte[] createSecretKey(String macAddress) throws NoSuchAlgorithmException {
|
|
byte[] secret_key_1 = SECRET_KEY_1_v23;
|
|
byte[] secret_key_2 = SECRET_KEY_2_v23;
|
|
if (authVersion == 1) {
|
|
secret_key_1 = SECRET_KEY_1_v1;
|
|
secret_key_2 = SECRET_KEY_2_v1;
|
|
}
|
|
|
|
byte[] macAddressKey = (macAddress.replace(":", "") + "0000").getBytes(StandardCharsets.UTF_8);
|
|
|
|
byte[] mixedSecretKey = new byte[16];
|
|
for (int i = 0; i < 16; i++) {
|
|
mixedSecretKey[i] = (byte)((((0xFF & secret_key_1[i]) << 4) ^ (0xFF & secret_key_2[i])) & 0xFF);
|
|
}
|
|
|
|
byte[] mixedSecretKeyHash = CryptoUtils.digest(mixedSecretKey);
|
|
byte[] finalMixedKey = new byte[16];
|
|
for (int i = 0; i < 16; i++) {
|
|
finalMixedKey[i] = (byte)((((0xFF & mixedSecretKeyHash[i]) >> 6) ^ (0xFF & macAddressKey[i])) & 0xFF);
|
|
}
|
|
byte[] finalMixedKeyHash = CryptoUtils.digest(finalMixedKey);
|
|
return Arrays.copyOfRange(finalMixedKeyHash, 0, 16);
|
|
}
|
|
|
|
public byte[] encryptBondingKey(byte encryptMethod, byte[] data, byte[] encryptionKey, byte[] iv) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IllegalArgumentException {
|
|
if (encryptMethod == 0x01)
|
|
return CryptoUtils.encryptAES_GCM_NoPad(data, encryptionKey, iv, null);
|
|
return CryptoUtils.encryptAES_CBC_Pad(data, encryptionKey, iv);
|
|
}
|
|
|
|
public byte[] decryptBondingKey(byte[] data, String mac, byte[] iv) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IllegalArgumentException {
|
|
byte[] encryptionKey = createSecretKey(mac);
|
|
return CryptoUtils.decryptAES_CBC_Pad(data, encryptionKey, iv);
|
|
}
|
|
|
|
public byte[] decryptPinCode(byte encryptMethod, byte[] message, byte[] iv) throws CryptoException {
|
|
byte[] secretKey = getDigestSecret();
|
|
try {
|
|
if (encryptMethod == 0x1)
|
|
return CryptoUtils.decryptAES_GCM_NoPad(message, secretKey, iv, null);
|
|
return CryptoUtils.decryptAES_CBC_Pad(message, secretKey, iv);
|
|
} catch (Exception e) {
|
|
throw new CryptoException(e);
|
|
}
|
|
}
|
|
|
|
public static byte[] encrypt(boolean useGCM, byte[] message, byte[] key, byte[] iv) throws CryptoException {
|
|
try {
|
|
if (useGCM)
|
|
return CryptoUtils.encryptAES_GCM_NoPad(message, key, iv, null);
|
|
return CryptoUtils.encryptAES_CBC_Pad(message, key, iv);
|
|
} catch (InvalidAlgorithmParameterException | NoSuchPaddingException | IllegalBlockSizeException | NoSuchAlgorithmException | BadPaddingException | InvalidKeyException | IllegalArgumentException e) {
|
|
throw new CryptoException(e);
|
|
}
|
|
}
|
|
|
|
public static byte[] decrypt(boolean useGCM, byte[] message, byte[] key, byte[] iv) throws CryptoException {
|
|
try {
|
|
if (useGCM)
|
|
return CryptoUtils.decryptAES_GCM_NoPad(message, key, iv, null);
|
|
return CryptoUtils.decryptAES_CBC_Pad(message, key, iv);
|
|
} catch (InvalidAlgorithmParameterException | NoSuchPaddingException | IllegalBlockSizeException | NoSuchAlgorithmException | BadPaddingException | InvalidKeyException | IllegalArgumentException e) {
|
|
throw new CryptoException(e);
|
|
}
|
|
}
|
|
}
|