sync wip: ux refactor, wallet encryption tests, wallet import/export, sync loop begin rewrite

This commit is contained in:
zeppi 2022-10-25 15:24:06 -04:00
parent bdfb8d29f5
commit d8be27aa50
13 changed files with 542 additions and 268 deletions

View file

@ -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}`};
}
}

View file

@ -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();

View file

@ -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--"
}

View file

@ -34,6 +34,7 @@ export default function HeaderMenuButtons(props: HeaderMenuButtonProps) {
<MenuList className="menu__list--header">
<HeaderMenuLink page={PAGES.UPLOAD} icon={ICONS.PUBLISH} name={__('Upload')} />
<HeaderMenuLink page={PAGES.CHANNEL_NEW} icon={ICONS.CHANNEL} name={__('New Channel')} />
<HeaderMenuLink page={PAGES.SETTINGS_SYNC} icon={ICONS.GAMING} name={__('Sign In')} />
</MenuList>
</Menu>

View file

@ -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';

View file

@ -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),

View file

@ -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();
}

View file

@ -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);

View file

@ -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 = (
<Card title={__('Doing Math')} subtitle={__('Hold on, doing some math.')} actions={<div>Math...</div>} />
const registerCard = (
<Card title={__('Registering')} subtitle={__('Hold on a moment, signing you up.')} actions={<div>Math...</div>} />
);
const signInCard = (
const syncCard = (
<Card
title={__('Syncing With Server')}
subtitle={__(`Great. Now we're syncing your wallet.`)}
actions={<div>Math...</div>}
/>
);
const signInEmailCard = (
<Card
title={__('Sign In')}
subtitle={
@ -172,7 +188,7 @@ export default function NotificationSettingsPage(props: Props) {
}
actions={
<div>
<Form onSubmit={handleSignIn} className="section">
<Form onSubmit={handleSignInEmail} className="section">
<FormField
autoFocus
placeholder={__('yourstruly@example.com')}
@ -181,25 +197,18 @@ export default function NotificationSettingsPage(props: Props) {
label={__('Email')}
value={email}
onChange={(e) => setEmail(e.target.value)}
helper={emailError && emailError}
/>
<FormField
type={showPass ? 'text' : 'password'}
name="root_password"
inputButton={
<>
<Button
icon={showPass ? ICONS.EYE : ICONS.EYE_OFF}
onClick={() => setShowPass(!showPass)}
className={'editable-text__input-button'}
/>
</>
}
label={__('Password')}
value={pass}
type={'text'}
name="sync_provider"
disabled
label={__('Sync Provider Url')}
value={'https://dev.lbry.id'}
onChange={(e) => setPass(e.target.value)}
/>
<div className="section__actions">
<Button button="primary" type="submit" label={__('Submit')} disabled={!email} />
<Button button="primary" type="submit" label={__('Submit')} disabled={checkingEmail} />
</div>
<p className="help--card-actions">
<I18nMessage
@ -207,7 +216,7 @@ export default function NotificationSettingsPage(props: Props) {
terms: <Button button="link" href="https://www.lbry.com/termsofservice" label={__('terms')} />,
}}
>
By creating an account, you agree to our %terms% and confirm you're over the age of 13.
Some sign-in relevant message.
</I18nMessage>
</p>
</Form>
@ -216,7 +225,7 @@ export default function NotificationSettingsPage(props: Props) {
/>
);
const signUpCard = (
const signUpEmailCard = (
<Card
title={__('Sign Up')}
subtitle={
@ -234,7 +243,7 @@ export default function NotificationSettingsPage(props: Props) {
}
actions={
<div>
<Form onSubmit={handleSignUp} className="section">
<Form onSubmit={handleSignUpEmail} className="section">
<FormField
autoFocus
placeholder={__('yourstruly@example.com')}
@ -243,9 +252,46 @@ export default function NotificationSettingsPage(props: Props) {
label={__('Email')}
value={email}
onChange={(e) => setEmail(e.target.value)}
helper={emailError && emailError}
/>
<FormField
type="password"
type={'text'}
name="sync_provider"
disabled
label={__('Sync Provider Url')}
value={'https://dev.lbry.id'}
onChange={(e) => setPass(e.target.value)}
/>
<div className="section__actions">
<Button button="primary" type="submit" label={__('Submit')} disabled={checkingEmail} />
</div>
</Form>
</div>
}
/>
);
const passwordCard = (
<Card
title={__('Enter Password')}
subtitle={
<>
<p>
<I18nMessage
tokens={{
sign_in: <Button button="link" onClick={() => setMode(EMAIL_SCREEN)} label={__('Sign in')} />,
}}
>
Enter a password.
</I18nMessage>
</p>
</>
}
actions={
<div>
<fieldset-section className="section">
<FormField
type={showPass ? 'text' : 'password'}
name="root_password"
inputButton={
<>
@ -261,7 +307,8 @@ export default function NotificationSettingsPage(props: Props) {
onChange={(e) => setPass(e.target.value)}
/>
<div className="section__actions">
<Button button="primary" type="submit" label={__('Submit')} disabled={!email} />
<Button button="primary" label={__('Submit')} onClick={handleRegister} />{' '}
{/* password input validation */}
</div>
<p className="help--card-actions">
<I18nMessage
@ -272,7 +319,7 @@ export default function NotificationSettingsPage(props: Props) {
By creating an account, you agree to our %terms% and confirm you're over the age of 13.
</I18nMessage>
</p>
</Form>
</fieldset-section>
</div>
}
/>
@ -287,11 +334,12 @@ export default function NotificationSettingsPage(props: Props) {
backout={{ title: __('Wallet Sync'), backLabel: __('Back') }}
>
<div className="card-stack">
{mode === DONE_MODE && <>{doneCard}</>}
{mode === SIGN_IN_MODE && <>{signInCard}</>}
{mode === SIGN_UP_MODE && <>{signUpCard}</>}
{mode === MATH_MODE && <>{deriveCard}</>}
{mode === VERIFY_MODE && <>{verifyCard}</>}
{STEP === EMAIL_SCREEN && mode === SIGN_IN_MODE && <>{signInEmailCard}</>}
{STEP === EMAIL_SCREEN && mode === SIGN_UP_MODE && <>{signUpEmailCard}</>}
{STEP === PASSWORD_SCREEN && <>{passwordCard}</>}
{STEP === REGISTERING_SCREEN && <>{registerCard}</>}
{STEP === VERIFY_SCREEN && <>{verifyCard}</>}
{STEP === SYNC_SCREEN && <>{syncCard}</>}
</div>
</Page>
);

View file

@ -8,7 +8,7 @@ import { ipcRenderer } from 'electron';
import * as Lbrysync from 'lbrysync';
import { safeStoreEncrypt, safeStoreDecrypt, getSavedPassword } from 'util/saved-passwords';
import { doWalletEncrypt, doWalletDecrypt } from 'redux/actions/wallet';
import { doWalletEncrypt, doWalletDecrypt, doUpdateBalance } from 'redux/actions/wallet';
import {
selectSyncHash,
selectGetSyncIsPending,
@ -21,6 +21,7 @@ import Comments from 'comments';
import { getSubsetFromKeysArray } from 'util/sync-settings';
let syncTimer = null;
let verifyInterval = null;
const SYNC_INTERVAL = 1000 * 60 * 5; // 5 minutes
const NO_WALLET_ERROR = 'no wallet found for this user';
const BAD_PASSWORD_ERROR_NAME = 'InvalidPasswordError';
@ -74,71 +75,168 @@ export function doSetDefaultAccount(success: () => void, failure: (string) => vo
};
}
export const doLbrysyncGetSalt = (email: string) => async (dispatch: Dispatch) => {
// "signupCheck=true" - if signupCheck and salt, return fail. else if salt return salt.
// export const doLbrysyncGetSalt = (email: string) => async (dispatch: Dispatch) => {
// const { fetchSaltSeed } = Lbrysync;
// dispatch({
// type: ACTIONS.LSYNC_GET_SALT_STARTED,
// });
// try {
// const saltOrError = await fetchSaltSeed(email);
// console.log('REDUX Salt', saltOrError)
// dispatch({
// type: ACTIONS.LSYNC_GET_SALT_COMPLETED,
// data: { email: email, saltSeed: saltOrError},
// });
// return saltOrError;
// } catch (e) {
// dispatch({
// type: ACTIONS.LSYNC_GET_SALT_FAILED,
// data: { email: email, saltError: 'Not Found'},
// });
// return 'not found';
// }
// };
export const doHandleEmail = (email, isSignUp) => async (dispatch: Dispatch) => {
dispatch({
type: ACTIONS.LSYNC_CHECK_EMAIL_STARTED,
// CLEAR STUFF
});
const { fetchSaltSeed } = Lbrysync;
dispatch({
type: ACTIONS.LSYNC_GET_SALT_STARTED,
});
if (isSignUp) {
try {
const saltOrError = await fetchSaltSeed(email);
await fetchSaltSeed(email);
// !!got salt seed
dispatch({
type: ACTIONS.LSYNC_GET_SALT_COMPLETED,
data: { email: email, saltSeed: saltOrError},
type: ACTIONS.LSYNC_CHECK_EMAIL_FAILED,
data: { emailError: 'Email Already Found' },
});
return saltOrError;
} catch (e) {
// no salt, we're good.
const seed = await ipcRenderer.invoke('invoke-get-salt-seed');
dispatch({
type: ACTIONS.LSYNC_GET_SALT_FAILED,
data: { email: email, saltError: 'Not Found'},
type: ACTIONS.LSYNC_CHECK_EMAIL_COMPLETED,
data: { candidateEmail: email, saltSeed: seed },
});
return 'not found';
}
} else {
// Sign In
try {
const saltResponse = await fetchSaltSeed(email);
// !!got salt seed
dispatch({
type: ACTIONS.LSYNC_CHECK_EMAIL_COMPLETED,
data: { candidateEmail: email, saltSeed: saltResponse.seed },
});
} catch (e) {
console.log('e', e.message);
dispatch({
type: ACTIONS.LSYNC_CHECK_EMAIL_FAILED,
data: { emailError: 'Email Not Found' },
});
}
}
};
// register an email (eventually username)
export const doLbrysyncRegister = (email: string, secrets: any, saltSeed: string) => async (dispatch: Dispatch) => {
const { register } = Lbrysync;
// started
export const doLbrysyncSync = () => async (dispatch: Dispatch, getState: GetState) => {
dispatch({
type: ACTIONS.LSYNC_SYNC_STARTED,
});
// Wallet status
const status = await Lbry.wallet_status();
// if (status.isLocked) {
// // error
// }
// return Lbry.wallet_unlock({ password });
// .then((status) => {
//
//
// }
// See if we should pull
const { pullWallet } = Lbrysync;
// Pull from sync
const walletInfo = await pullWallet();
if (walletInfo) {
console.log('walletInfo', walletInfo);
/*
wallet_state = WalletState(
encrypted_wallet=response.json()['encryptedWallet'],
sequence=response.json()['sequence'],
)
hmac = response.json()['hmac']
return wallet_state, hmac
*/
// Lbry.wallet_import();
// update sequence, others
}
const exported = await Lbry.wallet_export();
// const encWallet = encrypt(exported, password)
};
//register an email (eventually username)
export const doLbrysyncRegister = (password: string) => async (dispatch: Dispatch, getState: GetState) => {
dispatch({
type: ACTIONS.LSYNC_REGISTER_STARTED,
});
const resultIfError = await register(email, secrets.providerPass, saltSeed);
const encProviderPass = safeStoreEncrypt(secrets.providerPass);
const state = getState();
const { sync } = state;
const { candidateEmail: email, saltSeed } = sync;
// started
try {
const secrets = await ipcRenderer.invoke('invoke-get-secrets', password, email, saltSeed);
const encProviderKey = safeStoreEncrypt(secrets.providerKey);
const encHmacKey = safeStoreEncrypt(secrets.hmacKey);
const enctyptedRoot = safeStoreEncrypt(secrets.rootPassword);
const encDataKey = safeStoreEncrypt(secrets.dataKey);
const enctyptedRoot = safeStoreEncrypt(password);
const registerData = {
email,
email, // email
saltSeed,
providerPass: encProviderPass,
providerKey: encProviderKey,
hmacKey: encHmacKey,
dataKey: encDataKey,
rootPass: enctyptedRoot,
};
if (!resultIfError) {
dispatch({
type: ACTIONS.LSYNC_REGISTER_COMPLETED,
data: registerData,
});
} else {
return registerData;
} catch (e) {
console.log('e', e.message);
dispatch({
type: ACTIONS.LSYNC_REGISTER_FAILED,
data: resultIfError,
data: 'ohno',
});
return;
}
};
export function doEmailVerifySubscribe(stop) {
return (dispatch) => {
if (stop) {
clearInterval(verifyInterval);
} else {
dispatch(doLbrysyncAuthenticate());
verifyInterval = setInterval(() => dispatch(doLbrysyncAuthenticate()), 5000);
}
};
}
// get token given username/password
export const doLbrysyncAuthenticate =
() => async (dispatch: Dispatch, getState: GetState) => {
export const doLbrysyncAuthenticate = () => async (dispatch: Dispatch, getState: GetState) => {
dispatch({
type: ACTIONS.LSYNC_AUTH_STARTED,
});
const state = getState();
const { lbrysync } = state;
const { registeredEmail: email, encryptedProviderPass } = lbrysync;
const { sync } = state;
const { registeredEmail: email, encryptedProviderKey } = sync;
const status = await Lbry.status();
const { installation_id: deviceId } = status;
const password = safeStoreDecrypt(encryptedProviderPass);
const password = safeStoreDecrypt(encryptedProviderKey);
const { getAuthToken } = Lbrysync;
@ -149,49 +247,16 @@ export const doLbrysyncAuthenticate =
type: ACTIONS.LSYNC_AUTH_COMPLETED,
data: result.token,
});
clearInterval(verifyInterval);
} else {
dispatch({
type: ACTIONS.LSYNC_AUTH_FAILED,
data: result.error,
});
}
};
export const doGenerateSaltSeed = () => async (dispatch: Dispatch) => {
const result = await ipcRenderer.invoke('invoke-get-salt-seed');
return result;
};
export const doDeriveSecrets = (rootPassword: string, email: string, saltSeed: string) => async (dispatch: Dispatch) =>
{
dispatch({
type: ACTIONS.LSYNC_DERIVE_STARTED,
});
try {
const result = await ipcRenderer.invoke('invoke-get-secrets', rootPassword, email, saltSeed);
const data = {
hmacKey: result.hmacKey,
rootPassword,
providerPass: result.lbryIdPassword,
};
dispatch({
type: ACTIONS.LSYNC_DERIVE_COMPLETED,
data,
});
return data;
} catch (e) {
dispatch({
type: ACTIONS.LSYNC_DERIVE_FAILED,
data: {
error: e,
},
});
return { error: e.message };
}
};
// replaced with
export function doSetSync(oldHash: string, newHash: string, data: any) {
return (dispatch: Dispatch) => {
dispatch({
@ -441,6 +506,54 @@ export function doSyncApply(syncHash: string, syncData: any, password: string) {
};
}
export function doWalletExport(password?: string) {
return (dispatch: Dispatch) => {
dispatch({
type: ACTIONS.WALLET_EXPORT_STARTED,
});
Lbry.wallet_export({ password })
.then((walletData) => {
// if password, etc etc
// return data
dispatch({
type: ACTIONS.WALLET_EXPORT_COMPLETED,
});
})
.catch((e) => {
dispatch({
type: ACTIONS.WALLET_EXPORT_FAILED,
data: {
error: 'Wallet Export Failed',
},
});
});
};
}
export function doWalletImport(data: string, password?: string) {
return (dispatch: Dispatch) => {
dispatch({
type: ACTIONS.WALLET_IMPORT_STARTED,
});
Lbry.wallet_import({ data, password })
.then((walletData) => {
// if password, etc etc
// return data
dispatch({
type: ACTIONS.WALLET_IMPORT_COMPLETED,
});
})
.catch((e) => {
dispatch({
type: ACTIONS.WALLET_IMPORT_FAILED,
data: {
error: 'Wallet Import Failed',
},
});
});
};
}
export function doCheckSync() {
return (dispatch: Dispatch) => {
dispatch({

View file

@ -19,7 +19,7 @@ export const buildSharedStateMiddleware =
const actionResult = next(action);
// Call `getState` after calling `next` to ensure the state has updated in response to the action
function runPreferences() {
const nextState: { settings: any } = getState(); // bring in lbrysync
const nextState: { settings: any } = getState(); // bring in lbrysync?
const syncEnabled =
nextState.settings && nextState.settings.clientSettings && nextState.settings.clientSettings.enable_sync;
const signedIn = false; // get from new sync system newsync

View file

@ -17,7 +17,14 @@ const defaultState = {
hashChanged: false,
fatalError: false,
// lbrysync
syncProvider: null,
// --email
checkingEmail: false,
candidateEmail: null,
emailError: null,
//syncProvider: null,
gettingSalt: false,
saltSeed: null,
saltError: null,
// reg
registering: false,
registeredEmail: null,
@ -31,10 +38,7 @@ const defaultState = {
encryptedHmacKey: null,
encryptedRoot: null,
encryptedProviderPass: null,
// salt
gettingSalt: false,
saltSeed: null,
saltError: null,
encryptedDataKey: null,
};
reducers[ACTIONS.SYNC_STATE_POPULATE] = (state) => {
@ -136,17 +140,21 @@ reducers[ACTIONS.LSYNC_REGISTER_COMPLETED] = (state, action) => ({
...state,
registeredEmail: action.data.email,
encryptedHmacKey: action.data.hmacKey,
encryptedProviderPass: action.data.providerPass,
encryptedDataKey: action.data.dataKey,
encryptedProviderKey: action.data.providerKey,
encryptedRoot: action.data.rootPass,
saltSeed: action.data.saltSeed,
syncProvider: action.data.syncProvider,
registering: false,
});
// clear attempt
reducers[ACTIONS.LSYNC_REGISTER_FAILED] = (state, action) => ({
...state,
registeredEmail: null,
registering: false,
registerError: action.data.error,
});
// Auth
// Auth
reducers[ACTIONS.LSYNC_AUTH_STARTED] = (state) => ({
...state,
isAuthenticating: true,
@ -161,12 +169,13 @@ reducers[ACTIONS.LSYNC_AUTH_FAILED] = (state, action) => ({
authError: action.data,
isAuthenticating: false,
});
// derive
// derive
reducers[ACTIONS.LSYNC_DERIVE_STARTED] = (state) => ({
...state,
derivingKeys: true,
deriveError: null,
});
// add more keys
reducers[ACTIONS.LSYNC_DERIVE_COMPLETED] = (state, action) => ({
...state,
derivingKeys: false,
@ -176,7 +185,7 @@ reducers[ACTIONS.LSYNC_DERIVE_FAILED] = (state, action) => ({
deriveError: action.data.error,
derivingKeys: false,
});
// salt
// salt util
reducers[ACTIONS.LSYNC_GET_SALT_STARTED] = (state) => ({
...state,
gettingSalt: true,
@ -191,6 +200,27 @@ reducers[ACTIONS.LSYNC_GET_SALT_FAILED] = (state, action) => ({
saltError: action.data.error,
gettingSalt: false,
});
// email
reducers[ACTIONS.LSYNC_CHECK_EMAIL_STARTED] = (state) => ({
...state,
checkingEmail: true,
emailError: null,
candidateEmail: null,
saltSeed: null,
});
reducers[ACTIONS.LSYNC_CHECK_EMAIL_COMPLETED] = (state, action) => ({
...state,
checkingEmail: false,
candidateEmail: action.data.candidateEmail,
saltSeed: action.data.saltSeed,
});
reducers[ACTIONS.LSYNC_CHECK_EMAIL_FAILED] = (state, action) => ({
...state,
checkingEmail: false,
emailError: action.data.emailError,
candidateEmail: null,
saltSeed: null,
});
reducers[ACTIONS.SYNC_RESET] = () => defaultState;

View file

@ -1,49 +1,56 @@
import { createSelector } from 'reselect';
const selectState = state => state.sync || {};
const selectState = (state) => state.sync || {};
export const selectHasSyncedWallet = createSelector(selectState, state => state.hasSyncedWallet);
export const selectHasSyncedWallet = createSelector(selectState, (state) => state.hasSyncedWallet);
export const selectSyncHash = createSelector(selectState, state => state.syncHash);
export const selectSyncHash = createSelector(selectState, (state) => state.syncHash);
export const selectSyncData = createSelector(selectState, state => state.syncData);
export const selectSyncData = createSelector(selectState, (state) => state.syncData);
export const selectSetSyncErrorMessage = createSelector(selectState, state => state.setSyncErrorMessage);
export const selectSetSyncErrorMessage = createSelector(selectState, (state) => state.setSyncErrorMessage);
export const selectGetSyncErrorMessage = createSelector(selectState, state => state.getSyncErrorMessage);
export const selectGetSyncErrorMessage = createSelector(selectState, (state) => state.getSyncErrorMessage);
export const selectGetSyncIsPending = createSelector(selectState, state => state.getSyncIsPending);
export const selectGetSyncIsPending = createSelector(selectState, (state) => state.getSyncIsPending);
export const selectSetSyncIsPending = createSelector(selectState, state => state.setSyncIsPending);
export const selectSetSyncIsPending = createSelector(selectState, (state) => state.setSyncIsPending);
export const selectHashChanged = createSelector(selectState, state => state.hashChanged);
export const selectHashChanged = createSelector(selectState, (state) => state.hashChanged);
export const selectSyncApplyIsPending = createSelector(selectState, state => state.syncApplyIsPending);
export const selectSyncApplyIsPending = createSelector(selectState, (state) => state.syncApplyIsPending);
export const selectSyncApplyErrorMessage = createSelector(selectState, state => state.syncApplyErrorMessage);
export const selectSyncApplyErrorMessage = createSelector(selectState, (state) => state.syncApplyErrorMessage);
export const selectSyncApplyPasswordError = createSelector(selectState, state => state.syncApplyPasswordError);
export const selectSyncApplyPasswordError = createSelector(selectState, (state) => state.syncApplyPasswordError);
export const selectSyncIsLocked = createSelector(selectState, state => state.syncLocked);
export const selectSyncIsLocked = createSelector(selectState, (state) => state.syncLocked);
export const selectPrefsReady = createSelector(selectState, state => state.prefsReady);
export const selectPrefsReady = createSelector(selectState, (state) => state.prefsReady);
export const selectSyncFatalError = createSelector(selectState, state => state.fatalError);
export const selectSyncFatalError = createSelector(selectState, (state) => state.fatalError);
// lbrysync
export const selectLbrySyncRegistering = createSelector(selectState, (state) => state.registering);
export const selectLbrySyncEmail = createSelector(selectState, (state) => state.registeredEmail);
export const selectLbrySyncRegisterError = createSelector(selectState, (state) => state.registerError);
// begin
export const selectLbrySyncCheckingEmail = createSelector(selectState, (state) => state.checkingEmail);
export const selectLbrySyncEmailError = createSelector(selectState, (state) => state.emailError);
export const selectLbrySyncEmail = createSelector(selectState, (state) => state.registeredEmail);
export const selectLbrySyncEmailCandidate = createSelector(selectState, (state) => state.candidateEmail);
export const selectLbrySyncGettingSalt = createSelector(selectState, (state) => state.gettingSalt);
export const selectLbrySyncSaltError = createSelector(selectState, (state) => state.saltError);
export const selectLbrySyncSaltSeed = createSelector(selectState, (state) => state.saltSeed);
export const selectLbrySyncIsAuthenticating = createSelector(selectState, (state) => state.isAuthenticating);
export const selectLbrySyncAuthError = createSelector(selectState, (state) => state.authError);
export const selectLbrySyncToken = createSelector(selectState, (state) => state.authToken);
// password
export const selectLbrySyncDerivingKeys = createSelector(selectState, (state) => state.derivingKeys);
export const selectLbrySyncEncryptedHmacKey = createSelector(selectState, (state) => state.encryptedHmacKey);
export const selectLbrySyncEncryptedRoot = createSelector(selectState, (state) => state.encryptedRoot);
export const selectLbrySyncEncryptedProviderPass = createSelector(selectState, (state) => state.encryptedProviderPass);
// register
export const selectLbrySyncRegistering = createSelector(selectState, (state) => state.registering);
export const selectLbrySyncRegisterError = createSelector(selectState, (state) => state.registerError);
// auth
export const selectLbrySyncIsAuthenticating = createSelector(selectState, (state) => state.isAuthenticating);
export const selectLbrySyncAuthError = createSelector(selectState, (state) => state.authError);
export const selectLbrySyncToken = createSelector(selectState, (state) => state.authToken);
// push/pull?