/* * Copyright 2011 David Simmons * http://cafbit.com/ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.iiordanov.bVNC; import java.io.IOException; import java.math.BigInteger; import java.security.GeneralSecurityException; import java.security.Key; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Random; import javax.crypto.Cipher; import javax.crypto.KeyAgreement; import javax.crypto.interfaces.DHPrivateKey; import javax.crypto.interfaces.DHPublicKey; import javax.crypto.spec.DHParameterSpec; import javax.crypto.spec.DHPublicKeySpec; import javax.crypto.spec.SecretKeySpec; /** * This class implements "Mac Authentication", which uses Diffie-Hellman * key agreement (along with MD5 and AES128) to authenticate users to * Apple Remote Desktop, the VNC server which is built-in to Mac OS X. * * This authentication technique is based on the following steps: * * 1. Perform Diffie-Hellman key agreement, so both sides have * a shared secret key which can be used for further encryption. * 2. Take the MD5 hash of this DH secret key to produce a 128-bit * value which we will use as the actual encryption key. * 3. Encrypt the username and password with this key using the AES * 128-bit symmetric cipher in electronic codebook (ECB) mode. The * username/password credentials are stored in a 128-byte structure, * with 64 bytes for each, null-terminated. Ideally, write random * values into the portion of this 128-byte structure which is not * occupied by the username or password, but no further padding for * this block cipher. * * The ciphertext from step 3 and the DH public key from step 2 * are sent to the server. */ public class RFBSecurityARD { // The type and name identifies this authentication scheme to // the rest of the RFB code. private static final String NAME = "Mac Authentication"; public byte getType() { return RfbProto.SecTypeArd; } public String getTypeName() { return NAME; } // credentials private String username; private String password; /** * The DHResult class holds the output of the Diffie-Hellman * key agreement. */ private static class DHResult { private byte[] publicKey; private byte[] privateKey; private byte[] secretKey; }; public RFBSecurityARD(String username, String password) { this.username = username; this.password = password; } /** * Perform Mac (ARD) Authentication on the provided RFBStream using * the username and password provided in the constructor. */ public boolean perform(RfbProto rfb) throws IOException { // 1. read the Diffie-Hellman parameters from the server byte[] generator = new byte[2]; rfb.is.readFully(generator, 0, 2); // DH base generator value int keyLength = rfb.is.readShort(); // key length in bytes byte[] prime = new byte[keyLength]; rfb.is.readFully(prime); // predetermined prime modulus byte[] peerKey = new byte[keyLength]; rfb.is.readFully(peerKey); // other party's public key // 2. perform Diffie-Hellman key agreement to calculate // the publicKey and privateKey DHResult dh = performDHKeyAgreement( new BigInteger(+1, prime), new BigInteger(+1, generator), new BigInteger(+1, peerKey), keyLength ); // 3. calculate the MD5 hash of the DH shared secret byte[] secret = performMD5(dh.secretKey); // 4. ciphertext = AES128(shared, username[64]:password[64]); byte[] credentials = new byte[128]; // randomize the padding for security. Random random = new SecureRandom(); random.nextBytes(credentials); byte[] userBytes = username.getBytes("UTF-8"); byte[] passBytes = password.getBytes("UTF-8"); int userLength = (userBytes.length < 63) ? userBytes.length : 63; int passLength = (passBytes.length < 63) ? passBytes.length : 63; System.arraycopy(userBytes, 0, credentials, 0, userLength); System.arraycopy(passBytes, 0, credentials, 64, passLength); credentials[userLength] = '\0'; credentials[64+passLength] = '\0'; byte[] ciphertext = performAES128(secret, credentials); // 5. send the ciphertext + DH public key rfb.os.write(ciphertext); rfb.os.write(dh.publicKey); return true; } private final static String MSG_NO_SUPPORT = "Your device does not support the required cryptography to perform Mac Authentication."; private final static String MSG_ERROR = "A cryptography error occurred while trying to perform Mac Authentication."; private DHResult performDHKeyAgreement( BigInteger prime, BigInteger generator, BigInteger peerKey, int keyLength ) throws IOException { // fetch instances of all needed Diffie-Hellman support classes KeyPairGenerator keyPairGenerator; KeyAgreement keyAgreement; KeyFactory keyFactory; try { keyPairGenerator = KeyPairGenerator.getInstance("DH"); keyAgreement = KeyAgreement.getInstance("DH"); keyFactory = KeyFactory.getInstance("DH"); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); throw new IOException(MSG_NO_SUPPORT + " (Diffie-Hellman)"); } try { // parse the peerKey DHPublicKeySpec peerKeySpec = new DHPublicKeySpec( peerKey, prime, generator ); DHPublicKey peerPublicKey = (DHPublicKey) keyFactory.generatePublic(peerKeySpec); // generate my public/private key pair keyPairGenerator.initialize( new DHParameterSpec(prime, generator) ); KeyPair keyPair = keyPairGenerator.generateKeyPair(); // perform key agreement keyAgreement.init(keyPair.getPrivate()); keyAgreement.doPhase(peerPublicKey, true); // return the results DHResult result = new DHResult(); result.publicKey = keyToBytes(keyPair.getPublic(), keyLength); result.privateKey = keyToBytes(keyPair.getPrivate(), keyLength); result.secretKey = keyAgreement.generateSecret(); return result; } catch (GeneralSecurityException e) { e.printStackTrace(); throw new IOException(MSG_ERROR + " (Key agreement)"); } } private byte[] performMD5(byte[] input) throws IOException { byte[] output; try { // Create MD5 Hash MessageDigest digest = java.security.MessageDigest.getInstance("MD5"); digest.update(input); output = digest.digest(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); throw new IOException(MSG_NO_SUPPORT + " (MD5)"); } return output; } private byte[] performAES128(byte[] key, byte[] plaintext) throws IOException { byte[] ciphertext; try { SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES"); Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec); ciphertext = cipher.doFinal(plaintext); } catch (GeneralSecurityException e) { e.printStackTrace(); throw new IOException(MSG_ERROR + " (AES128)"); } return ciphertext; } /** * BigInteger.toByteArray() always includes a sign bit, which adds an * extra byte to the front. This is meaningless and annoying when we * are dealing purely with positive numbers, so drop it. */ private byte[] convertBigIntegerToByteArray(BigInteger bigInteger, int length) { byte[] bytes = bigInteger.toByteArray(); if (bytes.length > length) { byte[] array = new byte[length]; System.arraycopy(bytes, bytes.length-length, array, 0, length); return array; } else if (bytes.length < length) { byte[] array = new byte[length]; System.arraycopy(bytes, 0, array, length-bytes.length, bytes.length); return array; } else { return bytes; } } /** * Extract raw key bytes from a Key object. This is less than * straightforward, since Java loves dealing with DER-encoded * X.509 keys instead of straight key buffers. */ private byte[] keyToBytes(Key key, int length) throws IOException { if (key == null) { throw new IOException(MSG_ERROR + " (null key to bytes)"); } if (key instanceof DHPublicKey) { return convertBigIntegerToByteArray(((DHPublicKey)key).getY(), length); } else if (key instanceof DHPrivateKey) { return convertBigIntegerToByteArray(((DHPrivateKey)key).getX(), length); } else { throw new IOException(MSG_ERROR + " (key "+key.getClass().getSimpleName()+" to bytes)"); } } }