Skip to content

Commit

Permalink
Update merge vault logic
Browse files Browse the repository at this point in the history
  • Loading branch information
alchaplinsky committed Apr 4, 2020
1 parent 3063df1 commit 7c10cab
Show file tree
Hide file tree
Showing 17 changed files with 806 additions and 97 deletions.
3 changes: 1 addition & 2 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly",
"CONFIG": "readonly",
"mockDate": "readonly"
"CONFIG": "readonly"
},
"parserOptions": {
"ecmaFeatures": {
Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
"dependencies": {
"@swiftyapp/cryptor": "^1.0.4",
"classnames": "^2.2.6",
"dotenv-webpack": "^1.7.0",
"electron": "^8.2.0",
"electron-log": "^4.1.1",
"electron-unhandled": "^3.0.2",
"electron-util": "^0.14.0",
Expand All @@ -30,10 +28,10 @@
},
"scripts": {
"lint": "npx eslint --color src",
"start": "NODE_ENV=development bozon start",
"start": "NODE_ENV=development bozon start",
"test": "yarn lint && yarn test:unit && yarn test:features",
"test:unit": "NODE_ENV=test npx jest test/units",
"test:features": "NODE_ENV=test bozon test test/features"
"test:features": "NODE_ENV=test ../../Railsware/bozon/bin/bozon test test/features"
},
"author": {
"name": "Alex Chaplinsky",
Expand All @@ -46,10 +44,12 @@
"@babel/preset-react": "^7.9.4",
"babel-jest": "^25.2.4",
"babel-loader": "^8.1.0",
"bozon": "^1.0.0-alpha.6",
"bozon": "^1.0.0-alpha.7",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"css-loader": "^3.4.2",
"dotenv-webpack": "^1.7.0",
"electron": "^8.2.0",
"eslint": "^6.8.0",
"eslint-plugin-jest": "^23.8.2",
"eslint-plugin-jsx-a11y": "^6.2.3",
Expand Down
5 changes: 4 additions & 1 deletion src/__mocks__/main/application/vault.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { DateTime } from 'luxon'

const Vault = jest.fn(() => {
return {
isDecryptable: jest.fn().mockReturnValue(true),
read: jest.fn().mockReturnValue({
entries: [{ id: '2', password: 'qwerty' }]
entries: [{ id: '2', password: 'qwerty' }],
updatedAt: DateTime.local().toISO()
}),
write: jest.fn()
}
Expand Down
4 changes: 3 additions & 1 deletion src/main/application/auditor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ export default class Auditor {
}

isOld(item) {
const time = DateTime.fromISO(item.password_updated_at || item.updated_at)
const time = DateTime.fromISO(
item.password_updated_at || item.updatedAt || item.updated_at
)
return Math.abs(time.diffNow('days').days) > PASSWORD_FRESHNESS
}
}
49 changes: 49 additions & 0 deletions src/main/application/sync/base/merge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { merge, keyBy } from 'lodash'
import { DateTime } from 'luxon'
import { encrypt, decrypt } from 'application/helpers/encryption'

export const mergeData = (local, remote, cryptor) => {
if (!remote) {
local.updatedAt = now()
return local
}
return encrypt(
{
entries: combine(group(local, cryptor), group(remote, cryptor)),
updatedAt: now()
},
cryptor
)
}

const combine = (local, remote) => {
return remote.timestamp > local.timestamp
? mergeInto(local, remote)
: mergeInto(remote, local)
}

const mergeInto = (base, update) => {
for (let key in base.data) {
if (update.data[key]) {
merge(base.data[key], update.data[key])
delete update.data[key]
} else {
delete base.data[key]
}
}

for (let key in update.data) {
base.data[key] = update.data[key]
}
return Object.values(base.data)
}

const group = (encrypted, cryptor) => {
const data = decrypt(encrypted, cryptor)
return {
data: keyBy(data.entries, 'id'),
timestamp: DateTime.fromISO(data.updatedAt)
}
}

const now = () => DateTime.local().toISO()
6 changes: 5 additions & 1 deletion src/main/application/sync/gdrive/__mocks__/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DateTime } from 'luxon'
const isConfigured = jest.fn().mockReturnValue(true)

const setup = jest.fn(() => {
Expand All @@ -10,7 +11,10 @@ const push = jest.fn(() => {
return Promise.resolve()
})
const pull = jest.fn(() => {
return Promise.resolve({ entries: [{ id: '1', password: 'password' }] })
return Promise.resolve({
entries: [{ id: '1', password: 'password' }],
updatedAt: DateTime.local().toISO()
})
})

module.exports = jest.fn(() => {
Expand Down
34 changes: 5 additions & 29 deletions src/main/application/sync/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { merge, groupBy } from 'lodash'
import { encrypt, decrypt } from 'application/helpers/encryption'
import GDrive from './gdrive'
import { mergeData } from './base/merge'

export default class Sync {
initialize(cryptor, vault) {
Expand All @@ -18,7 +17,7 @@ export default class Sync {
return this.provider
.pull()
.then(data => {
this._mergeAndPush(data)
this.mergeAndPush(data)
})
.catch(() => this.provider.push(this.vault.read()))
})
Expand All @@ -33,39 +32,16 @@ export default class Sync {
}

perform() {
return this.provider.pull().then(data => this._mergeAndPush(data))
return this.provider.pull().then(data => this.mergeAndPush(data))
}

async _mergeAndPush(data) {
async mergeAndPush(data) {
if (this.vault.isDecryptable(data, this.cryptor)) {
const merged = this._merge(this.vault.read(), data, this.cryptor)
const merged = mergeData(this.vault.read(), data, this.cryptor)
this.vault.write(merged)
await this.provider.push(merged)
return merged
}
throw Error('Remote vault file is invalid')
}

_merge(localData, remoteData, cryptor) {
if (!remoteData) return localData
return encrypt(
{
entries: this._mergeArrays(
decrypt(localData, cryptor).entries,
decrypt(remoteData, cryptor).entries
),
updated_at: new Date().toISOString()
},
cryptor
)
}

_mergeArrays(local, remote) {
return Object.values(groupBy(local.concat(remote), item => item.id)).map(
group => {
if (group.length === 1) return group[0]
return merge(group[0], group[1])
}
)
}
}
9 changes: 5 additions & 4 deletions src/renderer/javascripts/actions/entries.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import shortid from 'shortid'
import { DateTime } from 'luxon'
import { encryptData } from 'services/cryptor'
const { sendSaveData, onOnce, sendVaultSyncStart } = window

Expand Down Expand Up @@ -55,7 +56,7 @@ const save = (data, state) => {

const update = (entries, data) => {
const index = entries.findIndex(item => item.id === data.id)
data.updated_at = date()
data.updatedAt = date()
entries[index] = data
return [entries, data]
}
Expand All @@ -67,15 +68,15 @@ const create = (entries, data) => {
}

const date = () => {
return new Date().toISOString()
return DateTime.local().toISO()
}

const buildItem = data => {
const now = date()
return {
id: shortid.generate(),
...data,
created_at: now,
updated_at: now
createdAt: now,
updatedAt: now
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState } from 'react'
import { DateTime } from 'luxon'
import { useSelector, useDispatch } from 'react-redux'

import Login from './login'
Expand Down Expand Up @@ -42,7 +43,7 @@ const Form = ({ entry }) => {
target: { name, value }
} = event
if (name === 'password') {
obj['password_updated_at'] = new Date().toISOString()
obj['password_updated_at'] = DateTime.local().toISO()
}
obj[name] = value
setModel({ ...model, ...obj })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,15 @@ const Show = ({ entry }) => {
<div className="entry-extra">
<div className="item">
<div className="label">Last Modified</div>
<div className="value">{formatDate(entry.updated_at)}</div>
<div className="value">
{formatDate(entry.updatedAt || entry.updated_at)}
</div>
</div>
<div className="item">
<div className="label">Created</div>
<div className="value">{formatDate(entry.created_at)}</div>
<div className="value">
{formatDate(entry.createdAt || entry.created_at)}
</div>
</div>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/stylesheets/components/lock-screen.sass
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@
position: relative
top: 2px
path
fill: #fff
fill: #fff
6 changes: 0 additions & 6 deletions test/setup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,3 @@ global.CONFIG = {
scope: 'user,email'
}
}
global.mockDate = () => {
let currentDate = new Date()
const RealDate = Date
global.Date = jest.fn(() => new RealDate(currentDate.toISOString()))
return currentDate
}
17 changes: 17 additions & 0 deletions test/units/__mocks__/luxon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { DateTime as Datetime } from 'luxon'

const now = Datetime.local()

export const DateTime = {
local: () => {
return {
toISO: jest.fn().mockReturnValue(now.toISO()),
__minus: (value, units) => {
return now.minus(value, units)
}
}
},
fromISO: jest.fn(string => {
return Datetime.fromISO(string)
})
}
Loading

0 comments on commit 7c10cab

Please sign in to comment.