const { Duplex } = require('stream')
const net = require('net')
const crypto = require('crypto')
const {
createHash, createHmac, createSign, createCipheriv,
createDecipheriv, publicEncrypt, randomFillSync, constants
} = crypto
const { alloc, concat, from } = Buffer
const { asn1, pki } = require('node-forge')
/**
* K combinator is a higher-order function which accepts two
* expressions `x` and `y`. It evalutes `x` then `y` and
* returns `x` finally. It is helpful for compact code.
* @function
*/
const K = x => y => x
/** @constant {buffer} - TLS version 1.2 */
const VER12 = from([0x03, 0x03])
/** @constant {buffer} - cipher suite */
const AES_128_CBC_SHA = from([0x00, 0x2f])
/** @constant {buffer} - signature algorithm */
const RSA_PKCS1_SHA256 = from([0x04, 0x01])
/** @constant {number} - for public key encryption padding */
const RSA_PKCS1_PADDING = constants.RSA_PKCS1_PADDING
/**
* reads a 24bit unsigned integer from the first 3-byte of a buffer
* @param {buffer} buf
* @returns {number}
*/
const readUInt24 = buf => buf[0] * 65536 + buf[1] * 256 + buf[2]
/**
* prepends 2-byte length to given buffer
* @param {buffer} b
* @returns {buffer}
*/
const prepend16 = b => concat([from([b.length >> 8, b.length]), b])
/**
* prepends 3-byte length to given buffer
* @param {buffer} b
* @returns {buffer}
*/
const prepend24 = b =>
concat([from([b.length >> 16, b.length >> 8, b.length]), b])
/**
* calculates sha256 digest
* @param {buffer} data
* @returns {buffer}
*/
const SHA256 = data => createHash('sha256').update(data).digest()
/**
* calculates sha1 hmac
* @param {buffer} key - mac key
* @param {buffer} data
* @returns {buffer}
*/
const HMAC1 = (key, data) => createHmac('sha1', key).update(data).digest()
/**
* calculates sha256 hmac
* @param {buffer} key - mac key
* @param {buffer} data
* @returns {buffer}
*/
const HMAC256 = (key, data) => createHmac('sha256', key).update(data).digest()
/**
* pseudo random function for key generation and expansion
* see rfc5246
*
* @function
* @param {buffer} secret
* @param {string} label
* @param {buffer} seed
* @param {number} length
* @returns {buffer} buffer of given length
*/
const PRF256 = (secret, label, seed, length) => {
seed = concat([from(label, 'binary'), seed])
let P_HASH = Buffer.alloc(0)
for (let A = from(seed); P_HASH.length < length;
A = HMAC256(secret, A),
P_HASH = concat([P_HASH, HMAC256(secret, concat([A, seed]))]));
return P_HASH.slice(0, length)
}
/**
* A sequence number function returns sequence number starting from 0
* @typedef SequenceNumberFunction
* @type {function}
* @return {buffer}
*/
/**
* create a sequence number function
* @returns {SequenceNumberFunction}
*/
const createSequenceNumber = () => {
const buf = Buffer.alloc(8)
return () => {
const r = from(buf)
buf.writeUInt32BE(buf.readUInt32BE(4) + 1, 4)
if (buf.readUInt32BE(4) === 0) {
buf.writeUInt32BE(buf.readUInt32BE(0) + 1, 0)
// rfc5246, seq num should never overflow since it is a 64bit number
// so this error is considered to be an internal error
if (buf.readUInt32BE(0) === 0) {
throw new Error('sequence number overflow')
}
}
return r
}
}
/**
* A cipher function encrypts a tls record.
* @typedef CipherFunction
* @type {function}
* @param { type - tls record type
* @param {buffer} data - tls record data (payload)
* @returns {buffer} encrypted tls record
*/
/**
* This is a (higher-order) factory function to generate a cipher function,
* which maintains sequence number internally.
* @function createCipher
* @param {buffer} key - encryption key
* @param {buffer} macKey - hmac key
* @param {bigint} _iv - initial iv
* @returns {CipherFunction}
*/
const createCipher = (key, macKey, _iv) => {
const SN = createSequenceNumber()
return (type, data) => {
const iv = SHA256((++_iv).toString()).slice(0, 16)
const tbs = concat([SN(), from([type]), VER12, prepend16(data)])
const mac = HMAC1(macKey, tbs)
const len = 16 - (data.length + mac.length) % 16
const pad = Buffer.alloc(len, len - 1)
const c = createCipheriv('aes-128-cbc', key, iv).setAutoPadding(false)
return concat([iv, c.update(concat([data, mac, pad])), c.final()])
}
}
/**
* A decipher function decrypts a tls record.
* This function does NOT throw TLSError. The caller is resposible
* for translating the thrown error to TLSError.
*
* @typedef DecipherFunction
* @type {function}
* @param { type - tls record type
* @param {buffer} data - encrypted tls record data
* @returns {buffer} decrypted data (payload), mac verified and stripped
*/
/**
* This is a higher order factory funtion to generate a decipher function,
* which maintains sequence number internally.
*
* @function createDecipher
* @param {buffer} key - decryption key
* @param {buffer} macKey - hmac key
* @returns {DecipherFunction}
*/
const createDecipher = (key, macKey) => {
const SN = createSequenceNumber()
return (type, data) => {
const iv = data.slice(0, 16)
const d = createDecipheriv('aes-128-cbc', key, iv).setAutoPadding(false)
const dec = concat([d.update(data.slice(16)), d.final()])
const len = dec[dec.length - 1] + 1
if (dec.length < len) {
throw new Error('bad padding')
}
const pad = dec.slice(dec.length - len)
if (!pad.equals(Buffer.alloc(len, len - 1))) {
throw new Error('bad padding')
}
data = dec.slice(0, dec.length - len - 20)
const smac = dec.slice(dec.length - len - 20, dec.length - len)
const tbs = concat([SN(), from([type]), VER12, prepend16(data)])
const cmac = HMAC1(macKey, tbs)
if (!smac.equals(cmac)) throw new Error('mac mismatch')
return data
}
}
/** @enum {number} tls record content type */
const ContentType = {
CHANGE_CIPHER_SPEC: 20,
ALERT: 21,
HANDSHAKE: 22,
APPLICATION_DATA: 23
}
/**
* @param {number} content type
* @returns {string} content type name
*/
const contentType = type => {
const {
CHANGE_CIPHER_SPEC,
ALERT,
HANDSHAKE,
APPLICATION_DATA
} = ContentType
switch (type) {
case CHANGE_CIPHER_SPEC:
return 'change_cipher_spec'
case ALERT:
return 'alert'
case HANDSHAKE:
return 'handshake'
case APPLICATION_DATA:
return 'application_data'
default:
throw new Error('unknown content type')
}
}
/** @enum {number} - handshake message type */
const HandshakeType = {
HELLO_REQUEST: 0,
CLIENT_HELLO: 1,
SERVER_HELLO: 2,
CERTIFICATE: 11,
SERVER_KEY_EXCHANGE: 12,
CERTIFICATE_REQUEST: 13,
SERVER_HELLO_DONE: 14,
CERTIFICATE_VERIFY: 15,
CLIENT_KEY_EXCHANGE: 16,
FINISHED: 20
}
/**
* @param {number} handshake message type
* @returns {string} handshake message type name
*/
const handshakeType = type => {
const {
HELLO_REQUEST,
CLIENT_HELLO,
SERVER_HELLO,
CERTIFICATE,
SERVER_KEY_EXCHANGE,
CERTIFICATE_REQUEST,
SERVER_HELLO_DONE,
CERTIFICATE_VERIFY,
CLIENT_KEY_EXCHANGE,
FINISHED
} = HandshakeType
switch (type) {
case HELLO_REQUEST:
return 'HelloRequest'
case CLIENT_HELLO:
return 'ClientHello'
case SERVER_HELLO:
return 'ServerHello'
case CERTIFICATE:
return 'Certificate'
case SERVER_KEY_EXCHANGE:
return 'ServerKeyExchange'
case CERTIFICATE_REQUEST:
return 'CertificateRequest'
case SERVER_HELLO_DONE:
return 'ServerHelloDone'
case CERTIFICATE_VERIFY:
return 'CertificateVerify'
case CLIENT_KEY_EXCHANGE:
return 'ClientKeyExchange'
case FINISHED:
return 'Finished'
default:
throw new Error(`unknown handshake type ${type}`)
}
}
/** @enum {number} tls alert level */
const AlertLevel = {
WARNING: 1,
FATAL: 2
}
/**
* @param {number} alert level
* @returns {string} alert level name
*/
const alertLevel = level => {
switch (level) {
case AlertLevel.WARNING:
return 'warning'
case AlertLevel.FATAL:
return 'fatal'
default:
throw new Error(`unknown alert level ${level}`)
}
}
/** @enum {number} alert description */
const AlertDescription = {
CLOSE_NOTIFY: 0,
UNEXPECTED_MESSAGE: 10,
BAD_RECORD_MAC: 20,
DECRYPTION_FAILED_RESERVED: 21,
RECORD_OVERFLOW: 22,
DECOMPRESSION_FAILURE: 30,
HANDSHAKE_FAILURE: 40,
NO_CERTIFICATE_RESERVED: 41,
BAD_CERTIFICATE: 42,
UNSUPPORTED_CERTIFICATE: 43,
CERTIFICATE_REVOKED: 44,
CERTIFICATE_EXPIRED: 45,
CERTIFICATE_UNKNOWN: 46,
ILLEGAL_PARAMETER: 47,
UNKNOWN_CA: 48,
ACCESS_DENIED: 49,
DECODE_ERROR: 50,
DECRYPT_ERROR: 51,
EXPORT_RESTRICTION_RESERVED: 60,
PROTOCOL_VERSION: 70,
INSUFFICIENT_SECURITY: 71,
INTERNAL_ERROR: 80,
USER_CANCELED: 90,
NO_RENEGOTIATION: 100,
UNSUPPORTED_EXTENSION: 110
}
/**
* @param {number} alert description
* @returns {string} alert description name
*/
const alertDescription = desc => {
const {
CLOSE_NOTIFY,
UNEXPECTED_MESSAGE,
BAD_RECORD_MAC,
DECRYPTION_FAILED_RESERVED,
RECORD_OVERFLOW,
DECOMPRESSION_FAILURE,
HANDSHAKE_FAILURE,
NO_CERTIFICATE_RESERVED,
BAD_CERTIFICATE,
UNSUPPORTED_CERTIFICATE,
CERTIFICATE_REVOKED,
CERTIFICATE_EXPIRED,
CERTIFICATE_UNKNOWN,
ILLEGAL_PARAMETER,
UNKNOWN_CA,
ACCESS_DENIED,
DECODE_ERROR,
DECRYPT_ERROR,
EXPORT_RESTRICTION_RESERVED,
PROTOCOL_VERSION,
INSUFFICIENT_SECURITY,
INTERNAL_ERROR,
USER_CANCELED,
NO_RENEGOTIATION,
UNSUPPORTED_EXTENSION
} = AlertDescription
switch (desc) {
case CLOSE_NOTIFY:
return 'close_notify'
case UNEXPECTED_MESSAGE:
return 'unexpected_message'
case BAD_RECORD_MAC:
return 'bad_record_mac'
case DECRYPTION_FAILED_RESERVED:
return 'decryption_failed_reserved'
case RECORD_OVERFLOW:
return 'record_overflow'
case DECOMPRESSION_FAILURE:
return 'decompression_failure'
case HANDSHAKE_FAILURE:
return 'handshake_failure'
case NO_CERTIFICATE_RESERVED:
return 'no_certificate_reserved'
case BAD_CERTIFICATE:
return 'bad_certificate'
case UNSUPPORTED_CERTIFICATE:
return 'unsupported_certificate'
case CERTIFICATE_REVOKED:
return 'certificate_revoked'
case CERTIFICATE_EXPIRED:
return 'certificate_expired'
case CERTIFICATE_UNKNOWN:
return 'certificate_unknown'
case ILLEGAL_PARAMETER:
return 'illegal_parameter'
case UNKNOWN_CA:
return 'unknown_ca'
case ACCESS_DENIED:
return 'access_denied'
case DECODE_ERROR:
return 'decode_error'
case DECRYPT_ERROR:
return 'decrypt_error'
case EXPORT_RESTRICTION_RESERVED:
return 'export_restriction_reserved'
case PROTOCOL_VERSION:
return 'protocol_version'
case INSUFFICIENT_SECURITY:
return 'insufficient_security'
case INTERNAL_ERROR:
return 'internal_error'
case USER_CANCELED:
return 'user_canceled'
case NO_RENEGOTIATION:
return 'no_renegotiation'
case UNSUPPORTED_EXTENSION:
return 'unsupported_extension'
default: // description may be extended by other spec
return 'unknown_alert_description'
}
}
/**
* TLSError represents an error in tls protocol handling,
* such as decoding or decryption error, malformatted message
* or illegal value. It is not used for error emitted from
* dependent components, such as socket error or file system error.
*
* TLSError has no error code defined.
*/
class TLSError extends Error {
/**
* constructs a TLSError.
*
* If `msg` is an Error, TLSError preserves its properties, including
* message and stack. If `msg` is string, it is used as error message.
* If `msg` is not provided, TLSError use the alert description name as
* error message.
*
* @param {number} desc - alert description
* @param {string|Error} [msg] - error or error message
*/
constructor (desc, msg) {
if (msg instanceof Error) {
super(msg.message)
Object.assign(this, msg)
this.stack = msg.stack
} else {
super(msg || alertDescription(desc))
}
this.name = this.name || this.constructor.name
/** alert level */
this.level = AlertLevel.FATAL
/** alert description */
this.description = desc
}
}
/**
* TLSAlert represents a tls alert received from the other party.
*
* TLSAlert has no error code defined.
*/
class TLSAlert extends Error {
/**
* constructs a TLSAlert
* @param {number} desc - alert description
* @param {number} [level] - alter level, defauts to `FATAL`
*/
constructor (desc, level = AlertLevel.FATAL) {
super(alertDescription(desc))
this.name = this.constructor.name
/** alert level */
this.level = level
/** alert description */
this.description = desc
}
}
/**
* @typedef {object} Fragment
* @property {number} type - content type
* @property {Buffer} data - fragment data
*/
/**
* @typedef {object} Message
* @property {number} type - content type
* @property {Buffer} data - message data (no fragment)
*/
/**
* #### States
*
* Telsa has four internal states:
*
* - Connecting
* - Handshaking
* - Established
* - Terminated
*
* The following operations or events are *external* events
* in terms of a state machine.
* - `_write`
* - `_final`
* - `_read`
* - socket error
* - read path error when processing socket data, including:
* + tls protocol error, defined as alert description
* + operation errors returned from asynchronous operation
* * some errors are protocol error, such as certificate not verified
* * others are operation errors, such as child process crashed.
* + other exceptions
* - write path error when executing `_write` operation:
* + all errors are exceptions (which is internal error in alert description)
* - server alert
* + fatal alert
* + close_notify
* + other warning alerts other than close_notify
* - close `without a server alert`
*
* #### Stateful Resources
* - write path
* + if a `_write` operation cannot be performed immediately, it is blocked.
* + if the underlying socket `write` returns `false` during a `_write` operation.
* - read path
* + the underlying socket may be paused
*
* #### Finish write path
* - the underlying socket could be finished by `end` method.
*
* #### Finish read path
* - the read path could be finished by push a null. This is only necessary
* after the tls is connected.
*
* #### Connecting State
*
* In connecting state, only the following
*/
class Telsa extends Duplex {
/**
* @param {object} opts
*/
constructor (opts) {
super(opts)
/** options */
this.opts = opts
if (!this.opts.ca) {
throw new Error('ca not provided')
}
/** root ca in forge format */
this.ca = pki.certificateFromPem(this.opts.ca)
/** forge ca store */
this.caStore = pki.createCaStore([this.ca])
/**
* blocked or draining `_write` operation
* - `null` if no blocked `_write`
* - `{ chunk, encoding, callback }` if a `_write` is blocked
* - `callback` if the operation is waiting for draining
* @type {object|function}
*/
this.writing = null
/**
* incomming data buffer, may contain fragmented records.
* @type {Buffer}
*/
this.incomming = Buffer.alloc(0)
/**
* current fragment, contains 0, 1 or more records of the same type.
* @type {Fragment|null}
*/
this.fragment = null
/** session id */
this.sessionId = 0
/** client random */
this.clientRandom = randomFillSync(alloc(32))
/** server random */
this.serverRandom = undefined
/** pre-master secret */
this.preMasterSecret = concat([VER12, randomFillSync(alloc(46))])
/** master secret */
this.masterSecret = undefined
/** client write mac key */
this.clientWriteMacKey = undefined
/** server write mac key */
this.serverWriteMacKey = undefined
/** client key */
this.clientWriteKey = undefined
/** server key */
this.serverWriteKey = undefined
/** handshake messages */
this.msgs = []
/** @type {CipherFunction} */
this.cipher = null
/**
* @type {DecipherFunction}
*/
this.decipher = null
/**
* tcp connection
* @type {net.Socket}
*/
this.socket = this.opts.socket || net.createConnection(opts)
this.socket.on('connect', () => {
this.state = 'HANDSHAKING'
/**
* handshake context
* @type {HandshakeContext}
*/
this.socket.on('close', () => this.terminate('socket'))
this.socket.on('data', data => {
try {
this.handleSocketData(data)
} catch (e) {
this.handleError(e)
}
})
// start handshaking
this.sendClientHello()
})
this.socket.on('error', err => this.terminate('socket', err))
this.state = 'CONNECTING'
}
/**
* handle errors from data handler, asynchronous operations, but not
* socket error
*/
handleError (e) {
if (e instanceof TLSAlert) {
this.terminate('alert', e)
} else {
this.terminate('error', e)
}
}
/**
* @return max fragment length
*/
maxFragmentLength () {
if (this.decipher) {
return Math.pow(2, 14) + 2048
} else {
return Math.pow(2, 14)
}
}
/**
* read a record out of incomming data buffer
* @returns {Fragment} the record type and payload
*/
readFragment () {
const {
DECODE_ERROR,
RECORD_OVERFLOW,
BAD_RECORD_MAC
} = AlertDescription
if (this.incomming.length < 1) return
const type = this.incomming[0]
if (type < 20 || type > 23) {
throw new TLSError(DECODE_ERROR, 'bad content type')
}
if (this.incomming.length < 3) return
const version = this.incomming.readUInt16BE(1)
if (version !== 0x0303) {
throw new TLSError(DECODE_ERROR, 'bad protocol version')
}
if (this.incomming.length < 5) return
const length = this.incomming.readUInt16BE(3)
if (length === 0) {
throw new TLSError(DECODE_ERROR, 'zero record payload length')
}
if (length > this.maxFragmentLength()) {
throw new TLSError(RECORD_OVERFLOW, 'record overflow')
}
if (this.incomming.length < 5 + length) return
let data = this.incomming.slice(5, 5 + length)
this.incomming = this.incomming.slice(5 + length)
if (this.decipher) {
try {
data = this.decipher(type, data)
} catch (e) {
/**
* ```
* RFC 4346, TLS v1.1, page 28
* Note: Differentiating between bad_record_mac and decryption_failed
* alerts may permit certain attacks against CBC mode as used in
* TLS [CBCATT]. It is preferable to uniformly use the
* bad_record_mac alert to hide the specific type of the error.
* ```
*/
throw new TLSError(BAD_RECORD_MAC, e)
}
}
return { type, data }
}
/**
* shift data chunk with given size from current fragment
* @returns {Fragment}
*/
shiftFragment (size) {
const { DECODE_ERROR } = AlertDescription
if (!this.fragment || this.fragment.data.length < size) {
throw new TLSError(DECODE_ERROR, 'bad fragment size')
}
const type = this.fragment.type
const data = this.fragment.data.slice(0, size)
if (size === this.fragment.data.length) {
this.fragment = null
} else {
this.fragment.data = this.fragment.data.slice(size)
}
return { type, data }
}
/**
* read a message from current fragment
* @returns {Message}
*/
readMessageFromFragment () {
const { DECODE_ERROR } = AlertDescription
if (!this.fragment) return
switch (this.fragment.type) {
case ContentType.ALERT:
if (this.fragment.data.length < 2) return
return this.shiftFragment(2)
case ContentType.CHANGE_CIPHER_SPEC:
return this.shiftFragment(1)
case ContentType.HANDSHAKE: {
if (this.fragment.data.length < 4) return
const length = readUInt24(this.fragment.data.slice(1))
if (this.fragment.data.length < 4 + length) return
return this.shiftFragment(4 + length)
}
case ContentType.APPLICATION_DATA:
return this.shiftFragment(this.fragment.data.length)
default:
throw new TLSError(DECODE_ERROR, 'invalid content type')
}
}
/**
* read a message
* @returns {Message}
*/
readMessage () {
const { DECODE_ERROR } = AlertDescription
while (true) {
const msg = this.readMessageFromFragment()
if (msg) return msg
const frag = this.readFragment()
if (!frag) return
if (this.fragment) {
if (frag.type !== this.fragment.type) {
throw new TLSError(DECODE_ERROR, 'incomplete fragment')
}
this.fragment.data = Buffer.concat([this.fragment.data, frag.data])
} else {
this.fragment = frag
}
}
}
/**
* save handshake message
*/
saveMessage (from, msg) {
if (from !== 'server' && from !== 'client') {
throw new Error('invalid parameter')
}
msg.from = from
this.msgs.push(msg)
}
/**
* assert last handshake message from and type
*/
assertLast (from, type) {
const { UNEXPECTED_MESSAGE } = AlertDescription
if (from !== 'server' && from !== 'client') {
throw new Error('invalid parameter')
}
if (!this.msgs.length) {
const msg =
`expected ${handshakeType(type)} from ${from}, ` +
'actual none'
throw new TLSError(UNEXPECTED_MESSAGE, msg)
}
const last = this.msgs[this.msgs.length - 1]
if (last.from !== from || last[0] !== type) {
const msg =
`expected ${handshakeType(type)} from ${from}, ` +
`actual ${handshakeType(last[0])} from ${last.from}`
throw new TLSError(UNEXPECTED_MESSAGE, msg)
}
}
/** derive keys from pre-master secret, client and server random */
deriveKeys () {
this.masterSecret = PRF256(this.preMasterSecret, 'master secret',
concat([this.clientRandom, this.serverRandom]), 48)
const keys = PRF256(this.masterSecret, 'key expansion',
concat([this.serverRandom, this.clientRandom]), 2 * (20 + 16) + 16)
this.clientWriteMacKey = keys.slice(0, 20)
this.serverWriteMacKey = keys.slice(20, 40)
this.clientWriteKey = keys.slice(40, 56)
this.serverWriteKey = keys.slice(56, 72)
this.iv = Array.from(keys.slice(72))
.reduce((sum, c, i) =>
(sum + BigInt(c) << (BigInt(8) * BigInt(i))), BigInt(0))
}
/**
* set server random and derives keys
* @param {buffer} random - server random
*/
setServerRandom (random) {
this.serverRandom = random
this.deriveKeys()
}
/** generates client verify data in client Finished message */
clientVerifyData () {
return PRF256(this.masterSecret, 'client finished',
SHA256(concat(this.msgs)), 12)
}
/** generates server verify data in server Finsihed message */
serverVerifyData () {
return PRF256(this.masterSecret, 'server finished',
SHA256(concat(this.msgs)), 12)
}
/**
* send change cipher spec message and set cipher
*/
changeCipherSpec () {
this.sendChangeCipherSpec()
this.cipher = createCipher(this.clientWriteKey,
this.clientWriteMacKey, this.iv)
}
/**
* set decipher
*/
// serverChangeCipherSpec (key, macKey) {
// this.decipher = createDecipher(key, macKey)
// }
/**
* handle socket data
* @param {Buffer} data - socket data
*/
handleSocketData (data) {
const { DECODE_ERROR } = AlertDescription
this.incomming = Buffer.concat([this.incomming, data])
while (true) {
const msg = this.readMessage()
if (!msg) return
const { type, data } = msg
switch (type) {
case ContentType.ALERT:
this.handleAlert(data)
break
case ContentType.CHANGE_CIPHER_SPEC:
this.handleChangeCipherSpec(data)
break
case ContentType.HANDSHAKE:
this.handleHandshakeMessage(data)
break
case ContentType.APPLICATION_DATA:
this.handleApplicationData(data)
break
default:
throw new TLSError(DECODE_ERROR, 'invalid content type')
}
}
}
/**
* handle alert message, all warnings are bypassed except `close_notify`
* @param {Buffer} data
*/
handleAlert (data) {
const { DECODE_ERROR, CLOSE_NOTIFY } = AlertDescription
const level = data[0]
const desc = data[1]
if (level !== AlertLevel.WARNING && level !== AlertLevel.FATAL) {
throw new TLSError(DECODE_ERROR, 'bad alert level')
}
if (level === AlertLevel.FATAL || desc === CLOSE_NOTIFY) {
throw new TLSAlert(desc, level)
} else {
// TODO console.log(`tls server alert: ${alertDescription(desc)}`)
}
}
/**
* handle handshake message
* @param {Buffer} msg - full message data, including type, length, and body
*/
handleHandshakeMessage (msg) {
const {
HELLO_REQUEST,
CLIENT_HELLO,
SERVER_HELLO,
CERTIFICATE,
SERVER_KEY_EXCHANGE,
CERTIFICATE_REQUEST,
SERVER_HELLO_DONE,
CERTIFICATE_VERIFY,
CLIENT_KEY_EXCHANGE,
FINISHED
} = HandshakeType
const { UNEXPECTED_MESSAGE, DECODE_ERROR } = AlertDescription
const type = msg[0]
const data = msg.slice(4)
console.log(' -> ' + handshakeType(type))
switch (type) {
case HELLO_REQUEST: // TODO may reply no_renegotiation
return
case CLIENT_HELLO:
throw new TLSError(UNEXPECTED_MESSAGE, 'unexpected client hello')
case SERVER_HELLO:
this.assertLast('client', CLIENT_HELLO)
this.handleServerHello(data)
this.saveMessage('server', msg)
break
case CERTIFICATE:
this.assertLast('server', SERVER_HELLO)
this.handleCertificate(data)
this.saveMessage('server', msg)
break
case SERVER_KEY_EXCHANGE:
throw new TLSError(UNEXPECTED_MESSAGE, 'unexpected server key exchange')
case CERTIFICATE_REQUEST:
this.assertLast('server', CERTIFICATE)
this.handleCertificateRequest(data)
this.saveMessage('server', msg)
break
case SERVER_HELLO_DONE:
this.assertLast('server', CERTIFICATE_REQUEST)
this.handleServerHelloDone(data)
this.saveMessage('server', msg)
this.sendClientCertificate()
this.sendClientKeyExchange()
this.sign((err, sig) => {
try {
if (this.state === 'TERMINATED') return
if (err) throw err
this.assertLast('client', CLIENT_KEY_EXCHANGE)
this.sendCertificateVerify(sig)
this.changeCipherSpec()
this.sendFinished()
} catch (e) {
this.handleError(e)
}
})
break
case CERTIFICATE_VERIFY:
throw new TLSError(UNEXPECTED_MESSAGE, 'unexpected certificate verify')
case CLIENT_KEY_EXCHANGE:
throw new TLSError(UNEXPECTED_MESSAGE, 'unexpected client key exchange')
case FINISHED:
this.assertLast('client', FINISHED)
this.handleServerFinished(data)
this.saveMessage('server', msg)
break
default:
throw new TLSError(DECODE_ERROR, 'bad handshake message type')
}
}
/**
* ```
* struct {
* ProtocolVersion server_version;
* Random random;
* SessionID session_id;
* CipherSuite cipher_suite;
* CompressionMethod compression_method;
* select (extensions_present) {
* case false:
* struct {};
* case true:
* Extension extensions<0..2^16-1>;
* };
* } ServerHello;
* ```
*/
handleServerHello (data) {
const shift = size => K(data.slice(0, size))(data = data.slice(size))
const { ILLEGAL_PARAMETER } = AlertDescription
const ProtocolVersion = shift(2)
if (!ProtocolVersion.equals(VER12)) {
throw new TLSError(ILLEGAL_PARAMETER, 'unsupported tls version')
}
const Random = shift(32)
this.setServerRandom(Random)
const SessionId = shift(shift(1)[0])
this.sessionId = SessionId
const CipherSuite = shift(2)
if (!CipherSuite.equals(AES_128_CBC_SHA)) {
throw new TLSError(ILLEGAL_PARAMETER, 'unsupported cipher suite')
}
const CompressionMethod = shift(1)[0]
if (CompressionMethod !== 0) {
throw new TLSError(ILLEGAL_PARAMETER, 'compression not supported')
}
/**
TODO new class ?
console.log('ServerHello', {
ProtocolVersion: ProtocolVersion.toString('hex'),
Random,
SessionId,
CipherSuite: CipherSuite.toString('hex'),
CompressionMethod,
data
})
*/
}
/**
* extracts and verifies server certificates. If succeeded,
* extracts server public key for futher usage.
*
* ```
* struct {
* ASN.1Cert certificate_list<0..2^24-1>;
* } Certificate;
* ```
*/
handleCertificate (data) {
const shift = size => K(data.slice(0, size))(data = data.slice(size))
const {
DECODE_ERROR, BAD_CERTIFICATE,
UNSUPPORTED_CERTIFICATE, ILLEGAL_PARAMETER, CERTIFICATE_UNKNOWN,
UNKNOWN_CA
} = AlertDescription
if (data.length < 3 || readUInt24(shift(3)) !== data.length) {
throw new TLSError(DECODE_ERROR, 'invalid message length')
}
// certificates are in DER format and reversed order
// parse data to be an array of forge cert objects
const certs = []
while (data.length) {
if (data.length < 3 || readUInt24(data) + 3 > data.length) {
throw new TLSError(DECODE_ERROR, 'invalid cert length')
}
const der = shift(readUInt24(shift(3)))
let certAsn1, cert
try {
certAsn1 = asn1.fromDer(der.toString('binary'))
} catch (e) {
console.log('asn1.fromDer failed()', e)
throw new TLSError(BAD_CERTIFICATE,
'failed to parse certificate')
}
try {
cert = pki.certificateFromAsn1(certAsn1)
} catch (e) {
console.log('pki.certificateFromAsn1() failed', e)
throw new TLSError(UNSUPPORTED_CERTIFICATE,
'failed to construct forge certificate from given asn1 data')
}
certs.push(cert)
}
if (!certs.length) {
throw new TLSError(ILLEGAL_PARAMETER, 'no certificate')
}
const highest = certs.findIndex(cert => cert.isIssuer(this.ca))
if (highest !== -1) {
const chain = certs.slice(0, highest + 1)
// skip check date for client may have bad time
const opts = { validityCheckDate: null }
let verified = false
try {
verified = pki.verifyCertificateChain(this.caStore, chain, opts)
} catch (e) {
console.log('pki.verifyCertificateChain() failed', e)
throw new TLSError(CERTIFICATE_UNKNOWN,
'failed to verify certificate chain')
}
if (verified) {
this.serverPublicKey = pki.publicKeyToPem(chain[0].publicKey)
return
}
}
throw new TLSError(UNKNOWN_CA, 'server certificates untrusted')
}
/**
* ```
* struct {
* ClientCertificateType certificate_types<1..2^8-1>;
* SignatureAndHashAlgorithm
* supported_signature_algorithms<2^16-1>;
* DistinguishedName certificate_authorities<0..2^16-1>;
* } CertificateRequest;
* ```
*/
handleCertificateRequest (data) {
const shift = size => K(data.slice(0, size))(data = data.slice(size))
const { DECODE_ERROR } = AlertDescription
if (data.length < 1 || data[0] + 1 > data.length) {
throw new TLSError(DECODE_ERROR, 'invalid length')
}
this.certificateTypes = Array.from(shift(shift(1)[0]))
if (data.length < 2 || data.readUInt16BE() % 2 ||
data.readUInt16BE() + 2 > data.length) {
throw new TLSError(DECODE_ERROR, 'invalid length')
}
this.signatureAlgorithms = Array
.from(shift(shift(2).readUInt16BE()))
.reduce((acc, c, i, arr) =>
(i % 2) ? [...acc, arr[i - 1] * 256 + c] : acc, [])
// ignore distinguished names (DER), observed 00 00
// no idea what it looks like if non-null
try {
asn1.fromDer(data.toString('binary'))
} catch (e) {
console.log(e)
}
/**
console.log('CertificateRequest', {
ClientCertificateType: this.certificateTypes,
SignatureAndHashAlgorithm: this.signatureAlgorithms,
data
})
*/
}
/**
* struct { } ServerHelloDone;
*/
handleServerHelloDone (data) {
const { DECODE_ERROR } = AlertDescription
if (data.length) {
throw new TLSError(DECODE_ERROR, 'invalid ServerHelloDone')
}
}
/**
* checks `verify_data` in server Finished message, transits to
* Established state or throw error
*
* ```
* struct {
* opaque verify_data[verify_data_length];
* } Finished;
* ```
* @param {Buffer} data
*/
handleServerFinished (data) {
const verifyData = this.serverVerifyData()
if (!data.equals(verifyData)) {
throw new TLSError(AlertDescription.DECRYPT_ERROR,
'failed to verify server Finished')
}
process.nextTick(() => {
// set state
this.state = 'ESTABLISHED'
// TODO console.log('telsa entering Established state')
// enter Established state
this.socket.on('drain', () => {
if (this.writing) {
const callback = this.writing.callback
this.writing = null
callback()
}
})
if (this.writing) {
const { chunk, encoding, callback } = this.writing
this.writing = null
this._write(chunk, encoding, callback)
}
})
}
/**
* handle change cipher spec
*/
handleChangeCipherSpec (data) {
// TODO expect
// TODO validate
console.log(' -> ChangeCipherSpec', data)
// this.serverChangeCipherSpec(this.serverWriteKey, this.serverWriteMacKey)
this.decipher = createDecipher(this.serverWriteKey, this.serverWriteMacKey)
}
/**
* handle application data
*/
handleApplicationData (data) {
// TODO
this.push(data)
}
/**
* constructs a record layer packet and send
* @param {number} type - content type
* @param {Buffer} data - content
*/
send (type, data) {
if (this.cipher) data = this.cipher(type, data)
const record = concat([from([type]), VER12, prepend16(data)])
return this.socket.write(record)
}
/**
* @return {boolean} false if buffer full
*/
sendAlert (level, description) {
return this.send(ContentType.ALERT, from([level, description]))
}
/**
* @return {boolean} false if buffer full
*/
sendChangeCipherSpec () {
console.log('<- Change Cipher Spec')
return this.send(ContentType.CHANGE_CIPHER_SPEC, from([1]))
}
/**
* @return {boolean} false if buffer full
*/
sendHandshakeMessage (type, data) {
console.log('<- ' + handshakeType(type))
data = concat([from([type]), prepend24(data)])
this.saveMessage('client', data)
return this.send(ContentType.HANDSHAKE, data)
}
/**
* send ClientHello handshake message
*/
sendClientHello () {
this.sendHandshakeMessage(HandshakeType.CLIENT_HELLO, concat([
VER12,
this.clientRandom,
from([0]), // session_id
from([0x00, 0x02, 0x00, 0x2f]), // cipher_suites
from([0x01, 0x00]) // compression_methods
]))
}
/**
* send client certificate if ServerHelloDone and
* server public key available (which also means server certificates
* verified)
*/
sendClientCertificate () {
this.sendHandshakeMessage(HandshakeType.CERTIFICATE,
prepend24(concat([
...this.opts.clientCertificates.map(c => prepend24(c))])))
}
/**
* send ClientKeyExchange message, preMasterSecret is encrypted
* using server's public key
*/
sendClientKeyExchange () {
this.sendHandshakeMessage(HandshakeType.CLIENT_KEY_EXCHANGE,
prepend16(publicEncrypt({
key: this.serverPublicKey,
padding: RSA_PKCS1_PADDING
}, this.preMasterSecret)))
}
/**
* sign all handshake messages sent and received so far
*/
sign (callback) {
const key = this.opts.clientPrivateKey
const tbs = concat(this.msgs)
if (typeof key === 'function') {
key(tbs, callback)
} else {
const sig = createSign('sha256').update(tbs).sign(key)
process.nextTick(() => callback(null, sig))
}
}
/**
* send CertificateVerify
*/
sendCertificateVerify (sig) {
this.sendHandshakeMessage(HandshakeType.CERTIFICATE_VERIFY,
concat([RSA_PKCS1_SHA256, prepend16(sig)]))
}
/**
* send Finished handshake message
*/
sendFinished () {
this.sendHandshakeMessage(HandshakeType.FINISHED,
this.clientVerifyData())
}
/**
* @return {boolean} false if buffer full
*/
sendApplicationData (data, callback) {
return this.send(ContentType.APPLICATION_DATA, data)
}
/**
* implements `Duplex` `_write`
*/
_write (chunk, encoding, callback) {
switch (this.state) {
// fallthrough
case 'CONNECTING':
case 'HANDSHAKING':
this.writing = { chunk, encoding, callback }
break
case 'ESTABLISHED':
if (this.sendApplicationData(chunk)) {
callback()
} else {
this.writing = { callback }
}
break
case 'TERMINATED':
callback(new Error('This socket has been terminated'))
break
default:
break
}
}
/** implement Duplex _final */
_final (callback) {
callback()
this.terminate('final')
}
_destroy (err, callback) {
callback(err)
this.terminate('destroy')
}
/** implement Duplex _read */
_read (size) {
if (this.state === 'ESTABLISHED') this.socket.resume()
}
/**
* terminate is the one-for-all method to end the telsa.
* unlike node tls, telsa terminates synchronously, which means
* that there is no closing state. This is allowed in TLS spec.
*
* - final
* - destroy
* - socket, [err]
* - error, TLSError | Error
* - alert, TLSAlert
* - (close_notify) redefined from alert
*/
terminate (reason, err) {
console.log(' terminate', this.state, reason, err && err.message)
const {
CLOSE_NOTIFY, USER_CANCELED,
INTERNAL_ERROR
} = AlertDescription
// redefine close_notify
if (reason === 'alert' && err.description === CLOSE_NOTIFY) {
reason = 'close_notify'
err = null
}
// send alert if socket available
try {
if ((reason === 'final' || reason === 'destroy') &&
this.state === 'HANDSHAKING') {
this.sendAlert(AlertLevel.WARNING, USER_CANCELED)
}
if (reason === 'final' ||
reason === 'destroy' ||
reason === 'close_notify') {
this.sendAlert(AlertLevel.WARNING, CLOSE_NOTIFY)
}
if (reason === 'error') {
if (err instanceof TLSError) {
this.sendAlert(AlertLevel.FATAL, err.description)
} else {
this.sendAlert(AlertLevel.FATAL, INTERNAL_ERROR)
}
}
} catch (e) { }
// clean socket
this.socket.removeAllListeners()
this.socket.on('error', () => {})
if (reason === 'destroy') {
this.socket.destroy()
} else {
this.socket.end()
}
// end
if (reason !== 'destroy') this.push(null)
let callback
if (this.writing) {
callback = this.writing.callback
this.writing = null
}
// socket close always an error TODO
if (reason === 'socket') err = err || new Error('premature close')
// close_notify cause error if
// 1. handshaking
// 2. draining write
if (reason === 'close_notify') {
if (this.state === 'HANDSHAKING') {
err = new Error('server close')
} else if (this.state === 'ESTABLISHED' && callback) {
err = new Error('socket has been ended by the other party')
err.code = 'EPIPE'
}
}
// over-simplified TODO
if (err && !err.code) {
if (this.state === 'HANDSHAKING') {
err.code = 'ERR_TLS_HANDSHAKE_FAILED'
} else if (this.state === 'ESTABLISHED') {
err.code = 'ERR_TLS_CONNECTION_FAILED'
}
}
if (err) {
if (callback) {
callback(err)
} else {
this.emit('error', err)
}
}
// duplex will emit close on its own for destroy
if (reason !== 'destroy') {
// read path `end` is emitted in nextTick()
// emitting `close` in nextTick guarantee it is after `end`
process.nextTick(() => this.emit('close'))
}
}
}
module.exports = Telsa