package com.beowulfe.hap.impl.pairing; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import org.bouncycastle.crypto.digests.SHA512Digest; import org.bouncycastle.crypto.generators.HKDFBytesGenerator; import org.bouncycastle.crypto.params.HKDFParameters; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.beowulfe.hap.HomekitAuthInfo; import com.beowulfe.hap.impl.HomekitRegistry; import com.beowulfe.hap.impl.crypto.*; import com.beowulfe.hap.impl.http.HttpRequest; import com.beowulfe.hap.impl.http.HttpResponse; import com.beowulfe.hap.impl.pairing.PairVerificationRequest.Stage1Request; import com.beowulfe.hap.impl.pairing.PairVerificationRequest.Stage2Request; import com.beowulfe.hap.impl.pairing.TypeLengthValueUtils.DecodeResult; import com.beowulfe.hap.impl.pairing.TypeLengthValueUtils.Encoder; import com.beowulfe.hap.impl.responses.NotFoundResponse; import com.beowulfe.hap.impl.responses.OkResponse; import djb.Curve25519; public class PairVerificationManager { private final static Logger logger = LoggerFactory.getLogger(PairVerificationManager.class); private static volatile SecureRandom secureRandom; private final HomekitAuthInfo authInfo; private final HomekitRegistry registry; private byte[] hkdfKey; private byte[] clientPublicKey; private byte[] publicKey; private byte[] sharedSecret; public PairVerificationManager(HomekitAuthInfo authInfo, HomekitRegistry registry) { this.authInfo = authInfo; this.registry = registry; } public HttpResponse handle(HttpRequest rawRequest) throws Exception { PairVerificationRequest request = PairVerificationRequest.of(rawRequest.getBody()); switch(request.getStage()) { case ONE: return stage1((Stage1Request) request); case TWO: return stage2((Stage2Request) request); default: return new NotFoundResponse(); } } private HttpResponse stage1(Stage1Request request) throws Exception { logger.debug("Starting pair verification for "+registry.getLabel()); clientPublicKey = request.getClientPublicKey(); publicKey = new byte[32]; byte[] privateKey = new byte[32]; getSecureRandom().nextBytes(privateKey); Curve25519.keygen(publicKey, null, privateKey); sharedSecret = new byte[32]; Curve25519.curve(sharedSecret, privateKey, clientPublicKey); byte[] material = ByteUtils.joinBytes(publicKey, authInfo.getMac().getBytes(StandardCharsets.UTF_8), clientPublicKey); byte[] proof = new EdsaSigner(authInfo.getPrivateKey()).sign(material); HKDFBytesGenerator hkdf = new HKDFBytesGenerator(new SHA512Digest()); hkdf.init(new HKDFParameters(sharedSecret, "Pair-Verify-Encrypt-Salt".getBytes(StandardCharsets.UTF_8), "Pair-Verify-Encrypt-Info".getBytes(StandardCharsets.UTF_8))); hkdfKey = new byte[32]; hkdf.generateBytes(hkdfKey, 0, 32); Encoder encoder = TypeLengthValueUtils.getEncoder(); encoder.add(MessageType.USERNAME, authInfo.getMac().getBytes(StandardCharsets.UTF_8)); encoder.add(MessageType.SIGNATURE, proof); byte[] plaintext = encoder.toByteArray(); ChachaEncoder chacha = new ChachaEncoder(hkdfKey, "PV-Msg02".getBytes(StandardCharsets.UTF_8)); byte[] ciphertext = chacha.encodeCiphertext(plaintext); encoder = TypeLengthValueUtils.getEncoder(); encoder.add(MessageType.STATE, (short) 2); encoder.add(MessageType.ENCRYPTED_DATA, ciphertext); encoder.add(MessageType.PUBLIC_KEY, publicKey); return new PairingResponse(encoder.toByteArray()); } private HttpResponse stage2(Stage2Request request) throws Exception { ChachaDecoder chacha = new ChachaDecoder(hkdfKey, "PV-Msg03".getBytes(StandardCharsets.UTF_8)); byte[] plaintext = chacha.decodeCiphertext(request.getAuthTagData(), request.getMessageData()); DecodeResult d = TypeLengthValueUtils.decode(plaintext); byte[] clientUsername = d.getBytes(MessageType.USERNAME); byte[] clientSignature = d.getBytes(MessageType.SIGNATURE); byte[] material = ByteUtils.joinBytes(clientPublicKey, clientUsername, publicKey); byte[] clientLtpk = authInfo.getUserPublicKey(authInfo.getMac()+new String(clientUsername, StandardCharsets.UTF_8)); if (clientLtpk == null) { throw new Exception("Unknown user: "+new String(clientUsername, StandardCharsets.UTF_8)); } Encoder encoder = TypeLengthValueUtils.getEncoder(); if (new EdsaVerifier(clientLtpk).verify(material, clientSignature)) { encoder.add(MessageType.STATE, (short) 4); logger.debug("Completed pair verification for "+registry.getLabel()); return new UpgradeResponse(encoder.toByteArray(), createKey("Control-Write-Encryption-Key"), createKey("Control-Read-Encryption-Key")); } else { encoder.add(MessageType.ERROR, (short) 4); logger.warn("Invalid signature. Could not pair "+registry.getLabel()); return new OkResponse(encoder.toByteArray()); } } private byte[] createKey(String info) { HKDFBytesGenerator hkdf = new HKDFBytesGenerator(new SHA512Digest()); hkdf.init(new HKDFParameters(sharedSecret, "Control-Salt".getBytes(StandardCharsets.UTF_8), info.getBytes(StandardCharsets.UTF_8))); byte[] key = new byte[32]; hkdf.generateBytes(key, 0, 32); return key; } private static SecureRandom getSecureRandom() { if (secureRandom == null) { synchronized(PairVerificationManager.class) { if (secureRandom == null) { secureRandom = new SecureRandom(); } } } return secureRandom; } }