/* Copyright (C) 2024 Jonathan Gobbo 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 . */ package nodomain.freeyourgadget.gadgetbridge.service.devices.redmibuds5pro.protocol; import java.math.BigInteger; import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; /* Authentication based on the custom Bluetooth version of the SAFER+ encryption algorithm with: - 128 bit key size - 8 rounds */ public class Authentication { private final static int PATTERN = 0x9999; private final static int BLOCK_SIZE = 16; byte[][] biasMatrix; byte[] expTab; byte[] logTab; public Authentication() { generateBiasMatrix(); generateExpTab(); generateLogTab(); } private void generateBiasMatrix() { biasMatrix = new byte[16][]; for (int i = 0; i < 16; i++) { byte[] biasVec = new byte[16]; for (int j = 0; j < 16; j++) { int exponent = (17 * (i + 2) + (j + 1)); BigInteger base = BigInteger.valueOf(45); BigInteger modExp = base.pow(base.pow(exponent).mod(BigInteger.valueOf(257)).intValue()) .mod(BigInteger.valueOf(257)); byte val = (byte) (modExp.intValue() == 256 ? 0 : modExp.intValue()); biasVec[j] = val; } biasMatrix[i] = biasVec; } } private void generateExpTab() { expTab = new byte[256]; for (int i = 0; i < 256; i++) { BigInteger base = BigInteger.valueOf(45); BigInteger exp = base.pow(i).mod(BigInteger.valueOf(257)); expTab[i] = (byte) (i == 128 ? 0 : exp.intValue()); } } private void generateLogTab() { logTab = new byte[256]; for (int i = 0; i < 256; i++) { if (i == 0) { logTab[i] = (byte) 128; } else { BigInteger base = BigInteger.valueOf(45); BigInteger modExp = base.pow(i).mod(BigInteger.valueOf(257)); if (modExp.intValue() != 256) { logTab[modExp.intValue()] = (byte) i; } } } } private byte[][] keySchedule(byte[] keyInit) { keyInit[15] ^= 6; byte[][] keys = new byte[17][]; keys[0] = keyInit; List register = new ArrayList<>(); for (byte b : keyInit) { register.add(b); } byte xor = register.stream().reduce((byte) 0x0, (cSum, e) -> (byte) (cSum ^ e)); register.add(xor); for (int keyIdx = 1; keyIdx < keys.length; keyIdx++) { byte[] keyI = new byte[16]; for (int i = 0; i < 17; i++) { byte rot = (byte) (((register.get(i) & 0xff) >>> 5) | ((register.get(i) & 0xff) << (8 - 5))); register.set(i, rot); } for (int i = 0; i < 16; i++) { keyI[i] = (byte) (register.get((keyIdx + i) % 17) + biasMatrix[keyIdx - 1][i]); } keys[keyIdx] = keyI; } return keys; } private byte[] encrypt(byte[] plaintext, byte[][] keys) { byte[] ciphertext = plaintext.clone(); for (int round = 0; round < 8; round++) { if (round == 2) { for (int i = 0; i < BLOCK_SIZE; i++) { if ((1 << i & PATTERN) != 0) { ciphertext[i] ^= plaintext[i]; } else { ciphertext[i] += plaintext[i]; } } } for (int i = 0; i < BLOCK_SIZE; i++) { if ((1 << i & PATTERN) != 0) { ciphertext[i] ^= keys[round * 2][i]; } else { ciphertext[i] += keys[round * 2][i]; } } for (int i = 0; i < BLOCK_SIZE; i++) { if ((1 << i & PATTERN) != 0) { ciphertext[i] = expTab[ciphertext[i] & 0xff]; } else { ciphertext[i] = logTab[ciphertext[i] & 0xff]; } } for (int i = 0; i < BLOCK_SIZE; i++) { if ((1 << i & PATTERN) != 0) { ciphertext[i] = (byte) (keys[round * 2 + 1][i] + ciphertext[i]); } else { ciphertext[i] = (byte) (keys[round * 2 + 1][i] ^ ciphertext[i]); } } byte[] ciphertextCopy = ciphertext.clone(); for (int i = 0; i < BLOCK_SIZE; i++) { byte cSum = 0; for (int j = 0; j < BLOCK_SIZE; j++) { cSum += (byte) (AuthData.COEFFICIENTS[i][j] * ciphertextCopy[j]); } ciphertext[i] = cSum; } } for (int i = 0; i < BLOCK_SIZE; i++) { if ((1 << i & PATTERN) != 0) { ciphertext[i] = (byte) (keys[16][i] ^ ciphertext[i]); } else { ciphertext[i] = (byte) (keys[16][i] + ciphertext[i]); } } return ciphertext; } public static byte[] getRandomChallenge() { byte[] res = new byte[BLOCK_SIZE]; SecureRandom rnd = new SecureRandom(); rnd.nextBytes(res); return res; } public byte[] computeChallengeResponse(byte[] challenge) { byte[][] keys = keySchedule(challenge); return encrypt(AuthData.SEQ, keys); } }