From d8be27aa50965049d9660bb575bc8c0bb5dd6172 Mon Sep 17 00:00:00 2001 From: zeppi Date: Tue, 25 Oct 2022 15:24:06 -0400 Subject: [PATCH] sync wip: ux refactor, wallet encryption tests, wallet import/export, sync loop begin rewrite --- electron/sync/sync.js | 38 +++- electron/sync/testsync.js | 28 ++- static/app-strings.json | 8 + ui/component/headerMenuButtons/view.jsx | 1 + ui/constants/action_types.js | 12 + ui/lbry.js | 4 +- ui/lbrysync.js | 51 +++-- ui/page/settingsSync/index.js | 32 +-- ui/page/settingsSync/view.jsx | 244 ++++++++++++-------- ui/redux/actions/sync.js | 287 +++++++++++++++++------- ui/redux/middleware/shared-state.js | 2 +- ui/redux/reducers/sync.js | 48 +++- ui/redux/selectors/sync.js | 55 +++-- 13 files changed, 542 insertions(+), 268 deletions(-) diff --git a/electron/sync/sync.js b/electron/sync/sync.js index b62537e72..abdf9e1ec 100644 --- a/electron/sync/sync.js +++ b/electron/sync/sync.js @@ -33,10 +33,10 @@ export function deriveSecrets(rootPassword, email, saltSeed, callback) { const DEFAULT_MAXMEM = MAXMEM_MULTIPLIER * SCRYPT_N * SCRYPT_R; function getKeyParts(key) { - const lbryIdPassword = key.slice(0, KEY_LENGTH).toString('base64'); + const providerKey = key.slice(0, KEY_LENGTH).toString('base64'); const hmacKey = key.slice(KEY_LENGTH, KEY_LENGTH * 2).toString('base64'); const dataKey = key.slice(KEY_LENGTH * 2).toString('base64'); - return { lbryIdPassword, hmacKey, dataKey }; // Buffer aa bb cc 6c + return { providerKey, hmacKey, dataKey }; // Buffer aa bb cc 6c } const salt = generateSalt(encodedEmail, saltSeed); @@ -52,8 +52,36 @@ export function deriveSecrets(rootPassword, email, saltSeed, callback) { crypto.scrypt(encodedPassword, salt, KEY_LENGTH * NUM_KEYS, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P, maxmem: DEFAULT_MAXMEM }, scryptCallback); } -export function walletHmac(inputString) { - const hmac = crypto.createHmac('sha256', inputString.toString('utf8')); - const res = hmac.digest('hex'); +export function walletHmac(inputString, hmacKey) { + const res = crypto.createHmac('sha256', hmacKey) + .update(inputString.toString('utf8')) + .digest('hex'); + console.log('hmac res', res) return res; } + +export function aesEncrypt(text, key) { + try { + const iv = crypto.randomBytes(16); + let cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key), iv); + let encrypted = cipher.update(text); + encrypted = Buffer.concat([encrypted, cipher.final()]); + return { iv: iv.toString('hex'), encryptedData: encrypted.toString('hex') }; + } catch (e) { + return { error: `Wallet decrypt failed error: ${e.message}` }; + } +} + +export function aesDecrypt(cipher, key) { + try { + let iv = Buffer.from(cipher.iv, 'hex'); + let encryptedText = Buffer.from(cipher.encryptedData, 'hex'); + let decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key), iv); + let decrypted = decipher.update(encryptedText); + decrypted = Buffer.concat([decrypted, decipher.final()]); + // handle errors here + return { result: decrypted.toString() }; + } catch (e) { + return { error: `Wallet decrypt failed error: ${e.message}`}; + } +} diff --git a/electron/sync/testsync.js b/electron/sync/testsync.js index a30242eef..337650ac2 100644 --- a/electron/sync/testsync.js +++ b/electron/sync/testsync.js @@ -1,8 +1,9 @@ import test from 'tape'; // import sync from '../sync.js'; -import { generateSalt, generateSaltSeed, deriveSecrets, walletHmac } from './sync.js'; +import { generateSalt, generateSaltSeed, deriveSecrets, walletHmac, aesEncrypt, aesDecrypt } from './sync.js'; +import crypto from 'crypto'; +import fs from 'fs'; export default function doTest() { - test('Generate sync seed', (assert) => { const seed = generateSaltSeed(); console.log('seed', seed); @@ -28,12 +29,13 @@ export default function doTest() { const email = 'example@example.com'; const expectedHmacKey = 'bCxUIryLK0Lf9nKg9yiZDlGleMuGJkadLzTje1PAI+8='; //base64 - const expectedLbryIdPassword = 'HKo/J+x4Hsy2NkMvj2JB9RI0yrvEiB4QSA/NHPaT/cA='; + const expectedProviderKey = 'HKo/J+x4Hsy2NkMvj2JB9RI0yrvEiB4QSA/NHPaT/cA='; + // add expectedDataKey to test function cb(e, r) { - console.log('result', r); + console.log('derive keys result:', r); assert.equal(r.hmacKey, expectedHmacKey, 'hmac is expected value'); - assert.equal(r.lbryIdPassword, expectedLbryIdPassword, 'lbryid password is expected value'); + assert.equal(r.providerKey, expectedProviderKey, 'lbryid password is expected value'); assert.end(); } @@ -44,13 +46,25 @@ export default function doTest() { const hmacKey = 'bCxUIryLK0Lf9nKg9yiZDlGleMuGJkadLzTje1PAI+8='; const sequence = 1; const walletState = `zo4MTkyOjE2OjE68QlIU76+W91/v/F1tu8h+kGB0Ee`; - const expectedHmacHex = '52edbad5b0f9d8cf6189795702790cc2cb92060be24672913ab3e4b69c03698b'; + const expectedHmacHex = '9fe70ebdeaf85b3afe5ae42e52f946acc54ded0350acacdded821845217839d4'; const input_str = `${sequence}:${walletState}`; - const hmacHex = walletHmac(input_str); + const hmacHex = walletHmac(input_str, hmacKey); assert.equal(hmacHex, expectedHmacHex); assert.end(); }); + + test('Encrypt/Decrypt', (assert) => { + const key = crypto.randomBytes(32); + // const wrongKey = crypto.randomBytes(32); // todo: what tests should fail; how much error handling needed? + // test a handy json file + const input = fs.readFileSync('../../package.json', 'utf8'); + const cipher = aesEncrypt(input, key); + console.log('cipher', cipher); + const output = aesDecrypt(cipher, key); + assert.equal(input, output.result); + assert.end(); + }); } doTest(); diff --git a/static/app-strings.json b/static/app-strings.json index a05f3edeb..008b71e5f 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -2334,5 +2334,13 @@ "Doing Math": "Doing Math", "Hold on, doing some math.": "Hold on, doing some math.", "You are signed in as %email%.": "You are signed in as %email%.", + "Sync Provider Url": "Sync Provider Url", + "Enter Password": "Enter Password", + "Enter a password.": "Enter a password.", + "Some sign-in relevant message.": "Some sign-in relevant message.", + "Hold on, doing some math. (Registering you)": "Hold on, doing some math. (Registering you)", + "Failed to view lbry://@rossmanngroup#a/Day-39-Gotham-City-Solutions#e, please try again. If this problem persists, visit https://lbry.com/faq/support for support.": "Failed to view lbry://@rossmanngroup#a/Day-39-Gotham-City-Solutions#e, please try again. If this problem persists, visit https://lbry.com/faq/support for support.", + "Anon --[used in <%anonymous% Reposted>]--": "Anon", + "%anonymous%": "%anonymous%", "--end--": "--end--" } diff --git a/ui/component/headerMenuButtons/view.jsx b/ui/component/headerMenuButtons/view.jsx index 1bdf70893..da0e3efe2 100644 --- a/ui/component/headerMenuButtons/view.jsx +++ b/ui/component/headerMenuButtons/view.jsx @@ -34,6 +34,7 @@ export default function HeaderMenuButtons(props: HeaderMenuButtonProps) { + diff --git a/ui/constants/action_types.js b/ui/constants/action_types.js index 53b1605b3..60ffd2e07 100644 --- a/ui/constants/action_types.js +++ b/ui/constants/action_types.js @@ -123,6 +123,12 @@ export const DO_UTXO_CONSOLIDATE_STARTED = 'DO_UTXO_CONSOLIDATE_STARTED'; export const DO_UTXO_CONSOLIDATE_COMPLETED = 'DO_UTXO_CONSOLIDATE_COMPLETED'; export const DO_UTXO_CONSOLIDATE_FAILED = 'DO_UTXO_CONSOLIDATE_FAILED'; export const PENDING_CONSOLIDATED_TXOS_UPDATED = 'PENDING_CONSOLIDATED_TXOS_UPDATED'; +export const WALLET_EXPORT_STARTED = 'WALLET_EXPORT_STARTED'; +export const WALLET_EXPORT_COMPLETED = 'WALLET_EXPORT_COMPLETED'; +export const WALLET_EXPORT_FAILED = 'WALLET_EXPORT_FAILED'; +export const WALLET_IMPORT_STARTED = 'WALLET_IMPORT_STARTED'; +export const WALLET_IMPORT_COMPLETED = 'WALLET_IMPORT_COMPLETED'; +export const WALLET_IMPORT_FAILED = 'WALLET_IMPORT_FAILED'; // Claims export const FETCH_FEATURED_CONTENT_STARTED = 'FETCH_FEATURED_CONTENT_STARTED'; @@ -511,3 +517,9 @@ export const LSYNC_DERIVE_FAILED = 'LSYNC_DERIVE_FAILED'; export const LSYNC_GET_SALT_STARTED = 'LSYNC_GET_SALT_STARTED'; export const LSYNC_GET_SALT_COMPLETED = 'LSYNC_GET_SALT_COMPLETED'; // got salt export const LSYNC_GET_SALT_FAILED = 'LSYNC_GET_SALT_FAILED'; +export const LSYNC_CHECK_EMAIL_STARTED = 'LSYNC_CHECK_EMAIL_STARTED'; +export const LSYNC_CHECK_EMAIL_COMPLETED = 'LSYNC_CHECK_EMAIL_COMPLETED'; // got salt +export const LSYNC_CHECK_EMAIL_FAILED = 'LSYNC_CHECK_EMAIL_FAILED'; +export const LSYNC_SYNC_STARTED = 'LSYNC_SYNC_STARTED'; +export const LSYNC_SYNC_COMPLETED = 'LSYNC_SYNC_COMPLETED'; // got salt +export const LSYNC_SYNC_FAILED = 'LSYNC_SYNC_FAILED'; diff --git a/ui/lbry.js b/ui/lbry.js index ac9768711..eeea69854 100644 --- a/ui/lbry.js +++ b/ui/lbry.js @@ -36,7 +36,7 @@ const Lbry = { // Returns a human readable media type based on the content type or extension of a file that is returned by the sdk getMediaType: (contentType: ?string, fileName: ?string) => { - if (fileName) { + if (fileName && fileName.split('.').length > 1) { const formats = [ [/\.(mp4|m4v|webm|flv|f4v|ogv)$/i, 'video'], [/\.(mp3|m4a|aac|wav|flac|ogg|opus)$/i, 'audio'], @@ -111,6 +111,8 @@ const Lbry = { wallet_list: (params = {}) => daemonCallWithResult('wallet_list', params), wallet_send: (params = {}) => daemonCallWithResult('wallet_send', params), wallet_status: (params = {}) => daemonCallWithResult('wallet_status', params), + wallet_export: (params = {}) => daemonCallWithResult('wallet_export', params), + wallet_import: (params = {}) => daemonCallWithResult('wallet_import', params), address_is_mine: (params = {}) => daemonCallWithResult('address_is_mine', params), address_unused: (params = {}) => daemonCallWithResult('address_unused', params), address_list: (params = {}) => daemonCallWithResult('address_list', params), diff --git a/ui/lbrysync.js b/ui/lbrysync.js index a12948076..adb3e0aa7 100644 --- a/ui/lbrysync.js +++ b/ui/lbrysync.js @@ -4,7 +4,7 @@ const SYNC_API_DOWN = 'sync_api_down'; const DUPLICATE_EMAIL = 'duplicate_email'; const UNKNOWN_ERROR = 'unknown_api_error'; const NOT_FOUND = 'not_found'; -console.log('process.env.', process.env.LBRYSYNC_BASE_URL); +// console.log('process.env.', process.env.LBRYSYNC_BASE_URL); const API_VERSION = 3; const POST = 'POST'; @@ -41,43 +41,47 @@ export async function getAuthToken(email, password, deviceId) { export async function register(email, password, saltSeed) { try { - await callWithResult(POST, REGISTER_ENDPOINT, { email, password, clientSaltSeed: saltSeed }); - return; + const result = await callWithResult(POST, REGISTER_ENDPOINT, { email, password, clientSaltSeed: saltSeed }); + return result; } catch (e) { return { error: e.message }; } } -export async function pushWallet(walletState, hmac, token) { - // token? - const body = { - token: token, - encryptedWallet: walletState.encryptedWallet, - sequence: walletState.sequence, - hmac: hmac, - }; - await callWithResult(POST, WALLET_ENDPOINT, { token, hmac, sequence }); -} - export async function pullWallet(token) { try { - await callWithResult(GET, REGISTER_ENDPOINT, { token }); - return; + const result = await callWithResult(GET, WALLET_ENDPOINT, { token }); + return result; } catch (e) { return { error: e.message }; } -} // token +} + +// export async function pushWallet(walletState, hmac, token) { +// // token? +// const body = { +// token: token, +// encryptedWallet: walletState.encryptedWallet, +// sequence: walletState.sequence, +// hmac: hmac, +// }; +// await callWithResult(POST, WALLET_ENDPOINT, { token, hmac, sequence }); +// } function callWithResult(method, endpoint, params = {}) { return new Promise((resolve, reject) => { - apiCall( + return apiCall( method, endpoint, params, (result) => { + console.log('cwr result', result); resolve(result); }, - reject + (er) => { + console.log('er', er); + reject(er); + } ); }); } @@ -96,13 +100,18 @@ function apiCall(method, endpoint, params, resolve, reject) { return fetch(`${Lbrysync.apiUrl}${endpoint}${searchString}`, options) .then(handleResponse) .then((response) => { - return response; + console.log('response 200', response); + return resolve(response); }) - .catch(reject); + .catch((r) => { + console.log('r', r); + return reject(r); + }); } function handleResponse(response) { if (response.status >= 200 && response.status < 300) { + console.log('200+'); return response.json(); } diff --git a/ui/page/settingsSync/index.js b/ui/page/settingsSync/index.js index 833ca02ec..fccaedd94 100644 --- a/ui/page/settingsSync/index.js +++ b/ui/page/settingsSync/index.js @@ -17,25 +17,18 @@ import { selectLbrySyncEncryptedHmacKey, selectLbrySyncEncryptedRoot, selectLbrySyncEncryptedProviderPass, + selectLbrySyncCheckingEmail, + selectLbrySyncEmailError, + selectLbrySyncEmailCandidate, } from 'redux/selectors/sync'; -import { - doLbrysyncGetSalt, - doLbrysyncRegister, - doGenerateSaltSeed, - doDeriveSecrets, - doLbrysyncAuthenticate, -} from 'redux/actions/sync'; +import { doHandleEmail, doLbrysyncRegister, doLbrysyncAuthenticate, doEmailVerifySubscribe } from 'redux/actions/sync'; const select = (state) => ({ isWalletEncrypted: selectWalletIsEncrypted(state), registering: selectLbrySyncRegistering(state), - registeredEmail: selectLbrySyncEmail(state), registerError: selectLbrySyncRegisterError(state), - gettingSalt: selectLbrySyncGettingSalt(state), - saltError: selectLbrySyncSaltError(state), - saltSeed: selectLbrySyncSaltSeed(state), token: selectLbrySyncToken(state), authenticating: selectLbrySyncIsAuthenticating(state), @@ -46,16 +39,25 @@ const select = (state) => ({ encHmacKey: selectLbrySyncEncryptedHmacKey(state), // ? encRootPass: selectLbrySyncEncryptedRoot(state), encProviderPass: selectLbrySyncEncryptedProviderPass(state), + // begin + // --email + isCheckingEmail: selectLbrySyncCheckingEmail(state), + candidateEmail: selectLbrySyncEmailCandidate(state), + emailError: selectLbrySyncEmailError(state), + registeredEmail: selectLbrySyncEmail(state), + saltSeed: selectLbrySyncSaltSeed(state), + // --password + // registerError }); const perform = (dispatch) => ({ encryptWallet: () => dispatch(doNotifyEncryptWallet()), decryptWallet: () => dispatch(doNotifyDecryptWallet()), - getSalt: (email) => dispatch(doLbrysyncGetSalt(email)), - generateSaltSeed: () => dispatch(doGenerateSaltSeed()), + handleEmail: (email, signUp) => dispatch(doHandleEmail(email, signUp)), authenticate: () => dispatch(doLbrysyncAuthenticate()), - deriveSecrets: (p, e, s) => dispatch(doDeriveSecrets(p, e, s)), - register: (email, secrets, saltseed) => dispatch(doLbrysyncRegister(email, secrets, saltseed)), + waitForVerify: (stop) => dispatch(doEmailVerifySubscribe(stop)), + // deriveSecrets: (p, e, s) => dispatch(doDeriveSecrets(p, e, s)), + register: (password) => dispatch(doLbrysyncRegister(password)), }); export default connect(select, perform)(SettingsSync); diff --git a/ui/page/settingsSync/view.jsx b/ui/page/settingsSync/view.jsx index 4ddf27f12..1c114810d 100644 --- a/ui/page/settingsSync/view.jsx +++ b/ui/page/settingsSync/view.jsx @@ -12,7 +12,6 @@ type Props = { encryptWallet: (string) => void, decryptWallet: (string) => void, registering: boolean, - email: string, registerError: string, token: string, authenticating: boolean, @@ -22,108 +21,117 @@ type Props = { encRootPass: string, encProviderPass: string, getSalt: (string) => void, - gettingSalt: boolean, - saltError: string, - saltSeed: string, deriveSecrets: (string, string, string) => void, // something + + // begin + // email + handleEmail: (string, string) => void, // return something? + checkingEmail: boolean, + candidateEmail?: string, + registeredEmail?: string, + saltSeed: string, + emailError: string, + // password/register + register: (string) => void, + waitForVerify: () => void, }; -export default function NotificationSettingsPage(props: Props) { +export default function SettingsSyncPage(props: Props) { // const { } = props; const { - walletEncrypted, - encryptWallet, - decryptWallet, registering, - registeredEmail, registerError, token, authenticating, authError, authenticate, - derivingKeys, - encHmacKey, // ? encRootPass, - encProviderPass, - getSalt, - generateSaltSeed, - deriveSecrets, - gettingSalt, - saltError, - saltSeed, register, + // begin + // .. email + registeredEmail, + handleEmail, + checkingEmail, + candidateEmail, + saltSeed, + emailError, + // password + // verify + waitForVerify, } = props; /* Register / auth */ - const SIGN_IN_MODE = 'sign_in'; - const SIGN_UP_MODE = 'sign_up'; - const VERIFY_MODE = 'verify'; - const MATH_MODE = 'math'; - const DONE_MODE = 'done'; - const [mode, setMode] = React.useState(registeredEmail ? VERIFY_MODE : SIGN_IN_MODE); + // modes + const SIGN_IN_MODE = 'sign_in_mode'; + const SIGN_UP_MODE = 'sign_up_mode'; + const [mode, setMode] = React.useState(SIGN_IN_MODE); + // steps + const EMAIL_SCREEN = 'sign_in'; // show email input + // checking email + const PASSWORD_SCREEN = 'password'; // show password input + // registering + const REGISTERING_SCREEN = 'register'; // show working page for deriving passwords, registering + const VERIFY_SCREEN = 'verify'; // show waiting for email verification + // waiting for email verification + const SYNC_SCREEN = 'sync'; + // syncing wallet with server + const DONE_SCREEN = 'done'; const [email, setEmail] = React.useState(); const [pass, setPass] = React.useState(); const [showPass, setShowPass] = React.useState(false); - const [error, setError] = React.useState(''); + + let STEP; + if (!candidateEmail) { + STEP = EMAIL_SCREEN; // present email form, on submit check email salt + } else if (!encRootPass && !registering) { + // make this "hasPasswords" + STEP = PASSWORD_SCREEN; // present password form, on submit derive keys and register + } else if (registering) { + STEP = REGISTERING_SCREEN; + } else if (encRootPass && !token) { + STEP = VERIFY_SCREEN; // until token + } else if (token) { + STEP = SYNC_SCREEN; + } + + // error comes from store + // const [error, setError] = React.useState(''); React.useEffect(() => { - let interval; - if (!token && registeredEmail) { - interval = setInterval(() => { - console.log('doauthint'); - authenticate(); - }, 5000); + if (registeredEmail && !token) { + waitForVerify(); } + return () => { - clearInterval(interval); + waitForVerify(true); }; - }, [registeredEmail, token, authenticate]); + }, [registeredEmail, token]); React.useEffect(() => { - if (token && registeredEmail) { - setMode(DONE_MODE); + if (token) { + // sign up + // what + // pushAndStart(); + // what + // } - }, [registeredEmail, token, setMode]); + }, [token]); - const handleSignUp = async () => { - // get salt for email to make sure - const saltSeedOrError = await getSalt(email); - if (saltSeedOrError.seed) { - setError('Email already registered'); - return; - } - // -- if found, report email already exists - sign in? - const saltSeed = await generateSaltSeed(); - // saltSeed = generateSaltSeed() - setMode(MATH_MODE); - const secrets = await deriveSecrets(pass, email, saltSeed); - setMode(VERIFY_MODE); - // passwords = driveKeys(root, email, saltSeed); - try { - const registerSuccess = await register(email, secrets, saltSeed); - } catch (e) { - console.log(e); - } - // registerSuccess = register(email, servicePassword, saltSeed) - - // poll auth until success - // store [token, rootPassword, providerPass, HmacKey, saltSeed, salt, registeredEmail] + const handleRegister = (e) => { + register(pass); }; - const handleSignIn = async () => { - // get saltseed for email - // saltSeed = getSaltSeed() - // -- if error, report email not registered - sign up? - // salt = generateSalt(seed) - // passwords = deriveKeys(root, email, saltSeed); - // token = authenticate(email, servicePassword, deviceId) - // store [token, rootPassword, servicePassword, HmacKey, saltSeed, salt, registeredEmail] - // kick off sync pull - // -- possibly merge conflicts + const handleSignUpEmail = async () => { + // get salt for email to make sure email doesn't exist + handleEmail(email, true); + }; + + const handleSignInEmail = () => { + handleEmail(email, false); }; const doneCard = ( @@ -150,11 +158,19 @@ export default function NotificationSettingsPage(props: Props) { /> ); - const deriveCard = ( - Math...} /> + const registerCard = ( + Math...} /> ); - const signInCard = ( + const syncCard = ( + Math...} + /> + ); + + const signInEmailCard = ( -
+ setEmail(e.target.value)} + helper={emailError && emailError} /> -