keychain-snap
Advanced tools
Comparing version 0.3.3 to 0.3.4
{ | ||
"name": "keychain-snap", | ||
"version": "0.3.3", | ||
"version": "0.3.4", | ||
"description": "EthSign Keychain Snap", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/MetaMask/template-snap-monorepo.git" | ||
"url": "https://github.com/EthSign/keychain-snap" | ||
}, | ||
"license": "(MIT-0 OR Apache-2.0)", | ||
"author": { | ||
"name": "Jordan Bettencourt" | ||
}, | ||
"main": "src/index.ts", | ||
@@ -11,0 +14,0 @@ "files": [ |
{ | ||
"version": "0.3.3", | ||
"version": "0.3.4", | ||
"description": "Password manager.", | ||
@@ -7,6 +7,6 @@ "proposedName": "EthSign Keychain", | ||
"type": "git", | ||
"url": "https://github.com/MetaMask/template-snap-monorepo.git" | ||
"url": "https://github.com/EthSign/keychain-snap" | ||
}, | ||
"source": { | ||
"shasum": "Fbsjbxl6FHyJrTnxGOniWBkAtRmuSy92QxT7HCNKnWw=", | ||
"shasum": "pJ+8LUXvoXjMAwR9Ykjz6ps7xxHE3X+fmTR8yqFNBGk=", | ||
"location": { | ||
@@ -28,7 +28,3 @@ "npm": { | ||
"snap_manageState": {}, | ||
"snap_getBip44Entropy": [ | ||
{ | ||
"coinType": 60 | ||
} | ||
], | ||
"snap_getEntropy": {}, | ||
"endowment:network-access": {} | ||
@@ -35,0 +31,0 @@ }, |
303
src/index.ts
// eslint-disable-next-line | ||
import * as types from '@metamask/snaps-types'; | ||
import { createHash } from 'crypto'; | ||
import { OnRpcRequestHandler } from '@metamask/snaps-types'; | ||
import { Mutex } from 'async-mutex'; | ||
import { heading, panel, text } from '@metamask/snaps-ui'; | ||
import { encrypt, decrypt } from 'eciesjs'; | ||
import nacl from 'tweetnacl'; | ||
import { | ||
@@ -12,5 +12,6 @@ decryptDataArrayFromString, | ||
getObjectsFromStorage, | ||
getRegistryFromAWS, | ||
getTransactionIdFromStorageUploadBatch, | ||
} from './arweave'; | ||
import { getAddress, getKeys } from './misc/address'; | ||
import { getEntropyAddress, getKeys } from './misc/address'; | ||
import { | ||
@@ -21,3 +22,2 @@ generateNonce, | ||
} from './misc/binary'; | ||
import nacl from 'tweetnacl'; | ||
import { | ||
@@ -29,9 +29,5 @@ importCredentials, | ||
} from './misc/popups'; | ||
import { AWS_API_ENDPOINT } from './constants'; | ||
import { RemoteLocation } from './types'; | ||
enum RemoteLocation { | ||
ARWEAVE, | ||
AWS, | ||
NONE, | ||
} | ||
type EthSignKeychainBase = { | ||
@@ -75,2 +71,3 @@ address?: string; | ||
remoteLocation: RemoteLocation | null; | ||
awsInitFailure: boolean | null; | ||
} & EthSignKeychainBase; | ||
@@ -128,2 +125,3 @@ | ||
remoteLocation: null, | ||
awsInitFailure: null, | ||
} as EthSignKeychainState; | ||
@@ -160,2 +158,3 @@ } | ||
remoteLocation: null, | ||
awsInitFailure: null, | ||
} as EthSignKeychainState; | ||
@@ -174,3 +173,3 @@ } | ||
keys.privateKey, | ||
state.password | ||
state.password, | ||
) ?? | ||
@@ -195,2 +194,3 @@ ({ | ||
remoteLocation: null, | ||
awsInitFailure: null, | ||
} as EthSignKeychainState) | ||
@@ -201,2 +201,15 @@ ); | ||
/** | ||
* Return a response object for limited functionality preventing this from running properly. | ||
* | ||
* @returns Response object with restricted functionality message. | ||
*/ | ||
function functionalityLimited() { | ||
return { | ||
success: false, | ||
message: | ||
"This functionality has been restricted due to MetaMask's last-minute API change on snaps. Please see https://github.com/MetaMask/snaps/issues/1665 for updates.", | ||
}; | ||
} | ||
/** | ||
* Save the new state to MetaMask encrypted with the user's private key. | ||
@@ -223,3 +236,3 @@ * | ||
keys.privateKey, | ||
state.password | ||
state.password, | ||
); | ||
@@ -244,4 +257,7 @@ | ||
async function registry( | ||
address: string | ||
address: string, | ||
): Promise<{ publicAddress: string; publicKey: string }> { | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore | ||
return functionalityLimited(); | ||
if (!address) { | ||
@@ -254,7 +270,7 @@ return { | ||
let files: any = await getFilesForUser(address.toLowerCase()); | ||
let files: any = await getRegistryFromAWS(address.toLowerCase()); | ||
files = files.filter((file: any) => file.type === 'registry'); | ||
// Registry entries are always unencrypted, so no keys or passwords are required. | ||
const state: EthSignKeychainState = await getObjectsFromStorage( | ||
let state: EthSignKeychainState = await getObjectsFromStorage( | ||
files, | ||
@@ -265,5 +281,23 @@ '', | ||
null, | ||
undefined | ||
undefined, | ||
); | ||
// We did not find any registry information on AWS. Try Arweave. | ||
if (state.registry.timestamp === 0) { | ||
files = await getFilesForUser( | ||
address.toLowerCase(), | ||
RemoteLocation.ARWEAVE, | ||
); | ||
files = files.filter((file: any) => file.type === 'registry'); | ||
state = await getObjectsFromStorage( | ||
files, | ||
'', | ||
'', | ||
address, | ||
null, | ||
undefined, | ||
); | ||
} | ||
// By this point, we found the registry or will be returning empty values. | ||
return { | ||
@@ -284,3 +318,3 @@ publicAddress: state.registry.publicAddress, | ||
state: EthSignKeychainState, | ||
data: string | ||
data: string, | ||
): Promise<{ success: boolean; message?: string }> { | ||
@@ -294,11 +328,12 @@ if (!data) { | ||
if (state.remoteLocation !== RemoteLocation.AWS) { | ||
if (!state.remoteLocation) { | ||
if (state.remoteLocation) { | ||
res = Boolean(await whereToSync('AWS')); | ||
if (res) { | ||
// eslint-disable-next-line require-atomic-updates | ||
state.remoteLocation = RemoteLocation.AWS; | ||
} | ||
} else { | ||
// We default to AWS syncing anyway, so if the local state's remoteLocation value | ||
// has never been set, we don't need to ask the user to initialize the value. | ||
state.remoteLocation = RemoteLocation.AWS; | ||
} else { | ||
res = !!(await whereToSync('AWS')); | ||
if (res) { | ||
state.remoteLocation = RemoteLocation.AWS; | ||
} | ||
} | ||
@@ -309,4 +344,5 @@ } | ||
if (state.remoteLocation !== RemoteLocation.ARWEAVE) { | ||
res = !!(await whereToSync('Arweave')); | ||
res = Boolean(await whereToSync('Arweave')); | ||
if (res) { | ||
// eslint-disable-next-line require-atomic-updates | ||
state.remoteLocation = RemoteLocation.ARWEAVE; | ||
@@ -318,4 +354,5 @@ } | ||
if (state.remoteLocation !== RemoteLocation.NONE) { | ||
res = !!(await whereToSync('None')); | ||
res = Boolean(await whereToSync('None')); | ||
if (res) { | ||
// eslint-disable-next-line require-atomic-updates | ||
state.remoteLocation = RemoteLocation.NONE; | ||
@@ -335,2 +372,36 @@ } | ||
/** | ||
* Initialize AWS server so that the current user is created in the system for storing future credential entries. | ||
* | ||
* @param state - Current EthSignKeychainState. | ||
* @param userPublicKey - Current user's public key. | ||
*/ | ||
async function initAws( | ||
state: EthSignKeychainState, | ||
userPublicKey: string, | ||
): Promise<boolean> { | ||
try { | ||
return await fetch(`${AWS_API_ENDPOINT}/users`, { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/json', | ||
}, | ||
body: JSON.stringify({ | ||
publicKey: userPublicKey, | ||
}), | ||
}) | ||
.then((res) => res.json()) | ||
.then((response) => { | ||
if (!response.data) { | ||
state.awsInitFailure = true; | ||
return false; | ||
} | ||
return true; | ||
}); | ||
} catch (err) { | ||
state.awsInitFailure = true; | ||
return false; | ||
} | ||
} | ||
/** | ||
* Sync the provided state with the remote state built from document retrieval on Arweave. | ||
@@ -341,11 +412,18 @@ * | ||
async function sync( | ||
state: EthSignKeychainState | ||
state: EthSignKeychainState, | ||
): Promise<EthSignKeychainState> { | ||
if (state.remoteLocation === null) { | ||
// Ask user where they want to store their remote passwords. | ||
// TODO: Default to AWS and don't ask where to sync here | ||
// const result = await whereToSync(); | ||
// state.remoteLocation = result ? RemoteLocation.AWS : RemoteLocation.ARWEAVE; | ||
if (state.remoteLocation === RemoteLocation.NONE) { | ||
// No location to sync with. We can safely return. | ||
return new Promise((resolve) => resolve(state)); | ||
} | ||
if (state.password === null || state.password === undefined) { | ||
// Request password from the user. | ||
const pass = await requestPassword( | ||
'Please create or enter the password associated with EthSign Keychain. Leave the form blank to opt out of a second layer of password encryption.', | ||
); | ||
state.password = | ||
pass !== null && pass !== undefined ? pass.toString() : null; | ||
} | ||
// Get internal MetaMask keys | ||
@@ -356,6 +434,6 @@ const keys = await getKeys(); | ||
if (!keys?.privateKey) { | ||
return state; | ||
return new Promise((resolve) => resolve(state)); | ||
} | ||
const files = await getFilesForUser(keys.publicKey); | ||
const files = await getFilesForUser(keys.publicKey, state.remoteLocation); | ||
@@ -369,3 +447,3 @@ // Get the remote state built on all remote objects | ||
state.password, | ||
state | ||
state, | ||
); | ||
@@ -378,3 +456,3 @@ | ||
keys.address, | ||
state.password | ||
state.password, | ||
); | ||
@@ -385,7 +463,8 @@ | ||
// Merge local state with remote state and get a list of changes that we need to upload remotely | ||
// const tmpState = await mergeStates(doc, state); | ||
const tmpState = await checkRemoteStatus(localState, remoteState); | ||
const addr = await getAddress(); | ||
const addr = await getEntropyAddress(); | ||
// If registry has never been initialized, initialize it | ||
// If registry has never been initialized, initialize it. | ||
// We will also use this time to create a new user on our AWS backend, if remote location is AWS. | ||
// Eventually, we will just create a new registry entry on AWS instead of a new user. | ||
if ( | ||
@@ -402,3 +481,5 @@ !tmpState.registry.publicAddress || | ||
// Add config set pending entry (for remote Arweave update) | ||
await initAws(tmpState, keys.publicKey); | ||
// Add registry set pending entry (for remote Arweave update) | ||
await arweaveMutex.runExclusive(async () => { | ||
@@ -410,3 +491,3 @@ const amidx = localState.pendingEntries.findIndex( | ||
e.payload.publicAddress === tmpState.registry.publicAddress && | ||
e.payload.publicKey === tmpState.registry.publicKey | ||
e.payload.publicKey === tmpState.registry.publicKey, | ||
); | ||
@@ -442,3 +523,3 @@ if (amidx < 0) { | ||
e.payload.address === tmpState.config.address && | ||
e.payload.encryptionMethod === tmpState.config.encryptionMethod | ||
e.payload.encryptionMethod === tmpState.config.encryptionMethod, | ||
); | ||
@@ -474,3 +555,3 @@ if (amidx < 0) { | ||
localState: EthSignKeychainState, | ||
remoteState: EthSignKeychainState | ||
remoteState: EthSignKeychainState, | ||
) { | ||
@@ -487,3 +568,3 @@ // Check registries | ||
e.payload.publicAddress === localState.registry.publicAddress && | ||
e.payload.publicKey === localState.registry.publicKey | ||
e.payload.publicKey === localState.registry.publicKey, | ||
); | ||
@@ -512,3 +593,3 @@ if (amidx < 0) { | ||
e.payload.address === localState.config.address && | ||
e.payload.encryptionMethod === localState.config.encryptionMethod | ||
e.payload.encryptionMethod === localState.config.encryptionMethod, | ||
); | ||
@@ -551,3 +632,3 @@ if (amidx < 0) { | ||
const idx = remoteState.pwState[key].logins.findIndex( | ||
(e) => e.username === entry.username | ||
(e) => e.username === entry.username, | ||
); | ||
@@ -567,3 +648,3 @@ if (idx >= 0) { | ||
e.payload.password === entry.password && | ||
e.payload.timestamp === entry.timestamp | ||
e.payload.timestamp === entry.timestamp, | ||
); | ||
@@ -589,3 +670,3 @@ if (amidx < 0) { | ||
e.payload.password === entry.password && | ||
e.payload.timestamp === entry.timestamp | ||
e.payload.timestamp === entry.timestamp, | ||
); | ||
@@ -607,3 +688,3 @@ if (amidx < 0) { | ||
const idx = localState.pwState[key].logins.findIndex( | ||
(e) => e.username === entry.username | ||
(e) => e.username === entry.username, | ||
); | ||
@@ -618,3 +699,3 @@ // The remote entry was not found locally and needs to be removed from the remote state. | ||
e.payload.username === entry.username && | ||
e.payload.timestamp === entry.timestamp | ||
e.payload.timestamp === entry.timestamp, | ||
); | ||
@@ -640,3 +721,3 @@ if (amidx < 0) { | ||
e.payload.password === entry.password && | ||
e.payload.timestamp === entry.timestamp | ||
e.payload.timestamp === entry.timestamp, | ||
); | ||
@@ -663,15 +744,17 @@ if (amidx < 0) { | ||
*/ | ||
async function processPending(remoteEmpty: boolean = false) { | ||
async function processPending(remoteEmpty = false) { | ||
return await arweaveMutex.runExclusive( | ||
async (): Promise<EthSignKeychainState> => { | ||
const state = await getEthSignKeychainState(); | ||
let shouldUpdate = false; | ||
if (remoteEmpty) { | ||
if (!state.password) { | ||
if (state.password === null || state.password === undefined) { | ||
// Request password from the user. | ||
const pass = await requestPassword( | ||
'Please create or enter the password associated with EthSign Keychain. Leave the form blank to opt out of a second layer of password encryption.' | ||
'Please create or enter the password associated with EthSign Keychain. Leave the form blank to opt out of a second layer of password encryption.', | ||
); | ||
state.password = | ||
pass && pass.toString().length > 0 ? pass.toString() : null; | ||
pass !== null && pass !== undefined ? pass.toString() : null; | ||
shouldUpdate = true; | ||
} | ||
@@ -686,17 +769,34 @@ } | ||
if (!state?.pendingEntries || state.pendingEntries.length === 0) { | ||
const res = await initAws(state, keys.publicKey); | ||
if (!res) { | ||
if (shouldUpdate) { | ||
await savePasswords(state); | ||
} | ||
return state; | ||
} | ||
state.awsInitFailure = false; | ||
shouldUpdate = true; | ||
if ( | ||
!state?.pendingEntries || | ||
state.pendingEntries.length === 0 || | ||
state.remoteLocation === RemoteLocation.NONE | ||
) { | ||
return state; | ||
} | ||
const ret: any = JSON.parse( | ||
(await getTransactionIdFromStorageUploadBatch( | ||
state.remoteLocation ?? RemoteLocation.AWS, | ||
keys.publicKey, | ||
keys.privateKey, | ||
state.password, | ||
state.pendingEntries as any | ||
)) ?? '{}' | ||
state.pendingEntries as any, | ||
)) ?? '{}', | ||
); | ||
if (ret?.transaction?.message === 'success') { | ||
state.pendingEntries = []; | ||
if (shouldUpdate || ret?.transaction?.message === 'success') { | ||
if (ret?.transaction?.message === 'success') { | ||
state.pendingEntries = []; | ||
} | ||
await savePasswords(state); | ||
@@ -706,3 +806,3 @@ } | ||
return state; | ||
} | ||
}, | ||
); | ||
@@ -725,3 +825,3 @@ } | ||
elevated: boolean, | ||
global: boolean | ||
global: boolean, | ||
) { | ||
@@ -759,3 +859,3 @@ const access = state?.credentialAccess | ||
website: string, | ||
neverSave: boolean | ||
neverSave: boolean, | ||
) { | ||
@@ -810,3 +910,3 @@ let timestamp: number; | ||
password: string, | ||
controlled: string | null | ||
controlled: string | null, | ||
) { | ||
@@ -820,3 +920,3 @@ let timestamp: number; | ||
idx = newPwState[website].logins.findIndex( | ||
(e) => e.username === username | ||
(e) => e.username === username, | ||
); | ||
@@ -893,3 +993,3 @@ } | ||
website: string, | ||
username: string | ||
username: string, | ||
) { | ||
@@ -903,3 +1003,3 @@ let timestamp: number; | ||
idx = newPwState[website].logins.findIndex( | ||
(e) => e.username === username | ||
(e) => e.username === username, | ||
); | ||
@@ -947,3 +1047,3 @@ } | ||
elevated: boolean, | ||
request: any | ||
request: any, | ||
) => { | ||
@@ -955,3 +1055,3 @@ // Make sure the current origin has explicit access to use this snap | ||
elevated, | ||
request?.params?.global ?? false | ||
request?.params?.global ?? false, | ||
); | ||
@@ -973,2 +1073,3 @@ if (!oha) { | ||
const eceisEncrypt = async (receiverAddress: string, data: string) => { | ||
return functionalityLimited(); | ||
const receiverRegistry = await registry(receiverAddress); | ||
@@ -998,2 +1099,3 @@ | ||
const eceisDecrypt = async (data: string) => { | ||
return functionalityLimited(); | ||
// Get internal MetaMask keys | ||
@@ -1020,6 +1122,6 @@ const keys = await getKeys(); | ||
* @param state - EthSignKeychainState containing password state we will be exporting. | ||
* @returns Object in the format { success: boolean, message?: string, data?: string } | ||
* @returns Object in the format { success: boolean, message?: string, data?: string }. | ||
*/ | ||
const exportState = async ( | ||
state: EthSignKeychainState | ||
state: EthSignKeychainState, | ||
): Promise<{ success: boolean; message?: string; data?: string }> => { | ||
@@ -1049,3 +1151,3 @@ if (!state?.pwState || Object.keys(state.pwState).length === 0) { | ||
nonce: uint8ArrayToString(nonce), | ||
}) | ||
}), | ||
) | ||
@@ -1056,3 +1158,3 @@ .digest('hex'); | ||
nonce, | ||
Uint8Array.from(Buffer.from(key, 'hex')) | ||
Uint8Array.from(Buffer.from(key, 'hex')), | ||
); | ||
@@ -1074,7 +1176,7 @@ | ||
* @param importedData - The imported and stringified JSON object containing nonce and data strings. | ||
* @returns Object in the format { success: boolean, message?: string, data?: EthSignKeychainState } | ||
* @returns Object in the format { success: boolean, message?: string, data?: EthSignKeychainState }. | ||
*/ | ||
const importState = async ( | ||
currentState: EthSignKeychainState, | ||
importedData: string | ||
importedData: string, | ||
): Promise<{ | ||
@@ -1089,3 +1191,3 @@ success: boolean; | ||
const result = await importCredentials(); | ||
merge = !!result; | ||
merge = Boolean(result); | ||
} | ||
@@ -1110,3 +1212,3 @@ | ||
} | ||
const key = createHash('sha256') | ||
const hashKey = createHash('sha256') | ||
.update(JSON.stringify({ password: pass, nonce: imported.nonce })) | ||
@@ -1117,3 +1219,3 @@ .digest('hex'); | ||
stringToUint8Array(imported.nonce), | ||
Uint8Array.from(Buffer.from(key, 'hex')) | ||
Uint8Array.from(Buffer.from(hashKey, 'hex')), | ||
); | ||
@@ -1137,11 +1239,3 @@ let decrypted: { | ||
const importedCredential = decrypted[key]; | ||
if (!currentState.pwState[key]) { | ||
// Current state does not contain any credentials for the imported origin | ||
currentState.pwState[key] = importedCredential; | ||
// Check global state timestamp | ||
if (currentState.timestamp < importedCredential.timestamp) { | ||
currentState.timestamp = importedCredential.timestamp; | ||
} | ||
} else { | ||
if (currentState.pwState[key]) { | ||
// Update data locally depending on the values of neverSave | ||
@@ -1170,3 +1264,3 @@ if ( | ||
currentState.pwState[key].logins = importedCredential.logins.filter( | ||
(item) => item.timestamp > currentState.pwState[key].timestamp | ||
(item) => item.timestamp > currentState.pwState[key].timestamp, | ||
); | ||
@@ -1185,3 +1279,3 @@ currentState.pwState[key].timestamp = importedCredential.timestamp; | ||
(item) => | ||
item.url === cred.url && item.username === cred.username | ||
item.url === cred.url && item.username === cred.username, | ||
); | ||
@@ -1202,2 +1296,3 @@ if (idx < 0) { | ||
} | ||
// Check current state's global timestamp | ||
@@ -1209,2 +1304,10 @@ if (currentState.timestamp < cred.timestamp) { | ||
} | ||
} else { | ||
// Current state does not contain any credentials for the imported origin | ||
currentState.pwState[key] = importedCredential; | ||
// Check global state timestamp | ||
if (currentState.timestamp < importedCredential.timestamp) { | ||
currentState.timestamp = importedCredential.timestamp; | ||
} | ||
} | ||
@@ -1249,3 +1352,6 @@ } | ||
*/ | ||
module.exports.onRpcRequest = async ({ origin, request }: any) => { | ||
export const onRpcRequest: OnRpcRequestHandler = async ({ | ||
origin, | ||
request, | ||
}: any) => { | ||
// Get the local state for this snap | ||
@@ -1274,3 +1380,3 @@ const state = await getEthSignKeychainState(); | ||
: website !== origin, | ||
request | ||
request, | ||
); | ||
@@ -1297,10 +1403,10 @@ if (!oha) { | ||
await setSyncTo(state, data); | ||
return state.remoteLocation !== null | ||
? RemoteLocation[state.remoteLocation] | ||
: null; | ||
return state.remoteLocation === null | ||
? null | ||
: RemoteLocation[state.remoteLocation]; | ||
case 'get_sync_to': | ||
return state.remoteLocation !== null | ||
? RemoteLocation[state.remoteLocation] | ||
: null; | ||
return state.remoteLocation === null | ||
? null | ||
: RemoteLocation[state.remoteLocation]; | ||
@@ -1317,3 +1423,3 @@ case 'set_neversave': | ||
password, | ||
controlled ? origin : null | ||
controlled ? origin : null, | ||
); | ||
@@ -1346,5 +1452,4 @@ return 'OK'; | ||
return { success: true, message: 'OK' }; | ||
} else { | ||
return ret; | ||
} | ||
return ret; | ||
@@ -1351,0 +1456,0 @@ default: |
Sorry, the diff of this file is too big to display
No contributors or author data
MaintenancePackage does not specify a list of contributors or an author in package.json.
Found 1 instance in 1 package
1
2138759
55802