Skip to content

Commit

Permalink
Merge pull request #30 from swiftyapp/totp
Browse files Browse the repository at this point in the history
Add Generating Time-based One Time Passwords
  • Loading branch information
alchaplinsky authored May 2, 2020
2 parents b5c39b9 + e3cdb7b commit 8ea38f2
Show file tree
Hide file tree
Showing 26 changed files with 187 additions and 104 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"dependencies": {
"@swiftyapp/cryptor": "^1.0.4",
"classnames": "^2.2.6",
"electron-log": "^4.1.1",
"electron-log": "^4.1.2",
"electron-unhandled": "^3.0.2",
"electron-util": "^0.14.1",
"fs-extra": "^9.0.0",
Expand All @@ -25,6 +25,7 @@
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"shortid": "^2.2.15",
"speakeasy": "^2.0.0",
"universal-analytics": "^0.4.20"
},
"scripts": {
Expand Down
24 changes: 20 additions & 4 deletions src/main/__mocks__/application/cryptor.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,30 @@ const __setEncryption = value => {
encryption = value
}

const encrypt = (secret, value) => {
if (encryption) return `${value}.${secret}`
return value
}

const decrypt = (secret, value) => {
if (encryption) return value.split('.')[0]
return value
}
const constructor = jest.fn(secret => {
return {
encrypt: jest.fn(value => {
if (encryption) return `${value}.${secret}`
return value
return encrypt(secret, value)
}),
decrypt: jest.fn(value => {
if (encryption) return value.split('.')[0]
return value
return decrypt(secret, value)
}),
obscure: jest.fn(data => {
data.password = encrypt(secret, data.password)
return data
}),
expose: jest.fn(data => {
data.password = decrypt(secret, data.password)
return data
}),
encryptData: jest.fn(data => {
if (encryption) {
Expand All @@ -26,6 +41,7 @@ const constructor = jest.fn(secret => {
}
return data
}),

__secret: secret
}
})
Expand Down
31 changes: 29 additions & 2 deletions src/main/application/cryptor/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { Cryptor as BaseCryptor } from '@swiftyapp/cryptor'

const SENSITIVE_FIELDS = {
login: ['password', 'otp'],
note: ['note'],
card: ['pin']
}

const btoa = data => {
const buffer = Buffer.from(data, 'utf8')
return buffer.toString('base64')
Expand All @@ -10,17 +16,30 @@ const atob = data => {
return buffer.toString('utf8')
}

const prepareFields = (data, callback) => {
if (!data) return
const object = Object.assign({}, data)
SENSITIVE_FIELDS[object.type].forEach(field => {
if (!object[field] || object[field] === '') {
object[field] = ''
} else {
object[field] = callback(object[field])
}
})
return object
}

export class Cryptor {
constructor(secret) {
this.cryptor = new BaseCryptor(secret)
}

encryptData(data) {
return btoa(this.cryptor.encrypt(JSON.stringify(data)))
return btoa(this.encrypt(JSON.stringify(data)))
}

decryptData(encrypted) {
return JSON.parse(this.cryptor.decrypt(atob(encrypted)))
return JSON.parse(this.decrypt(atob(encrypted)))
}

encrypt(data) {
Expand All @@ -30,4 +49,12 @@ export class Cryptor {
decrypt(data) {
return this.cryptor.decrypt(data)
}

obscure(data) {
return prepareFields(data, property => this.encrypt(property))
}

expose(data) {
return prepareFields(data, property => this.decrypt(property))
}
}
25 changes: 1 addition & 24 deletions src/main/application/events/vault.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,5 @@
import { Cryptor } from 'application/cryptor'

const SENSITIVE_FIELDS = {
login: ['password'],
note: ['note'],
card: ['pin']
}

const obscure = (data, cryptor) => {
return prepareFields(data, property => cryptor.encrypt(property))
}

const expose = (data, cryptor) => {
return prepareFields(data, property => cryptor.decrypt(property))
}

const prepareFields = (data, callback) => {
if (!data) return
const object = Object.assign({}, data)
SENSITIVE_FIELDS[object.type].forEach(field => {
object[field] = callback(object[field])
})
return object
}

export const onMasterPasswordChange = function (_, data) {
const currentCryptor = new Cryptor(data.current)
const newCryptor = new Cryptor(data.new)
Expand All @@ -32,7 +9,7 @@ export const onMasterPasswordChange = function (_, data) {
if (this.vault.isDecryptable(encrypted, currentCryptor)) {
let decrypted = currentCryptor.decryptData(this.vault.read())
decrypted.entries = decrypted.entries.map(entry => {
return obscure(expose(entry, currentCryptor), newCryptor)
return newCryptor.obscure(currentCryptor.expose(entry))
})
const newEncrypted = newCryptor.encryptData(decrypted)
this.vault.write(newEncrypted)
Expand Down
13 changes: 6 additions & 7 deletions src/preload/cryptor.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@ window.setupCryptor = secret => {
cryptor = new Cryptor(secret)
}

window.encrypt = value => {
return cryptor.encrypt(value)
}

window.decrypt = value => {
return cryptor.decrypt(value)
}
window.decryptData = data => cryptor.decryptData(data)
window.encryptData = data => cryptor.encryptData(data)
window.encrypt = value => cryptor.encrypt(value)
window.decrypt = value => cryptor.decrypt(value)
window.obscure = value => cryptor.obscure(value)
window.expose = value => cryptor.expose(value)
1 change: 1 addition & 0 deletions src/preload/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import './remote'
import './clipboard'
import './generator'
import './cryptor'
import './otp'

window.isSpectron = () => {
return process.env.RUNNING_IN_SPECTRON
Expand Down
19 changes: 19 additions & 0 deletions src/preload/otp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Speakeasy from 'speakeasy'

window.generateOTP = secret => {
const code = Speakeasy.totp({
secret: secret,
encoding: 'base32'
})
const time = 30 - Math.floor((new Date().getTime() / 1000.0) % 30)
return { code, time }
}

window.verifyOTP = (secret, token) => {
Speakeasy.totp.verify({
secret: secret,
encoding: 'base32',
token: token,
window: 1
})
}
4 changes: 2 additions & 2 deletions src/renderer/javascripts/actions/entries.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import shortid from 'shortid'
import { DateTime } from 'luxon'
import { encryptData } from 'services/cryptor'
const { sendSaveData, onOnce, sendVaultSyncStart } = window

const { sendSaveData, onOnce, sendVaultSyncStart, encryptData } = window

export const deleteEntry = id => {
return (dispatch, getState) => {
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/javascripts/components/auth/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Masterpass from '../elements/masterpass'
import Controls from '../elements/controls'
import img from 'swifty.png'

export default ({ touchID }) => {
export const Auth = ({ touchID }) => {
const [error, setError] = useState(null)

const handleEnter = value => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import Card from './card'
import Note from './note'

import { saveEntry, isValid } from 'actions/entries'
import { obscure, expose } from 'services/cryptor'
import entries from 'defaults/entries'

const { obscure, expose } = window

const Form = ({ entry }) => {
const dispatch = useDispatch()
const { scope } = useSelector(state => state.filters)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const Login = ({ entry, validate, onChange, onTagsChange }) => {
generate
</span>
</SecureField>
<SecureField name="OTP" entry={entry} onChange={onChange} />
<Field name="Email" entry={entry} onChange={onChange} />
<TagField entry={entry} onChange={onTagsChange} />
<Field name="Note" entry={entry} onChange={onChange} rows="5" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,10 @@
import React from 'react'
import Copy from 'copy.svg'
import { copy } from 'services/copy'

const { decrypt } = window

export default ({ entry, name, link, cc, secure }) => {
const copy = value => {
const notification = document.getElementsByClassName(
'copied-notification'
)[0]
window.copyToClipboard(value, 60000)
notification.classList.remove('hidden')
setTimeout(() => {
notification.classList.add('hidden')
}, 2000)
}

const onClick = event => {
window.openLink(event.target.href)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { useState, useEffect } from 'react'
import Copy from 'copy.svg'
import { copy } from 'services/copy'

const { generateOTP } = window

export default ({ name, entry }) => {
if (!entry.otp || entry.otp == '') return null

const [time, setTime] = useState(0)
const [code, setCode] = useState('')

useEffect(() => {
setOTPData()
const interval = setInterval(() => {
if (time > 0) {
setTime(time - 1)
} else {
setOTPData()
}
}, 1000)
return () => clearInterval(interval)
}, [])

const setOTPData = () => {
const otp = generateOTP(entry.otp)
setTime(otp.time)
setCode(otp.code)
}

const formattedValue = () => `${code.substr(0, 3)} ${code.substr(3)}`

return (
<div className="item">
<div className="label">{name}</div>
<div className="value">
<strong className="muted">{formattedValue()}</strong>
</div>
<div className="secondary">{time}</div>
<Copy width="16" height="16" onClick={() => copy(code)} />
</div>
)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react'
import Item from './item'
import Totp from './item/totp'
import Tags from './item/tags'

const Login = ({ entry }) => {
Expand All @@ -8,6 +9,7 @@ const Login = ({ entry }) => {
<Item name="Website" entry={entry} link />
<Item name="Username" entry={entry} />
<Item name="Password" entry={entry} secure />
<Totp name="OTP" entry={entry} />
<Item name="Email" entry={entry} />
<Tags entry={entry} />
<Item name="Note" entry={entry} />
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/javascripts/components/main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Sidebar from './sidebar'
import Header from './header'
import Body from './body'

export default () => {
export const Main = () => {
return (
<div className="layout">
<Sidebar />
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/javascripts/components/start/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Choice from './choice'
import Setup from './setup'
import Restore from './restore'

export default () => {
export const Start = () => {
const [flow, setFlow] = useState(null)

if (!flow) return <Choice onSelect={setFlow} />
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/javascripts/components/swifty.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react'
import Start from './start'
import Auth from './auth'
import Main from './main'
import { Start } from './start'
import { Auth } from './auth'
import { Main } from './main'
import { useSelector } from 'react-redux'

const Swifty = () => {
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/javascripts/defaults/entries.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ export default {
username: '',
password: '',
email: '',
note: ''
note: '',
otp: ''
},
note: {
type: 'note',
Expand Down
4 changes: 1 addition & 3 deletions src/renderer/javascripts/reducers/entries.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { decryptData } from 'services/cryptor'

export default (state = initialState(), action) => {
switch (action.type) {
case 'SET_FILTER_SCOPE':
Expand Down Expand Up @@ -39,7 +37,7 @@ export default (state = initialState(), action) => {
}

const decodeEntries = data => {
return decryptData(data).entries
return window.decryptData(data).entries
}

const findEntry = (state, id) => {
Expand Down
11 changes: 11 additions & 0 deletions src/renderer/javascripts/services/copy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const CLIPBOARD_TIMEOUT = 60000
const NOTIFICATION_TIMEOUT = 2000

export const copy = value => {
const notification = document.getElementsByClassName('copied-notification')[0]
window.copyToClipboard(value, CLIPBOARD_TIMEOUT)
notification.classList.remove('hidden')
setTimeout(() => {
notification.classList.add('hidden')
}, NOTIFICATION_TIMEOUT)
}
Loading

0 comments on commit 8ea38f2

Please sign in to comment.