blob: 6fdb3b1f69e2a6ba8fa8fac9b0a41a1cc101cc27 [file] [log] [blame]
// 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);
}
}