From 6ac1577323ea485f84f54bc81e4733596722e429 Mon Sep 17 00:00:00 2001 From: madshall Date: Fri, 8 May 2020 12:22:50 -0400 Subject: [PATCH 1/2] feat: Add 2FA flow handlers --- README.md | 12 ++++++- lib/blink.js | 91 ++++++++++++++++++++++++++++++++++-------------- lib/constants.js | 8 +++-- lib/util.js | 15 ++++++++ package.json | 2 +- 5 files changed, 97 insertions(+), 31 deletions(-) create mode 100644 lib/util.js diff --git a/README.md b/README.md index 58b81fd..89200c8 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,11 @@ npm install node-blink-security ``` # Usage + ```javascript const Blink = require('node-blink-security'); -var blink = new Blink('YOUR_EMAIL', 'YOUR_PASSWORD'); +var blink = new Blink('YOUR_EMAIL', 'YOUR_PASSWORD', 'DEVICE_ID'); blink.setupSystem() .then(() => { blink.setArmed() @@ -37,6 +38,15 @@ blink.setupSystem() class Blink ``` +## Constructor +* `email` - your Blink account email +* `password` - your Blink account password +* `deviceId` - identifies your device and registers it in your account. It's required since version 4.0.0 of this package as this is when Blink switched to 2-factor authentication flow +* `options` +* * `auth_2FA: false` - set to `true` if you want to receive verification code for each login, otherwise you'll receive verification email only once for the first time and after that the device will be remembered by Blink. +* * `verification_timeout: 60000` - number of milliseconds to wait for email verification until retrying account login +* * `device_name: "node-blink-security"` - this name appears in verification email along with your `deviceId` + ## Properties * `blink.cameras` - the information about all available cameras diff --git a/lib/blink.js b/lib/blink.js index 63b3c7e..df8d519 100644 --- a/lib/blink.js +++ b/lib/blink.js @@ -1,20 +1,26 @@ /** * Created by madshall on 3/17/17. */ - +const readline = require('readline'); require('./constants'); const BlinkCamera = require('./blink_camera'); const BlinkException = require('./blink_exception'); const BlinkAuthenticationException = require('./blink_auth_exception'); const BlinkURLHandler = require('./blink_url_handler'); +const { guid } = require('./util'); const request = require('request'); +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + function _statusCodeIsError(response){ return response.statusCode < 200 || response.statusCode > 299 } module.exports = class Blink { - constructor(username, password, options) { + constructor(username, password, device_id, options) { this._username = username; this._password = password; this._token = null; @@ -27,8 +33,12 @@ module.exports = class Blink { this._events = []; this._cameras = {}; this._idlookup = {}; + this._device_id = device_id; + this.auth_2FA = false; + this.verification_timeout = 1e3 * 60; + this.device_name = "node-blink-security"; this.urls = null; - Object.assign(this, options) + Object.assign(this, options); } get cameras() { @@ -338,23 +348,35 @@ module.exports = class Blink { }; this.urls = new BlinkURLHandler(this._region_id); } - _getAuthToken() { + _getAuthToken(repeats = 0) { return new Promise((resolve, reject) => { if (typeof this._username != 'string') { - reject(new BlinkAuthenticationException("Username must be a string")); + return reject(new BlinkAuthenticationException("Username must be a string")); } if (typeof this._password != 'string') { - reject(new BlinkAuthenticationException("Password must be a string")); + return reject(new BlinkAuthenticationException("Password must be a string")); + } + if (typeof this._device_id != 'string') { + return reject(new BlinkAuthenticationException("Device ID must be a string")); } let headers = { 'Host': DEFAULT_URL, 'Content-Type': 'application/json' }; + const notification_key = guid(SIZE_NOTIFICATION_KEY); let data = { "email": this._username, "password": this._password, - "client_specifier": "iPhone 9.2 | 2.2 | 222" + "notification_key": notification_key, + "unique_id": this._device_id, + "app_version": "6.0.7 (520300) #afb0be72a", + "client_name": this.device_name, + "client_type": "android", + "device_identifier": this._device_id, + "device_name": this.device_name, + "os_version": "5.1.1", + "reauth": "true", }; let authenticate = (response) => { @@ -371,30 +393,47 @@ module.exports = class Blink { }; request.post({ - url: LOGIN_URL, + url: this.auth_2FA ? LOGIN_URL_2FA : LOGIN_URL, json: true, headers: headers, body: data }, (err, response, body) => { - if (err || _statusCodeIsError(response) ) { - request.post({ - url: LOGIN_BACKUP_URL, - json: true, - headers: headers, - body: data - }, (err, response, body) => { - if (err || _statusCodeIsError(response) ) { - reject(new BlinkAuthenticationException("Authentication problem: second attempt")); - } else { - if (body.message) { - return reject(new BlinkAuthenticationException(body.message)); - } - this._region_id = 'rest.piri'; - this._region = 'UNKNOWN'; - authenticate(body); - } - }) + if (err || _statusCodeIsError(response)) { + return reject(new BlinkAuthenticationException(`Authentication problem: ${body.message}`)); } else { + if ((body.client || {}).verification_required) { + if (!this.auth_2FA) { + if (repeats === 1) return reject(new BlinkAuthenticationException(`Authentication problem: verification timeout`)); + return new Promise((resolve, reject) => { + setTimeout(() => { + this._getAuthToken(repeats + 1).then(resolve, reject); + }, this.verification_timeout); + }).then(resolve, reject); + } + return rl.question(`Enter the verification code sent to ${this._username}: `, pin => { + request.post({ + url: `${BASE_URL}/api/v4/account/${body.account.id}/client/${body.client.id}/pin/verify`, + json: true, + headers: headers, + body: { + pin: `${pin}`, + } + }, (err, response, body) => { + if (err || _statusCodeIsError(response)) { + return reject(new BlinkAuthenticationException(`Authentication problem: ${body.message}`)); + } + if (!body.region) { + return reject(new BlinkAuthenticationException(`Authentication problem: ${body.message}`)); + } + for (var key in body.region) { + this._region_id = key; + this._region = body.region[key]; + } + authenticate(body); + }); + rl.close(); + }); + } if (!body.region) { return reject(new BlinkAuthenticationException(body.message)); } diff --git a/lib/constants.js b/lib/constants.js index 21ad29a..e377920 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -3,7 +3,9 @@ */ global.BLINK_URL = 'immedia-semi.com'; -global.LOGIN_URL = 'https://rest.prod.' + BLINK_URL + '/login'; -global.LOGIN_BACKUP_URL = 'https://rest.piri/' + BLINK_URL + '/login'; +global.LOGIN_URL = 'https://rest.prod.' + BLINK_URL + '/api/v3/login'; +global.LOGIN_URL_2FA = 'https://rest.prod.' + BLINK_URL + '/api/v4/account/login'; global.BASE_URL = 'https://rest.prod.' + BLINK_URL; -global.DEFAULT_URL = 'prod.' + BLINK_URL; \ No newline at end of file +global.DEFAULT_URL = 'prod.' + BLINK_URL; +global.SIZE_NOTIFICATION_KEY = 152; +global.SIZE_UID = 16; diff --git a/lib/util.js b/lib/util.js new file mode 100644 index 0000000..45af97f --- /dev/null +++ b/lib/util.js @@ -0,0 +1,15 @@ +const guid = (len = 32) => { + let buf = [], + chars = 'abcdef0123456789', + charlen = chars.length; + + for (var i = 0; i < len; i++) { + buf[i] = chars.charAt(Math.floor(Math.random() * charlen)); + } + + return buf.join(''); +} + +module.exports = { + guid, +}; \ No newline at end of file diff --git a/package.json b/package.json index ee4dd8a..d7e70cd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-blink-security", - "version": "3.0.6", + "version": "4.0.0", "description": "A NodeJS module for the Blink Security Camera System", "main": "index.js", "scripts": { From fd9333f81147c49d4202dc4089d3e17849519bf6 Mon Sep 17 00:00:00 2001 From: madshall Date: Sat, 9 May 2020 18:32:50 -0400 Subject: [PATCH 2/2] chore: Update README with details around deviceId --- README.md | 2 +- lib/blink.js | 53 +++++++++++++++++++++++----------------------------- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 89200c8..0a9e1c5 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ class Blink ## Constructor * `email` - your Blink account email * `password` - your Blink account password -* `deviceId` - identifies your device and registers it in your account. It's required since version 4.0.0 of this package as this is when Blink switched to 2-factor authentication flow +* `deviceId` - identifies your device and registers it in your account. It's required since version 4.0.0 of this package as this is when Blink switched to 2-factor authentication flow. The value is provided by you and it should let you identify the device correctly when you receive a verification email from Blink. * `options` * * `auth_2FA: false` - set to `true` if you want to receive verification code for each login, otherwise you'll receive verification email only once for the first time and after that the device will be remembered by Blink. * * `verification_timeout: 60000` - number of milliseconds to wait for email verification until retrying account login diff --git a/lib/blink.js b/lib/blink.js index df8d519..91f7ac9 100644 --- a/lib/blink.js +++ b/lib/blink.js @@ -98,7 +98,7 @@ module.exports = class Blink { networks.forEach(networkId => { promises.push(new Promise((resolve, reject) => { if (!this._auth_header) { - reject(new BlinkException("Authentication token must be set")); + return reject(new BlinkException("Authentication token must be set")); } request({ @@ -107,10 +107,9 @@ module.exports = class Blink { json: true }, (err, response, body) => { if (err || _statusCodeIsError(response)) { - reject(new BlinkException(`Can't retrieve system summary`)); - } else { - resolve(body); - } + return reject(new BlinkException(`Can't retrieve system summary`)); + } + return resolve(body); }); })); }); @@ -121,7 +120,6 @@ module.exports = class Blink { acc[networks[index]] = result; return acc; }, {}); - }); } @@ -150,11 +148,10 @@ module.exports = class Blink { json: true }, (err, response, body) => { if (err || _statusCodeIsError(response)) { - reject(new BlinkException(`Can't retrieve system events`)); - } else { - this._events = body.event; - resolve(this._events); - } + return reject(new BlinkException(`Can't retrieve system events`)); + } + this._events = body.event; + return resolve(this._events); }); })); }); @@ -180,10 +177,9 @@ module.exports = class Blink { json: true }, (err, response, body) => { if (err || _statusCodeIsError(response)) { - reject(new BlinkException(`Can't retrieve system status`)); - } else { - resolve(body.syncmodule.status === 'online'); - } + return reject(new BlinkException(`Can't retrieve system status`)); + } + return resolve(body.syncmodule.status === 'online'); }); })); }); @@ -205,10 +201,9 @@ module.exports = class Blink { json: true }, (err, response, body) => { if (err || _statusCodeIsError(response)) { - reject(new BlinkException(`Can't retrieve connected clients`)); - } else { - resolve(body); + return reject(new BlinkException(`Can't retrieve connected clients`)); } + return resolve(body); }); }); } @@ -258,10 +253,9 @@ module.exports = class Blink { body: {} }, (err, response, body) => { if (err || _statusCodeIsError(response)) { - reject(new BlinkException(`Can't ${state} the network: ${networkId}`)); - } else { - resolve(body); + return reject(new BlinkException(`Can't ${state} the network: ${networkId}`)); } + return resolve(body); }); })); }); @@ -283,10 +277,9 @@ module.exports = class Blink { json: true }, (err, response, body) => { if (err || _statusCodeIsError(response)) { - reject(new BlinkException(`Can't fetch videos`)); - } else { - resolve(body); - } + return reject(new BlinkException(`Can't fetch videos`)); + } + return resolve(body); }); }); } @@ -451,7 +444,7 @@ module.exports = class Blink { var that = this; return new Promise((resolve, reject) => { if (!this._auth_header) { - reject(new BlinkException("You have to be authenticated before calling this method")); + return reject(new BlinkException("You have to be authenticated before calling this method")); } request({ url: this.urls.networks_url, @@ -459,7 +452,7 @@ module.exports = class Blink { json: true }, (err, response, body) => { if (err || _statusCodeIsError(response)) { - reject(new BlinkException(`Can't retrieve system status`)); + return reject(new BlinkException(`Can't retrieve system status`)); } else { var network = false; if (typeof name_or_id != 'undefined') { @@ -471,11 +464,11 @@ module.exports = class Blink { }); if (!network) { - reject(new BlinkException("No network found for " + name_or_id)); + return reject(new BlinkException("No network found for " + name_or_id)); } } else { if (!body.networks.length) { - reject(new BlinkException("No networks found")); + return reject(new BlinkException("No networks found")); } body.networks.forEach(network => { that._networks.push(network); @@ -484,7 +477,7 @@ module.exports = class Blink { that._account_id = that._networks[0].account_id; that.urls = new BlinkURLHandler(that.regionId); - resolve(that); + return resolve(that); } }); });