1
0
Fork 0
mirror of https://github.com/openstf/stf synced 2025-10-04 18:29:17 +02:00

VNC authentication works, although there is no UI for adding passwords

yet. Direct database manipulation is required for now.
This commit is contained in:
Simo Kinnunen 2015-10-13 03:14:18 +09:00
parent a902c66131
commit 5b5520b705
8 changed files with 332 additions and 32 deletions

17
doc/VNC.doc Normal file
View file

@ -0,0 +1,17 @@
# VNC
## Implementation details
### Authentication
#### According to the spec
VNC authentication is very weak by default, and doesn't encrypt traffic in any way. It works by sending a random 16-byte challenge to the user, who then encrypts with his/her password and sends back the 16-byte result. The server then encrypts the challenge as well, and checks whether the result sent by the client matches the server's result. Passwords are required to be 8 characters long. Shorter passwords are padded with zeroes and longer passwords simply truncated. Both the server and the client have to know the password. There are no usernames.
#### The way we do it
Since the authentication is very weak anyway, we might as well exploit it. The problem with the spec method is that since there's no username, it's difficult to know *who* wants to connect to a device. The only place for any kind of information is the password, but without knowing the password we can't decrypt the challenge response to see the contents. We could use a bruteforce method against our whole user database, of course, but that doesn't really scale.
Instead, we send over a *static* challenge, e.g. 16 zeroes, every time. Then we simply identify the user by the returned challenge response itself, which will be unique for each password. This makes the authentication more susceptible to eavesdropping since responses from previous sessions could be reused, but given the already weak nature of the password this shouldn't be a massive downgrade, and we should be running inside an internal network anyway. For real security, all connections should be over a secure tunnel.
Furthermore, one password is only valid for a single device. This will enable interesting proxying and/or load balancing opportunities in the future as we should be able to expose every single device in the system via a single port, if desired.

View file

