// Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you 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 vncclient.vnc; import java.security.spec.KeySpec; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.DESKeySpec; import streamer.ByteBuffer; import streamer.Element; import streamer.Link; import streamer.OneTimeSwitch; import streamer.Pipeline; import streamer.PipelineImpl; import streamer.debug.FakeSink; import streamer.debug.MockSink; import streamer.debug.MockSource; public class Vnc33Authentication extends OneTimeSwitch { /** * Password to use when authentication is required. */ protected String password = null; /** * Authentication stage: * <ul> * <li>0 - challenge received, response must be sent * <li>1 - authentication result received. * </ul> */ protected int stage = 0; public Vnc33Authentication(String id) { super(id); } public Vnc33Authentication(String id, String password) { super(id); this.password = password; } @Override protected void handleOneTimeData(ByteBuffer buf, Link link) { if (verbose) System.out.println("[" + this + "] INFO: Data received: " + buf + "."); switch (stage) { case 0: // Read security with optional challenge and response stage0(buf, link); break; case 1: // Read authentication response stage1(buf, link); break; } } /** * Read security type. If connection type is @see * RfbConstants.CONNECTION_FAILED, then throw exception. If connection type is @see * RfbConstants.NO_AUTH, then switch off this element. If connection type is @see * RfbConstants.VNC_AUTH, then read challenge, send encoded password, and read * authentication response. */ private void stage0(ByteBuffer buf, Link link) { // At least 4 bytes are necessary if (!cap(buf, 4, UNLIMITED, link, true)) return; // Read security type int authType = buf.readSignedInt(); switch (authType) { case RfbConstants.CONNECTION_FAILED: { // Server forbids to connect. Read reason and throw exception int length = buf.readSignedInt(); String reason = new String(buf.data, buf.offset, length, RfbConstants.US_ASCII_CHARSET); throw new RuntimeException("Authentication to VNC server is failed. Reason: " + reason); } case RfbConstants.NO_AUTH: { // Client can connect without authorization. Nothing to do. // Switch off this element from circuit switchOff(); break; } case RfbConstants.VNC_AUTH: { // Read challenge and generate response responseToChallenge(buf, link); break; } default: throw new RuntimeException("Unsupported VNC protocol authorization scheme, scheme code: " + authType + "."); } } private void responseToChallenge(ByteBuffer buf, Link link) { // Challenge is exactly 16 bytes long if (!cap(buf, 16, 16, link, true)) return; ByteBuffer challenge = buf.slice(buf.cursor, 16, true); buf.unref(); // Encode challenge with password ByteBuffer response; try { response = encodePassword(challenge, password); challenge.unref(); } catch (Exception e) { throw new RuntimeException("Cannot encrypt client password to send to server: " + e.getMessage()); } if (verbose) { response.putMetadata("sender", this); } // Send encoded challenge nextStage(); pushDataToOTOut(response); } private void nextStage() { stage++; if (verbose) System.out.println("[" + this + "] INFO: Next stage: " + stage + "."); } /** * Encode password using DES encryption with given challenge. * * @param challenge * a random set of bytes. * @param password * a password * @return DES hash of password and challenge */ public ByteBuffer encodePassword(ByteBuffer challenge, String password) { if (challenge.length != 16) throw new RuntimeException("Challenge must be exactly 16 bytes long."); // VNC password consist of up to eight ASCII characters. byte[] key = {0, 0, 0, 0, 0, 0, 0, 0}; // Padding byte[] passwordAsciiBytes = password.getBytes(RfbConstants.US_ASCII_CHARSET); System.arraycopy(passwordAsciiBytes, 0, key, 0, Math.min(password.length(), 8)); // Flip bytes (reverse bits) in key for (int i = 0; i < key.length; i++) { key[i] = flipByte(key[i]); } try { KeySpec desKeySpec = new DESKeySpec(key); SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("DES"); SecretKey secretKey = secretKeyFactory.generateSecret(desKeySpec); Cipher cipher = Cipher.getInstance("DES/ECB/NoPadding"); cipher.init(Cipher.ENCRYPT_MODE, secretKey); ByteBuffer buf = new ByteBuffer(cipher.doFinal(challenge.data, challenge.offset, challenge.length)); return buf; } catch (Exception e) { throw new RuntimeException("Cannot encode password.", e); } } /** * Reverse bits in byte, so least significant bit will be most significant * bit. E.g. 01001100 will become 00110010. * * See also: http://www.vidarholen.net/contents/junk/vnc.html , * http://bytecrafter .blogspot.com/2010/09/des-encryption-as-used-in-vnc.html * * @param b * a byte * @return byte in reverse order */ private static byte flipByte(byte b) { int b1_8 = (b & 0x1) << 7; int b2_7 = (b & 0x2) << 5; int b3_6 = (b & 0x4) << 3; int b4_5 = (b & 0x8) << 1; int b5_4 = (b & 0x10) >>> 1; int b6_3 = (b & 0x20) >>> 3; int b7_2 = (b & 0x40) >>> 5; int b8_1 = (b & 0x80) >>> 7; byte c = (byte)(b1_8 | b2_7 | b3_6 | b4_5 | b5_4 | b6_3 | b7_2 | b8_1); return c; } /** * Read authentication result, send nothing. */ private void stage1(ByteBuffer buf, Link link) { // Read authentication response if (!cap(buf, 4, 4, link, false)) return; int authResult = buf.readSignedInt(); switch (authResult) { case RfbConstants.VNC_AUTH_OK: { // Nothing to do if (verbose) System.out.println("[" + this + "] INFO: Authentication successfull."); break; } case RfbConstants.VNC_AUTH_TOO_MANY: throw new RuntimeException("Connection to VNC server failed: too many wrong attempts."); case RfbConstants.VNC_AUTH_FAILED: throw new RuntimeException("Connection to VNC server failed: wrong password."); default: throw new RuntimeException("Connection to VNC server failed, reason code: " + authResult); } switchOff(); } @Override public String toString() { return "VNC3.3 Authentication(" + id + ")"; } /** * Example. */ public static void main(String args[]) { // System.setProperty("streamer.Link.debug", "true"); System.setProperty("streamer.Element.debug", "true"); // System.setProperty("streamer.Pipeline.debug", "true"); final String password = "test"; Element source = new MockSource("source") { { bufs = ByteBuffer.convertByteArraysToByteBuffers( // Request authentication and send 16 byte challenge new byte[] {0, 0, 0, RfbConstants.VNC_AUTH, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, // Respond to challenge with AUTH_OK new byte[] {0, 0, 0, RfbConstants.VNC_AUTH_OK}); } }; Element mainSink = new FakeSink("mainSink"); final Vnc33Authentication auth = new Vnc33Authentication("auth", password); Element initSink = new MockSink("initSink") { { // Expect encoded password bufs = new ByteBuffer[] {auth.encodePassword(new ByteBuffer(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}), password)}; } }; Pipeline pipeline = new PipelineImpl("test"); pipeline.addAndLink(source, auth, mainSink); pipeline.add(initSink); pipeline.link("auth >otout", "initSink"); pipeline.runMainLoop("source", STDOUT, false, false); } }