Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce device id v2 #362

Merged
merged 5 commits into from Dec 23, 2019
Merged
Changes from all commits
Commits
File filter...
Filter file types
Jump to…
Jump to file
Failed to load files.

Always

Just for now

@@ -34,12 +34,17 @@ const messages = {
* saved. 'config' contains apiVersion, serverUrl, debug;
* see server/config/default.json
*/
GOT_INIT_DATA: _, /* @param {Array} seed, @param {Array} deviceId, @param {Object} config */
GOT_INIT_DATA: _, /* @param {Array} seed, @param {Array} deviceId,
* @param {Object} config, @param {string} deviceIdV2
* */
/**
* webview -> browser
* browser must save values in persistent storage if non-empty
* It will be called during initial setup or deviceIdV2 migration
*/
SAVE_INIT_DATA: _, /* @param {Uint8Array} seed, @param {Uint8Array} deviceId */
SAVE_INIT_DATA: _, /* @param {Uint8Array} seed, @param {Uint8Array} deviceId,
* @param {string} deviceIdV2
*/
/**
* webview -> browser
* sent when sync has finished initialization
@@ -1,6 +1,7 @@
'use strict'

const crypto = require('brave-crypto')
const {generateDeviceIdV2} = require('../lib/crypto')
const messages = require('./constants/messages')
const {syncVersion} = require('./config')

@@ -12,23 +13,25 @@ module.exports.init = function () {
return new Promise((resolve, reject) => {
const ipc = window.chrome.ipcRenderer
ipc.send(messages.GET_INIT_DATA, syncVersion)
ipc.once(messages.GOT_INIT_DATA, (e, seed, deviceId, config) => {
ipc.once(messages.GOT_INIT_DATA, (e, seed, deviceId, config, deviceIdV2) => {
if (seed === null) {
// Generate a new "persona"
seed = crypto.getSeed() // 32 bytes
deviceId = new Uint8Array([0])
ipc.send(messages.SAVE_INIT_DATA, seed, deviceId)
deviceIdV2 = generateDeviceIdV2()
ipc.send(messages.SAVE_INIT_DATA, seed, deviceId, deviceIdV2)
// TODO: The background process should listen for SAVE_INIT_DATA and emit
// GOT_INIT_DATA once the save is successful
}

// XXX: workaround #17
seed = seed instanceof Array ? new Uint8Array(seed) : seed
deviceId = deviceId instanceof Array ? new Uint8Array(deviceId) : deviceId
if (!(seed instanceof Uint8Array) || seed.length !== crypto.DEFAULT_SEED_SIZE) {
reject(new Error('Invalid crypto seed'))
return
}
resolve({seed, deviceId, config})
resolve({seed, deviceId, config, deviceIdV2})
})
})
}
@@ -2,6 +2,7 @@

const awsSdk = require('aws-sdk')
const cryptoUtil = require('./cryptoUtil')
const deepEqual = require('deep-equal')
const recordUtil = require('./recordUtil')
const proto = require('./constants/proto')
const {limitConcurrency} = require('../lib/promiseHelper')
@@ -30,6 +31,26 @@ const isExpiredCredentialError = (error) => {
})
}

const createAndSubscribeSQSforCategory = function (deviceId, category, thisRef) {
let newQueueParams = {
QueueName: thisRef.sqsName(deviceId, category),
Attributes: {
'MessageRetentionPeriod': s3Helper.SQS_RETENTION
}
}
return new Promise((resolve, reject) => {
thisRef.sqs.createQueue(newQueueParams, (error, data) => {
if (error) {
console.log('SQS creation failed with error: ' + error)
reject(error)
} else if (data) {
thisRef.SQSUrlByCat[category] = data.QueueUrl
resolve([])
}
})
})
}

/**
* @param {{
* apiVersion: <string>,
@@ -64,6 +85,7 @@ const RequestUtil = function (opts = {}) {
this.saveAWSCredentials(credentials)
}
this.SQSUrlByCat = []
this.oldSQSUrlByCat = []
this.missingObjectsCache = new LRUCache(50)
// This is used to keep the most recent records for each object id
this.latestRecordsCache = new LRUCache(100)
@@ -249,8 +271,30 @@ RequestUtil.prototype.list = function (category, startAt, maxRecords, nextContin
WaitTimeSeconds: CONFIG.SQS_MESSAGES_LONGPOLL_TIMEOUT
}

return s3Helper.listNotifications(this.sqs, notificationParams, category,
prefix)
// We will fetch from both old and new SQS queues until old one gets retired
if (this.oldSQSUrlByCat[category]) {
let oldNotificationParams = Object.assign({}, notificationParams)
oldNotificationParams.QueueUrl = `${this.oldSQSUrlByCat[category]}`

return s3Helper.listNotifications(
this.sqs, notificationParams, category, prefix).then((values) => {
if (this.shouldRetireOldSQSQueue(parseInt(values.createdTimeStamp))) {
return this.deleteSQSQueue(this.oldSQSUrlByCat[category]).then(() => {

This comment has been minimized.

Copy link
@darkdh

darkdh Dec 12, 2019

Author Member

@AlexeyBarabash reminded me that when device id is duplicated, if there is an old Brave which doesn't contain the fix, its SQS queue will be unavailable until next relaunch because upgraded Brave deletes it after 24 hours

This comment has been minimized.

Copy link
@AlexeyBarabash

AlexeyBarabash Dec 12, 2019

Contributor

I was confused and I thought this.deleteSQSQueue can delete queues belonging to other devices. And like @darkdh mentioned that only can happen if device id was duplicated. Which is a separate case.

This comment has been minimized.

Copy link
@darkdh

darkdh Dec 13, 2019

Author Member

Non upgraded device will get reset anyway, so duplicate case won’t be an issue

delete this.oldSQSUrlByCat[category]
return values
})
}
return s3Helper.listNotifications(
this.sqs, oldNotificationParams, category, prefix).then((oldValues) => {
if (deepEqual(values.contents, oldValues.contents, {strict: true})) {
return values
}
return {contents: values.contents.concat(oldValues.contents),
createdTimeStamp: values.createdTimeStamp}
})
})
}
return s3Helper.listNotifications(this.sqs, notificationParams, category, prefix)
})
}

@@ -275,6 +319,21 @@ RequestUtil.prototype.shouldListObject = function (startAt, category) {
this.listInProgress === true
}

/**
* The retention time of our SQS queue is 24 hours so we will retire old SQS
* queue created by device id after 24 hours
* @param {number=} createdTimestamp is the when new device id v2 queue was
* created
* @returns {boolean}
*/
RequestUtil.prototype.shouldRetireOldSQSQueue = function (createdTimestamp) {
let currentTime = new Date().getTime()
let newQueueCreatedTime =
this.normalizeTimestampToMs(createdTimestamp, currentTime)

return (currentTime - newQueueCreatedTime) > parseInt(s3Helper.SQS_RETENTION, 10) * 1000
}

/**
* Checks do we need to use s3 list Object or SQS notifications
* @param {number=} startAt return objects with timestamp >= startAt (e.g. 1482435340). Could be seconds or milliseconds
@@ -339,40 +398,57 @@ RequestUtil.prototype.sqsName = function (deviceId, category) {
return queueName
}

/**
* Main purpose of this function is to create old SQS queue to test device id v2
* migration
* @param {string} deviceId
* @returns {Promise}
*/
RequestUtil.prototype.createAndSubscribeSQSForTest = function (deviceId) {
var createSQSPromises = []
// Simple for loop instead foreach to capture 'this'
for (var i = 0; i < CATEGORIES_FOR_SQS.length; ++i) {
createSQSPromises.push(createAndSubscribeSQSforCategory(deviceId, CATEGORIES_FOR_SQS[i], this))
}

return Promise.all(createSQSPromises)
}

/**
* Creates SQS for the current device.
* @param {string} deviceId
* @param {string} deviceIdV2
* @returns {Promise}
*/
RequestUtil.prototype.createAndSubscribeSQS = function (deviceId) {
RequestUtil.prototype.createAndSubscribeSQS = function (deviceId, deviceIdV2) {
// Creating a query for the current userId
if (!deviceId) {
throw new Error('createSQS failed. deviceId is null!')
if (!deviceIdV2) {
throw new Error('createSQS failed. deviceIdV2 is null!')
}
this.deviceId = deviceId
const createAndSubscribeSQSforCategory = function (deviceId, category, thisRef) {
let newQueueParams = {
QueueName: thisRef.sqsName(deviceId, category),
Attributes: {
'MessageRetentionPeriod': s3Helper.SQS_RETENTION
}
}
this.deviceIdV2 = deviceIdV2

const subscribeOldSQSforCategory = function (deviceId, category, thisRef) {
return new Promise((resolve, reject) => {
thisRef.sqs.createQueue(newQueueParams, (error, data) => {
let params = {
QueueName: thisRef.sqsName(deviceId, category)
}
thisRef.sqs.getQueueUrl(params, (error, data) => {
if (error) {
console.log('SQS creation failed with error: ' + error)
reject(error)
// queue doesn't exist
resolve()
} else if (data) {
thisRef.SQSUrlByCat[category] = data.QueueUrl
resolve([])
thisRef.oldSQSUrlByCat[category] = data.QueueUrl
resolve()
}
})
})
}
var createSQSPromises = []
// Simple for loop instead foreach to capture 'this'
for (var i = 0; i < CATEGORIES_FOR_SQS.length; ++i) {
createSQSPromises.push(createAndSubscribeSQSforCategory(deviceId, CATEGORIES_FOR_SQS[i], this))
// Doesn't have to create about to deprecated queues
createSQSPromises.push(subscribeOldSQSforCategory(deviceId, CATEGORIES_FOR_SQS[i], this))
createSQSPromises.push(createAndSubscribeSQSforCategory(deviceIdV2, CATEGORIES_FOR_SQS[i], this))
}

return Promise.all(createSQSPromises)
@@ -530,7 +606,7 @@ RequestUtil.prototype.deleteUser = function () {
RequestUtil.prototype.purgeUserCategoryQueue = function (category) {
return new Promise((resolve, reject) => {
let params = {
QueueName: this.sqsName(this.deviceId, category)
QueueName: this.sqsName(this.deviceIdV2, category)
}
this.sqs.getQueueUrl(params, (error, data) => {
if (error) {
@@ -553,6 +629,24 @@ RequestUtil.prototype.purgeUserCategoryQueue = function (category) {
})
}

/**
* Delete SQS queue by url
* @param {string} url - SQS queue url
*/
RequestUtil.prototype.deleteSQSQueue = function (url) {
return new Promise((resolve, reject) => {
let params = {
QueueUrl: url
}
this.sqs.deleteQueue(params, (err, data) => {
if (err) {
console.log('SQS deleteQueue failed with error: ' + err)
}
resolve([])
})
})
}

RequestUtil.prototype.purgeUserQueue = function () {
var purgeQueuePromises = []
for (var i = 0; i < CATEGORIES_FOR_SQS.length; ++i) {
@@ -7,7 +7,7 @@ const recordUtil = require('./recordUtil')
const messages = require('./constants/messages')
const proto = require('./constants/proto')
const serializer = require('../lib/serializer')
const {deriveKeys} = require('../lib/crypto')
const {deriveKeys, generateDeviceIdV2} = require('../lib/crypto')

let ipc = window.chrome.ipcRenderer

@@ -18,6 +18,7 @@ const ERROR = 2
const logElement = document.querySelector('#output')

var clientDeviceId = null
var clientDeviceIdV2 = null
var clientUserId = null
var clientKeys = {}
var config = {}
@@ -52,6 +53,10 @@ const logSync = (message, logLevel = DEBUG) => {
*/
const maybeSetDeviceId = (requester) => {
if (clientDeviceId !== null) {
if (clientDeviceIdV2.length === 0) {
clientDeviceIdV2 = generateDeviceIdV2()
ipc.send(messages.SAVE_INIT_DATA, seed, clientDeviceId, clientDeviceIdV2)
}
return Promise.resolve(requester)
}
if (!requester || !requester.s3) {
@@ -71,7 +76,8 @@ const maybeSetDeviceId = (requester) => {
})
}
clientDeviceId = new Uint8Array([maxId + 1])
ipc.send(messages.SAVE_INIT_DATA, seed, clientDeviceId)
clientDeviceIdV2 = generateDeviceIdV2()
ipc.send(messages.SAVE_INIT_DATA, seed, clientDeviceId, clientDeviceIdV2)
return Promise.resolve(requester)
})
}
@@ -252,6 +258,7 @@ const main = () => {
const clientSerializer = values[0]
const keys = deriveKeys(values[1].seed)
const deviceId = values[1].deviceId
clientDeviceIdV2 = values[1].deviceIdV2
seed = values[1].seed
clientKeys = keys
config = values[1].config
@@ -288,8 +295,8 @@ const main = () => {
})
.then((requester) => {
if (clientDeviceId !== null && requester && requester.s3) {
logSync('set device ID: ' + clientDeviceId)
requester.createAndSubscribeSQS(clientDeviceId).then(() => {
logSync('set device ID: ' + clientDeviceId + ' device ID V2: ' + clientDeviceIdV2)
requester.createAndSubscribeSQS(clientDeviceId, clientDeviceIdV2).then(() => {
startSync(requester)
})
.catch((e) => {
@@ -82,6 +82,7 @@ message SyncRecord {
}
message Device {
string name = 1;
string deviceIdV2 = 2;
}
Action action = 1;
bytes deviceId = 2;
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.