New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

keychain-snap

Package Overview
Dependencies
Maintainers
1
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

keychain-snap - npm Package Compare versions

Comparing version 0.3.3 to 0.3.4

7

package.json
{
"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 @@ },

// 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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc