| // 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); |
| |
| } |
| } |