Pbkdf2ChallengeResponse.java
/**
* A Java API for managing FritzBox HomeAutomation
* Copyright (C) 2017 Christoph Pirkl <christoph at users.sourceforge.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.github.kaklakariada.fritzbox.login;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
/**
* PBKDF2 challenge-response. See
* https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/AVM_Technical_Note_-_Session_ID_english_2021-05-03.pdf
*/
class Pbkdf2ChallengeResponse implements ChallengeResponse {
@Override
public String calculateResponse(final String challenge, final String password) {
final String[] challengeParts = challenge.split("\\$");
if (challengeParts.length != 5) {
throw new IllegalArgumentException("Challenge '" + challenge + "' has an invalid format");
}
final int iter1 = Integer.parseInt(challengeParts[1]);
final byte[] salt1 = fromHex(challengeParts[2]);
final int iter2 = Integer.parseInt(challengeParts[3]);
final byte[] salt2 = fromHex(challengeParts[4]);
final byte[] hash1 = pbkdf2HmacSha256(password.getBytes(StandardCharsets.UTF_8), salt1, iter1);
final byte[] hash2 = pbkdf2HmacSha256(hash1, salt2, iter2);
return challengeParts[4] + "$" + toHex(hash2);
}
/** Hex string to bytes */
static byte[] fromHex(final String hexString) {
final int len = hexString.length() / 2;
final byte[] ret = new byte[len];
for (int i = 0; i < len; i++) {
ret[i] = (byte) Short.parseShort(hexString.substring(i * 2, i *
2 + 2), 16);
}
return ret;
}
/** Byte array to hex string */
static String toHex(final byte[] bytes) {
final StringBuilder s = new StringBuilder(bytes.length * 2);
for (final byte b : bytes) {
s.append(String.format("%02x", b));
}
return s.toString();
}
/**
* Create a pbkdf2 HMAC by appling the Hmac iter times as specified. We can't use the Android-internal PBKDF2 here,
* as it only accepts char[] arrays, not bytes (for multi-stage hashing)
*/
static byte[] pbkdf2HmacSha256(final byte[] password, final byte[] salt, final int iters) {
final String algorithm = "HmacSHA256";
try {
final Mac sha256mac = Mac.getInstance(algorithm);
sha256mac.init(new SecretKeySpec(password, algorithm));
final byte[] ret = new byte[sha256mac.getMacLength()];
byte[] tmp = new byte[salt.length + 4];
System.arraycopy(salt, 0, tmp, 0, salt.length);
tmp[salt.length + 3] = 1;
for (int i = 0; i < iters; i++) {
tmp = sha256mac.doFinal(tmp);
for (int k = 0; k < ret.length; k++) {
ret[k] ^= tmp[k];
}
}
return ret;
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new IllegalStateException("Failed to calculate HMAC", e);
}
}
}