@ -100,6 +100,27 @@ dbapi.lookupUserByAdbFingerprint = function(fingerprint) {
}) })
} }
dbapi.lookupUserByVncAuthResponse = function(response, serial) {
return db.run(r.table('vncauth').getAll([response, serial], {
index: 'responsePerDevice'
})
.eqJoin('userId', r.table('users'))('right')
.pluck('email', 'name', 'group'))
.then(function(cursor) {
return cursor.toArray()
})
.then(function(groups) {
switch (groups.length) {
case 1:
return groups[0]
case 0:
return null
default:
throw new Error('Found multiple users with the same VNC response')
}
})
}
dbapi.loadGroup = function(email) { dbapi.loadGroup = function(email) {
return db.run(r.table('devices').getAll(email, { return db.run(r.table('devices').getAll(email, {
index: 'owner' index: 'owner'

View file

@ -14,6 +14,15 @@ module.exports = {
} }
} }
} }
, vncauth: {
primaryKey: 'password'
, indexes: {
response: null
, responsePerDevice: function(row) {
return [row('response'), row('deviceId')]
}
}
}
, devices: { , devices: {
primaryKey: 'serial' primaryKey: 'serial'
, indexes: { , indexes: {

View file

@ -8,6 +8,9 @@ var uuid = require('node-uuid')
var jpeg = require('jpeg-turbo') var jpeg = require('jpeg-turbo')
var logger = require('../../../../util/logger') var logger = require('../../../../util/logger')
var grouputil = require('../../../../util/grouputil')
var wire = require('../../../../wire')
var wireutil = require('../../../../wire/util')
var lifecycle = require('../../../../util/lifecycle') var lifecycle = require('../../../../util/lifecycle')
var VncServer = require('./util/server') var VncServer = require('./util/server')
@ -15,11 +18,75 @@ var VncConnection = require('./util/connection')
var PointerTranslator = require('./util/pointertranslator') var PointerTranslator = require('./util/pointertranslator')
module.exports = syrup.serial() module.exports = syrup.serial()
.dependency(require('../../support/router'))
.dependency(require('../../support/push'))
.dependency(require('../screen/stream')) .dependency(require('../screen/stream'))
.dependency(require('../touch')) .dependency(require('../touch'))
.define(function(options, screenStream, touch) { .dependency(require('../group'))
.dependency(require('../solo'))
.define(function(options, router, push, screenStream, touch, group, solo) {
var log = logger.createLogger('device:plugins:vnc') var log = logger.createLogger('device:plugins:vnc')
function vncAuthHandler(data) {
log.info('VNC authentication attempt using "%s"', data.fingerprint)
var resolver = Promise.defer()
function notify() {
group.get()
.then(function(currentGroup) {
push.send([
solo.channel
, wireutil.envelope(new wire.JoinGroupByVncAuthResponseMessage(
options.serial
, data.response.toString('hex')
, currentGroup.group
))
])
})
.catch(grouputil.NoGroupError, function() {
push.send([
solo.channel
, wireutil.envelope(new wire.JoinGroupByVncAuthResponseMessage(
options.serial
, data.response.toString('hex')
))
])
})
}
function joinListener(newGroup, identifier) {
if (!data.response.equals(new Buffer(identifier || '', 'hex'))) {
resolver.reject(new Error('Someone else took the device'))
}
}
function autojoinListener(identifier, joined) {
if (data.response.equals(new Buffer(identifier, 'hex'))) {
if (joined) {
resolver.resolve()
}
else {
resolver.reject(new Error('Device is already in use'))
}
}
}
group.on('join', joinListener)
group.on('autojoin', autojoinListener)
router.on(wire.VncAuthResponsesUpdatedMessage, notify)
notify()
return resolver.promise
.timeout(5000)
.finally(function() {
group.removeListener('join', joinListener)
group.removeListener('autojoin', autojoinListener)
router.removeListener(wire.VncAuthResponsesUpdatedMessage, notify)
})
}
function createServer() { function createServer() {
log.info('Starting VNC server on port %d', options.vncPort) log.info('Starting VNC server on port %d', options.vncPort)
@ -27,6 +94,11 @@ module.exports = syrup.serial()
name: options.serial name: options.serial
, width: options.vncInitialSize[0] , width: options.vncInitialSize[0]
, height: options.vncInitialSize[1] , height: options.vncInitialSize[1]
, security: [{
type: VncConnection.SECURITY_VNC
, challenge: new Buffer(16).fill(0)
, auth: vncAuthHandler
}]
} }
var vnc = new VncServer(net.createServer({ var vnc = new VncServer(net.createServer({
@ -57,6 +129,8 @@ module.exports = syrup.serial()
return createServer() return createServer()
.then(function(vnc) { .then(function(vnc) {
vnc.on('connection', function(conn) { vnc.on('connection', function(conn) {
log.info('New VNC connection from %s', conn.conn.remoteAddress)
var id = util.format('vnc-%s', uuid.v4()) var id = util.format('vnc-%s', uuid.v4())
var connState = { var connState = {
@ -89,23 +163,6 @@ module.exports = syrup.serial()
touch.touchCommit() touch.touchCommit()
}) })
function vncStartListener(frameProducer) {
return new Promise(function(resolve/*, reject*/) {
connState.frameWidth = frameProducer.banner.virtualWidth
connState.frameHeight = frameProducer.banner.virtualHeight
resolve()
})
}
function vncFrameListener(frame) {
return new Promise(function(resolve/*, reject*/) {
connState.lastFrame = frame
connState.lastFrameTime = Date.now()
maybeSendFrame()
resolve()
})
}
function maybeSendFrame() { function maybeSendFrame() {
if (!connState.updateRequests) { if (!connState.updateRequests) {
return return
@ -142,6 +199,27 @@ module.exports = syrup.serial()
connState.sentFrameTime = connState.lastFrameTime connState.sentFrameTime = connState.lastFrameTime
} }
function vncStartListener(frameProducer) {
return new Promise(function(resolve/*, reject*/) {
connState.frameWidth = frameProducer.banner.virtualWidth
connState.frameHeight = frameProducer.banner.virtualHeight
resolve()
})
}
function vncFrameListener(frame) {
return new Promise(function(resolve/*, reject*/) {
connState.lastFrame = frame
connState.lastFrameTime = Date.now()
maybeSendFrame()
resolve()
})
}
function groupLeaveListener() {
conn.end()
}
conn.on('authenticated', function() { conn.on('authenticated', function() {
screenStream.updateProjection( screenStream.updateProjection(
options.vncInitialSize[0], options.vncInitialSize[1]) options.vncInitialSize[0], options.vncInitialSize[1])
@ -157,7 +235,7 @@ module.exports = syrup.serial()
}) })
conn.on('formatchange', function(format) { conn.on('formatchange', function(format) {
var same = os.endianness() == 'BE' == format.bigEndianFlag var same = os.endianness() === 'BE' === format.bigEndianFlag
switch (format.bitsPerPixel) { switch (format.bitsPerPixel) {
case 8: case 8:
connState.frameConfig = { connState.frameConfig = {
@ -191,7 +269,14 @@ module.exports = syrup.serial()
conn.on('close', function() { conn.on('close', function() {
screenStream.broadcastSet.remove(id) screenStream.broadcastSet.remove(id)
group.removeListener('leave', groupLeaveListener)
}) })
conn.on('userActivity', function() {
group.keepalive()
})
group.on('leave', groupLeaveListener)
}) })
lifecycle.observe(function() { lifecycle.observe(function() {

View file

@ -1,10 +1,13 @@
var util = require('util') var util = require('util')
var os = require('os') var os = require('os')
var crypto = require('crypto')
var EventEmitter = require('eventemitter3').EventEmitter var EventEmitter = require('eventemitter3').EventEmitter
var debug = require('debug')('vnc:connection') var debug = require('debug')('vnc:connection')
var Promise = require('bluebird')
var PixelFormat = require('./pixelformat') var PixelFormat = require('./pixelformat')
var vncauth = require('../../../../../util/vncauth')
function VncConnection(conn, options) { function VncConnection(conn, options) {
this.options = options this.options = options
@ -21,7 +24,15 @@ function VncConnection(conn, options) {
this._changeState(VncConnection.STATE_NEED_CLIENT_VERSION) this._changeState(VncConnection.STATE_NEED_CLIENT_VERSION)
this._serverVersion = VncConnection.V3_008 this._serverVersion = VncConnection.V3_008
this._serverSupportedSecurity = [VncConnection.SECURITY_NONE] this._serverSupportedSecurity = this.options.security
this._serverSupportedSecurityByType =
this.options.security.reduce(
function(map, method) {
map[method.type] = method
return map
}
, Object.create(null)
)
this._serverWidth = this.options.width this._serverWidth = this.options.width
this._serverHeight = this.options.height this._serverHeight = this.options.height
this._serverPixelFormat = new PixelFormat({ this._serverPixelFormat = new PixelFormat({
@ -45,12 +56,16 @@ function VncConnection(conn, options) {
this._clientEncodings = [] this._clientEncodings = []
this._clientCutTextLength = 0 this._clientCutTextLength = 0
this._authChallenge = this.options.challenge || crypto.randomBytes(16)
this.conn = conn this.conn = conn
.on('error', this._bound._errorListener) .on('error', this._bound._errorListener)
.on('readable', this._bound._readableListener) .on('readable', this._bound._readableListener)
.on('end', this._bound._endListener) .on('end', this._bound._endListener)
.on('close', this._bound._closeListener) .on('close', this._bound._closeListener)
this._blockingOps = []
this._writeServerVersion() this._writeServerVersion()
this._read() this._read()
} }
@ -80,6 +95,7 @@ var StateReverse = Object.create(null), State = {
STATE_NEED_CLIENT_VERSION: 10 STATE_NEED_CLIENT_VERSION: 10
, STATE_NEED_CLIENT_SECURITY: 20 , STATE_NEED_CLIENT_SECURITY: 20
, STATE_NEED_CLIENT_INIT: 30 , STATE_NEED_CLIENT_INIT: 30
, STATE_NEED_CLIENT_VNC_AUTH: 31
, STATE_NEED_CLIENT_MESSAGE: 40 , STATE_NEED_CLIENT_MESSAGE: 40
, STATE_NEED_CLIENT_MESSAGE_SETPIXELFORMAT: 50 , STATE_NEED_CLIENT_MESSAGE_SETPIXELFORMAT: 50
, STATE_NEED_CLIENT_MESSAGE_SETENCODINGS: 60 , STATE_NEED_CLIENT_MESSAGE_SETENCODINGS: 60
@ -171,18 +187,12 @@ VncConnection.prototype._writeSupportedSecurity = function() {
chunk[0] = this._serverSupportedSecurity.length chunk[0] = this._serverSupportedSecurity.length
this._serverSupportedSecurity.forEach(function(security, i) { this._serverSupportedSecurity.forEach(function(security, i) {
chunk[1 + i] = security chunk[1 + i] = security.type
}) })
this._write(chunk) this._write(chunk)
} }
VncConnection.prototype._writeSelectedSecurity = function() {
var chunk = new Buffer(4)
chunk.writeUInt32BE(VncConnection.SECURITY_NONE, 0)
this._write(chunk)
}
VncConnection.prototype._writeSecurityResult = function(result, reason) { VncConnection.prototype._writeSecurityResult = function(result, reason) {
var chunk var chunk
switch (result) { switch (result) {
@ -224,11 +234,40 @@ VncConnection.prototype._writeServerInit = function() {
this._write(chunk) this._write(chunk)
} }
VncConnection.prototype._writeVncAuthChallenge = function() {
var vncSec = this._serverSupportedSecurityByType[VncConnection.SECURITY_VNC]
debug('vnc auth challenge', vncSec.challenge)
this._write(vncSec.challenge)
}
VncConnection.prototype._readableListener = function() { VncConnection.prototype._readableListener = function() {
this._read() this._read()
} }
VncConnection.prototype._read = function() { VncConnection.prototype._read = function() {
Promise.all(this._blockingOps).bind(this)
.then(this._unguardedRead)
}
VncConnection.prototype._auth = function(type, data) {
var security = this._serverSupportedSecurityByType[type]
this._blockingOps.push(
security.auth(data).bind(this)
.then(function() {
this._changeState(VncConnection.STATE_NEED_CLIENT_INIT)
this._writeSecurityResult(VncConnection.SECURITYRESULT_OK)
this.emit('authenticated')
this._read()
})
.catch(function() {
this._writeSecurityResult(
VncConnection.SECURITYRESULT_FAIL, 'Authentication failure')
this.end()
})
)
}
VncConnection.prototype._unguardedRead = function() {
var chunk, lo, hi var chunk, lo, hi
while (this._append(this.conn.read())) { while (this._append(this.conn.read())) {
do { do {
@ -250,14 +289,35 @@ VncConnection.prototype._read = function() {
if ((chunk = this._consume(1))) { if ((chunk = this._consume(1))) {
if ((this._clientSecurity = this._parseSecurity(chunk)) === null) { if ((this._clientSecurity = this._parseSecurity(chunk)) === null) {
this._writeSecurityResult( this._writeSecurityResult(
VncConnection.SECURITYRESULT_FAIL, 'Unsupported security type') VncConnection.SECURITYRESULT_FAIL, 'Unimplemented security type')
this.end() this.end()
return return
} }
debug('client security', this._clientSecurity) debug('client security', this._clientSecurity)
this._writeSecurityResult(VncConnection.SECURITYRESULT_OK) if (!(this._clientSecurity in this._serverSupportedSecurityByType)) {
this.emit('authenticated') this._writeSecurityResult(
this._changeState(VncConnection.STATE_NEED_CLIENT_INIT) VncConnection.SECURITYRESULT_FAIL, 'Unsupported security type')
this.end()
return
}
switch (this._clientSecurity) {
case VncConnection.SECURITY_NONE:
this._auth(VncConnection.SECURITY_NONE)
return
case VncConnection.SECURITY_VNC:
this._writeVncAuthChallenge()
this._changeState(VncConnection.STATE_NEED_CLIENT_VNC_AUTH)
break
}
}
break
case VncConnection.STATE_NEED_CLIENT_VNC_AUTH:
if ((chunk = this._consume(16))) {
this._auth(VncConnection.SECURITY_VNC, {
response: chunk
, fingerprint: vncauth.format(chunk)
})
return
} }
break break
case VncConnection.STATE_NEED_CLIENT_INIT: case VncConnection.STATE_NEED_CLIENT_INIT:
@ -284,14 +344,17 @@ VncConnection.prototype._read = function() {
VncConnection.STATE_NEED_CLIENT_MESSAGE_FBUPDATEREQUEST) VncConnection.STATE_NEED_CLIENT_MESSAGE_FBUPDATEREQUEST)
break break
case VncConnection.CLIENT_MESSAGE_KEYEVENT: case VncConnection.CLIENT_MESSAGE_KEYEVENT:
this.emit('userActivity')
this._changeState( this._changeState(
VncConnection.STATE_NEED_CLIENT_MESSAGE_KEYEVENT) VncConnection.STATE_NEED_CLIENT_MESSAGE_KEYEVENT)
break break
case VncConnection.CLIENT_MESSAGE_POINTEREVENT: case VncConnection.CLIENT_MESSAGE_POINTEREVENT:
this.emit('userActivity')
this._changeState( this._changeState(
VncConnection.STATE_NEED_CLIENT_MESSAGE_POINTEREVENT) VncConnection.STATE_NEED_CLIENT_MESSAGE_POINTEREVENT)
break break
case VncConnection.CLIENT_MESSAGE_CLIENTCUTTEXT: case VncConnection.CLIENT_MESSAGE_CLIENTCUTTEXT:
this.emit('userActivity')
this._changeState( this._changeState(
VncConnection.STATE_NEED_CLIENT_MESSAGE_CLIENTCUTTEXT) VncConnection.STATE_NEED_CLIENT_MESSAGE_CLIENTCUTTEXT)
break break

View file

@ -124,12 +124,46 @@ module.exports = function(options) {
}) })
.catch(function(err) { .catch(function(err) {
log.error( log.error(
'Unable to lookup user by fingerprint "%s"' 'Unable to lookup user by ADB fingerprint "%s"'
, message.fingerprint , message.fingerprint
, err.stack , err.stack
) )
}) })
}) })
.on(wire.JoinGroupByVncAuthResponseMessage, function(channel, message) {
dbapi.lookupUserByVncAuthResponse(message.response, message.serial)
.then(function(user) {
if (user) {
devDealer.send([
channel
, wireutil.envelope(new wire.AutoGroupMessage(
new wire.OwnerMessage(
user.email
, user.name
, user.group
)
, message.response
))
])
}
else if (message.currentGroup) {
appDealer.send([
message.currentGroup
, wireutil.envelope(new wire.JoinGroupByVncAuthResponseMessage(
message.serial
, message.response
))
])
}
})
.catch(function(err) {
log.error(
'Unable to lookup user by VNC auth response "%s"'
, message.response
, err.stack
)
})
})
.on(wire.JoinGroupMessage, function(channel, message, data) { .on(wire.JoinGroupMessage, function(channel, message, data) {
dbapi.setDeviceOwner(message.serial, message.owner) dbapi.setDeviceOwner(message.serial, message.owner)
appDealer.send([channel, data]) appDealer.send([channel, data])

60
lib/util/vncauth.js Normal file
View file

@ -0,0 +1,60 @@
var crypto = require('crypto')
// See http://graphics.stanford.edu/~seander/bithacks.html#ReverseByteWith32Bits
function reverseByteBits(b) {
return ((b * 0x0802 & 0x22110) | (b * 0x8020 & 0x88440)) * 0x10101 >> 16 & 0xFF
}
function reverseBufferByteBits(b) {
var result = new Buffer(b.length)
for (var i = 0; i < result.length; ++i) {
result[i] = reverseByteBits(b[i])
}
return result
}
function normalizePassword(password) {
var key = new Buffer(8).fill(0)
// Make sure the key is always 8 bytes long. VNC passwords cannot be
// longer than 8 bytes. Shorter passwords are padded with zeroes.
reverseBufferByteBits(password).copy(key, 0, 0, 8)
return key
}
function encrypt(challenge, password) {
var key = normalizePassword(password)
, iv = new Buffer(0).fill(0)
// Note: do not call .final(), .update() is the one that gives us the
// desired result.
return crypto.createCipheriv('des-ecb', key, iv).update(challenge)
}
module.exports.encrypt = encrypt
function decrypt(challenge, password) {
var key = normalizePassword(password)
, iv = new Buffer(0).fill(0)
// Note: do not call .final(), .update() is the one that gives us the
// desired result.
return crypto.createDecipheriv('des-ecb', key, iv).update(challenge)
}
module.exports.decrypt = decrypt
function format(fingerprint) {
return fingerprint.toString('hex').match(/\w{4}/g).join(':')
}
module.exports.format = format
function verify(response, challenge, password) {
return encrypt(challenge, password).equals(response)
}
module.exports.verify = verify

View file

@ -17,6 +17,8 @@ enum MessageType {
PhysicalIdentifyMessage = 29; PhysicalIdentifyMessage = 29;
JoinGroupMessage = 11; JoinGroupMessage = 11;
JoinGroupByAdbFingerprintMessage = 69; JoinGroupByAdbFingerprintMessage = 69;
JoinGroupByVncAuthResponseMessage = 90;
VncAuthResponsesUpdatedMessage = 91;
AutoGroupMessage = 70; AutoGroupMessage = 70;
AdbKeysUpdatedMessage = 71; AdbKeysUpdatedMessage = 71;
KeyDownMessage = 12; KeyDownMessage = 12;
@ -271,9 +273,18 @@ message JoinGroupByAdbFingerprintMessage {
optional string currentGroup = 4; optional string currentGroup = 4;
} }
message JoinGroupByVncAuthResponseMessage {
required string serial = 1;
required string response = 2;
optional string currentGroup = 4;
}
message AdbKeysUpdatedMessage { message AdbKeysUpdatedMessage {
} }
message VncAuthResponsesUpdatedMessage {
}
message LeaveGroupMessage { message LeaveGroupMessage {
required string serial = 1; required string serial = 1;
required OwnerMessage owner = 2; required OwnerMessage owner = 2;