adding e2e key management in mobile app

This commit is contained in:
balzack 2022-12-07 23:27:27 -08:00
parent 32e2479793
commit 4a8c2f1776
7 changed files with 276 additions and 6 deletions

View File

@ -669,7 +669,7 @@ SPEC CHECKSUMS:
FirebaseInstallations: 99d24bac0243cf8b0e96cf5426340d211f0bcc80
FirebaseMessaging: 4487bbff9b9b927ba1dd3ea40d1ceb58e4ee3cb5
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: 3d02b25ca00c2d456734d0bcff864cbc62f6ae1a
glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
GoogleDataTransport: 1c8145da7117bd68bbbed00cf304edb6a24de00f
GoogleUtilities: 1d20a6ad97ef46f67bbdec158ce00563a671ebb7
nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431

View File

@ -0,0 +1,7 @@
import { checkResponse, fetchWithTimeout } from './fetchUtil';
export async function setAccountSeal(server, token, seal) {
let res = await fetchWithTimeout(`https://${server}/account/seal?agent=${token}`, { method: 'PUT', body: JSON.stringify(seal) })
checkResponse(res);
}

View File

@ -48,8 +48,9 @@ export function useAccountContext() {
setSession: async (access) => {
const { guid, server, appToken } = access;
const status = await store.actions.getAccountStatus(guid);
const sealKey = await store.actions.getAccountSealKey(guid);
const revision = await store.actions.getAccountRevision(guid);
updateState({ status });
updateState({ status, sealKey });
setRevision.current = revision;
curRevision.current = revision;
session.current = access;
@ -70,6 +71,16 @@ export function useAccountContext() {
const { server, appToken } = session.current;
await setAccountSearchable(server, appToken, flag);
},
setAccountSeal: async (seal, key) => {
const { guid, server, appToken } = session.current;
await setAccountSeal(server, appToken, seal);
await store.actions.setAccountSealKey(guid, key);
updateState({ sealKey: key });
},
unlockAccountSeal: async (key) => {
await store.actions.setAccountSealKey(guid, key);
updateState({ sealKey: key });
},
setLogin: async (username, password) => {
const { server, appToken } = session.current;
await setAccountLogin(server, appToken, username, password);

View File

@ -76,6 +76,14 @@ export function useStoreContext() {
const dataId = `${guid}_status`;
await db.current.executeSql("INSERT OR REPLACE INTO app (key, value) values (?, ?);", [dataId, encodeObject(status)]);
},
getAccountSealKey: async (guid) => {
const dataId = `${guid}_sealkey`;
return await getAppValue(db.current, dataId, {});
},
setAccountSealKey: async (guid, key) => {
const dataId = `${guid}_sealkey`;
await db.current.executeSql("INSERT OR REPLACE INTO app (key, value) values (?, ?);", [dataId, encodeObject(key)]);
},
getAccountRevision: async (guid) => {
const dataId = `${guid}_accountRevision`;
return await getAppValue(db.current, dataId, null);

View File

@ -143,10 +143,12 @@ export function ProfileBody({ navigation }) {
</TouchableOpacity>
<Switch style={styles.visibleSwitch} value={state.pushEnabled} onValueChange={setNotifications} trackColor={styles.switch}/>
</View>
<TouchableOpacity style={styles.link} onPress={actions.sealTest}>
<Text style={styles.linkText}>Test Seal</Text>
<TouchableOpacity style={styles.link} onPress={actions.showSealEdit}>
<Ionicons name="setting" size={14} color={Colors.primary} />
<Text style={styles.linkText}>Sealed Topics</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.link} onPress={actions.showLoginEdit}>
<Ionicons name="lock" size={14} color={Colors.primary} />
<Text style={styles.linkText}>Change Login</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.link} onPress={actions.showBlockedCards}>
@ -256,6 +258,128 @@ export function ProfileBody({ navigation }) {
</View>
</KeyboardAvoidingView>
</Modal>
<Modal
animationType="fade"
transparent={true}
visible={state.sealEdit}
supportedOrientations={['portrait', 'landscape']}
onRequestClose={actions.hideSealEdit}
>
<KeyboardAvoidingView behavior="height" style={styles.editWrapper}>
<View style={styles.editContainer}>
<Text style={styles.editHeader}>Sealed Topics:</Text>
<View style={styles.sealable}>
<TouchableOpacity onPress={() => actions.setSealable(!state.sealable)} activeOpacity={1}>
<Text style={styles.sealableText}>Enable Sealed Topics</Text>
</TouchableOpacity>
<Switch style={styles.sealableSwitch} value={state.sealable} onValueChange={actions.setSealable} trackColor={styles.switch}/>
</View>
{ state.sealMode === 'unlocking' && (
<>
{ !state.showSealUnlock && (
<View style={styles.inputField}>
<TextInput style={styles.input} value={state.sealUnlock} onChangeText={actions.setSealUnlock}
autoCapitalize={'none'} secureTextEntry={true} placeholder="Seal Password"
placeholderTextColor={Colors.grey} />
<TouchableOpacity onPress={actions.showSealUnlock}>
<Ionicons style={styles.icon} name="eyeo" size={18} color="#888888" />
</TouchableOpacity>
</View>
)}
{ state.showSealUnlock && (
<View style={styles.inputField}>
<TextInput style={styles.input} value={state.sealUnlock} onChangeText={actions.setSealUnlock}
autoCapitalize={'none'} secureTextEntry={false} placeholder="Seal Password"
placeholderTextColor={Colors.grey} />
<TouchableOpacity onPress={actions.hideSealUnlock}>
<Ionicons style={styles.icon} name="eye" size={18} color="#888888" />
</TouchableOpacity>
</View>
)}
</>
)}
{ (state.sealMode === 'updating' || state.sealMode === 'enabling') && (
<>
{ !state.showSealPassword && (
<View style={styles.inputField}>
<TextInput style={styles.input} value={state.sealPassword} onChangeText={actions.setSealPassword}
autoCapitalize={'none'} secureTextEntry={true} placeholder="Password Seal"
placeholderTextColor={Colors.grey} />
<TouchableOpacity onPress={actions.showSealPassword}>
<Ionicons style={styles.icon} name="eyeo" size={18} color="#888888" />
</TouchableOpacity>
</View>
)}
{ state.showSealPassword && (
<View style={styles.inputField}>
<TextInput style={styles.input} value={state.sealPassword} onChangeText={actions.setSealPassword}
autoCapitalize={'none'} secureTextEntry={false} placeholder="Password Seal"
placeholderTextColor={Colors.grey} />
<TouchableOpacity onPress={actions.hideSealPassword}>
<Ionicons style={styles.icon} name="eye" size={18} color="#888888" />
</TouchableOpacity>
</View>
)}
{ !state.showSealConfirm && (
<View style={styles.inputField}>
<TextInput style={styles.input} value={state.sealConfirm} onChangeText={actions.setSealConfirm}
autoCapitalize={'none'} secureTextEntry={true} placeholder="Confirm Seal"
placeholderTextColor={Colors.grey} />
<TouchableOpacity onPress={actions.showSealConfirm}>
<Ionicons style={styles.icon} name="eyeo" size={18} color="#888888" />
</TouchableOpacity>
</View>
)}
{ state.showSealConfirm && (
<View style={styles.inputField}>
<TextInput style={styles.input} value={state.sealConfirm} onChangeText={actions.setSealConfirm}
autoCapitalize={'none'} secureTextEntry={false} placeholder="Confirm Seal"
placeholderTextColor={Colors.grey} />
<TouchableOpacity onPress={actions.hideSealConfirm}>
<Ionicons style={styles.icon} name="eye" size={18} color="#888888" />
</TouchableOpacity>
</View>
)}
</>
)}
{ state.sealMode === 'disabling' && (
<View style={styles.inputField}>
<Ionicons style={styles.warn} name="exclamationcircleo" size={18} color="#888888" />
<TextInput style={styles.input} value={state.sealDelete} onChangeText={actions.setSealDelete}
autoCapitalize={'none'} placeholder="Type 'delete' to remove sealing key"
placeholderTextColor={Colors.grey} />
</View>
)}
{ state.sealMode === 'unlocked' && (
<View style={styles.inputField}>
<TextInput style={styles.input} value={'xxxxxxxx'} editable="false" secureTextEntry={true} />
<TouchableOpacity style={styles.sealUpdate} onPress={actions.updateSeal} />
</View>
)}
<View style={styles.editControls}>
<TouchableOpacity style={styles.cancel} onPress={actions.hideSealEdit}>
<Text>Cancel</Text>
</TouchableOpacity>
{ state.sealMode !== 'unlocking' && (
<TouchableOpacity style={styles.cancel} onPress={actions.hideSealEdit}>
<Text>Save</Text>
</TouchableOpacity>
)}
{ state.sealMode === 'unlocking' && (
<TouchableOpacity style={styles.cancel} onPress={actions.hideSealEdit}>
<Text>Unlock</Text>
</TouchableOpacity>
)}
</View>
</View>
</KeyboardAvoidingView>
</Modal>
<Modal
animationType="fade"
transparent={true}

View File

@ -14,6 +14,10 @@ export const styles = StyleSheet.create({
icon: {
paddingTop: 2,
},
warn: {
paddingTop: 2,
paddingRight: 8,
},
wrapper: {
backgroundColor: Colors.formBackground,
},
@ -133,6 +137,27 @@ export const styles = StyleSheet.create({
fontSize: 16,
color: Colors.text,
},
sealUpdate: {
position: 'absolute',
top: 0,
height: '100%',
left: 0,
width: '100%',
},
sealable: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
paddingBottom: 8,
},
sealableText: {
fontSize: 16,
color: Colors.text,
},
sealableSwitch: {
transform: [{ scaleX: .7 }, { scaleY: .7 }],
},
visible: {
display: 'flex',
flexDirection: 'row',
@ -282,8 +307,11 @@ export const styles = StyleSheet.create({
},
link: {
marginTop: 16,
display: 'flex',
flexDirection: 'row',
},
linkText: {
paddingLeft: 8,
color: Colors.primary,
},
delete: {

View File

@ -36,6 +36,22 @@ export function useProfileBody() {
blockedMessages: false,
tabbed: null,
disconnected: false,
seal: null,
sealKey: null,
sealEnabled: false,
sealUnlocked: false,
sealEdit: false,
sealMode: null,
sealable: false,
sealUnlock: null,
showSealUnlock: false,
sealPassword: null,
showSealPassword: false,
sealConfirm: null,
showSealConfirm: false,
sealDelete: null,
});
const app = useContext(AppContext);
@ -66,8 +82,11 @@ export function useProfileBody() {
}, [profile]);
useEffect(() => {
const { searchable, pushEnabled } = account.state.status;
updateState({ searchable, pushEnabled });
const { searchable, pushEnabled, seal } = account.state.status;
const sealKey = account.state.sealKey;
const sealEnabled = seal?.publicKey != null;
const sealUnlocked = seal?.publicKey === sealKey?.public && sealKey?.private && sealKey?.public;
updateState({ searchable, pushEnabled, seal, sealKey, sealEnabled, sealUnlocked });
}, [account]);
useEffect(() => {
@ -122,6 +141,79 @@ export function useProfileBody() {
console.log("SEAL:", seal);
},
showSealEdit: () => {
let sealMode = null;
const sealable = state.sealEnabled;
if (state.sealEnabled && !state.sealUnlocked) {
sealMode = 'unlocking';
}
else if (state.sealEnabled && state.sealUnlocked) {
sealMode = 'unlocked';
}
else {
sealMode = 'disabled';
}
updateState({ sealEdit: true, sealable, sealMode });
},
hideSealEdit: () => {
updateState({ sealEdit: false });
},
setSealable: (sealable) => {
let sealMode = null;
if (sealable !== state.sealEnabled) {
if (sealable) {
sealMode = 'enabling';
}
else {
sealMode = 'disabling';
}
}
else {
if (state.sealEnabled && !state.sealUnlocked) {
sealMode = 'unlocking';
}
else if (state.sealEnabled && state.sealUnlocked) {
sealMode = 'unlocked';
}
else {
sealMode = 'disabled';
}
}
updateState({ sealable, sealMode });
},
showSealUnlock: () => {
updateState({ showSealUnlock: true });
},
hideSealUnlock: () => {
updateState({ showSealUnlock: false });
},
setSealUnlock: (sealUnlock) => {
updateState({ sealUnlock });
},
showSealPassword: () => {
updateState({ showSealPassword: true });
},
hideSealPassword: () => {
updateState({ showSealPassword: false });
},
setSealPassword: (sealPassword) => {
updateState({ sealPassword });
},
showSealConfirm: () => {
updateState({ showSealConfirm: true });
},
hideSealConfirm: () => {
updateState({ showSealConfirm: false });
},
setSealConfirm: (sealConfirm) => {
updateState({ sealConfirm });
},
setSealDelete: (sealDelete) => {
updateState({ sealDelete });
},
updateSeal: () => {
updateState({ sealMode: 'updating' });
},
logout: () => {
app.actions.logout();
navigate('/');