diff --git a/client_auth.go b/client_auth.go index c88f911..91d73e7 100644 --- a/client_auth.go +++ b/client_auth.go @@ -16,7 +16,7 @@ type ClientAuth interface { } // ClientAuthNone is the "none" authentication. See 7.1.2 -type ClientAuthNone byte +type ClientAuthNone struct{} func (*ClientAuthNone) SecurityType() uint8 { return 1 diff --git a/client_auth_test.go b/client_auth_test.go index 746b46f..78a7935 100644 --- a/client_auth_test.go +++ b/client_auth_test.go @@ -1,6 +1,10 @@ package vnc -import "testing" +import ( + "encoding/hex" + "strings" + "testing" +) func TestClientAuthNone_Impl(t *testing.T) { var raw interface{} @@ -9,3 +13,48 @@ func TestClientAuthNone_Impl(t *testing.T) { t.Fatal("ClientAuthNone doesn't implement ClientAuth") } } + +func TestClientAuthVNC_Impl(t *testing.T) { + var raw interface{} + raw = new(ClientAuthVNC) + if _, ok := raw.(ClientAuth); !ok { + t.Fatal("ClientAuthVNC doesn't implement ClientAuth") + } +} + +// wiresharkToChallenge converts VNC authentication challenge and response +// values captured with Wireshark (https://www.wireshark.org) into usable byte +// streams. +func wiresharkToChallenge(h string) [challengeSize]byte { + var c [challengeSize]byte + r := strings.NewReplacer(":", "") + b, err := hex.DecodeString(r.Replace(h)) + if err != nil { + return c + } + copy(c[:], b) + return c +} + +func TestClientAuthVNCEncode(t *testing.T) { + tests := []struct { + pw string + challenge, response string + }{ + {".", "7f:e2:e1:3d:a4:ae:10:9c:54:c5:5f:52:74:aa:db:31", "1d:86:92:71:1f:00:24:35:02:d3:91:ef:e9:bc:c5:d5"}, + {"12345678", "13:8e:a4:2e:0e:66:f3:ad:2d:f3:08:c3:04:cd:c4:2a", "5b:e1:56:fa:49:49:ef:56:d3:f8:44:97:73:27:95:9f"}, + {"abc123", "c6:30:45:d2:57:9e:e7:f2:f9:0c:62:3e:52:40:86:c6", "a3:63:59:e4:28:c8:7f:b3:45:2c:d7:e0:ca:d6:70:3e"}, + } + + for _, tt := range tests { + challenge := wiresharkToChallenge(tt.challenge) + a := ClientAuthVNC{tt.pw} + if err := a.encode(&challenge); err != nil { + t.Errorf("ClientAuthVNC.encode() failed: key=%v, err=%v", tt.pw, err) + } + response := wiresharkToChallenge(tt.response) + if challenge != response { + t.Errorf("ClientAuthVNC.encode() failed: key=%v got=%v, want=%v", tt.pw, challenge, response) + } + } +} diff --git a/client_auth_vnc.go b/client_auth_vnc.go new file mode 100644 index 0000000..df81aa8 --- /dev/null +++ b/client_auth_vnc.go @@ -0,0 +1,67 @@ +/* +ClientAuthVNC implements the ClientAuth interface to provide support for +VNC Authentication. + +See http://tools.ietf.org/html/rfc6143#section-7.2.2 for more info. +*/ +package vnc + +import ( + "crypto/des" + "encoding/binary" + "net" +) + +// ClientAuthVNC is the standard password authentication +type ClientAuthVNC struct { + Password string +} + +func (*ClientAuthVNC) SecurityType() uint8 { + return 2 +} + +// 7.2.2. VNC Authentication uses a 16-byte challenge. +const challengeSize = 16 + +func (auth *ClientAuthVNC) Handshake(conn net.Conn) error { + // Read challenge block + var challenge [challengeSize]byte + if err := binary.Read(conn, binary.BigEndian, &challenge); err != nil { + return err + } + + auth.encode(&challenge) + + // Send the encrypted challenge back to server + if err := binary.Write(conn, binary.BigEndian, challenge); err != nil { + return err + } + + return nil +} + +func (auth *ClientAuthVNC) encode(c *[challengeSize]byte) error { + // Copy password string to 8 byte 0-padded slice + key := make([]byte, 8) + copy(key, auth.Password) + + // Each byte of the password needs to be reversed. This is a + // non RFC-documented behaviour of VNC clients and servers + for i := range key { + key[i] = (key[i]&0x55)<<1 | (key[i]&0xAA)>>1 // Swap adjacent bits + key[i] = (key[i]&0x33)<<2 | (key[i]&0xCC)>>2 // Swap adjacent pairs + key[i] = (key[i]&0x0F)<<4 | (key[i]&0xF0)>>4 // Swap the 2 halves + } + + // Encrypt challenge with key. + cipher, err := des.NewCipher(key) + if err != nil { + return err + } + for i := 0; i < challengeSize; i += cipher.BlockSize() { + cipher.Encrypt(c[i:i+cipher.BlockSize()], c[i:i+cipher.BlockSize()]) + } + + return nil +}