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:
parent
a902c66131
commit
5b5520b705
8 changed files with 332 additions and 32 deletions
17
doc/VNC.doc
Normal file
17
doc/VNC.doc
Normal 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.
|
|
@ -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'
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
60
lib/util/vncauth.js
Normal 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
|
|
@ -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;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue