Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: unify import/export private key format #15415

Merged
merged 11 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/angry-rocks-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@mysten/sui.js': patch
---

deprecate ExportedKeypair
5 changes: 5 additions & 0 deletions .changeset/two-olives-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@mysten/sui.js': major
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if we do major updates for the sdk - noting for @hayes-mysten or @Jordan-Mysten to confirm

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes everything should be a minor until we get to 1.0

---

Use Bech32 instead of Hex for private key encoding for import and export
1 change: 1 addition & 0 deletions apps/wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@
"@sentry/browser": "^7.61.0",
"@tanstack/react-query": "^5.0.0",
"@tanstack/react-query-persist-client": "^4.29.25",
"bech32": "^2.0.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this might not be needed anymore?

"bignumber.js": "^9.1.1",
"bootstrap-icons": "^1.10.5",
"buffer": "^6.0.3",
Expand Down
3 changes: 1 addition & 2 deletions apps/wallet/src/background/accounts/Account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import { type Serializable } from '_src/shared/cryptography/keystore';
import {
toSerializedSignature,
type ExportedKeypair,
type Keypair,
type SerializedSignature,
} from '@mysten/sui.js/cryptography';
Expand Down Expand Up @@ -186,7 +185,7 @@ export function isSigningAccount(account: any): account is SigningAccount {

export interface KeyPairExportableAccount {
readonly exportableKeyPair: true;
exportKeyPair(password: string): Promise<ExportedKeypair>;
exportKeyPair(password: string): Promise<string>;
}

export function isKeyPairExportableAccount(account: any): account is KeyPairExportableAccount {
Expand Down
18 changes: 10 additions & 8 deletions apps/wallet/src/background/accounts/ImportedAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
// SPDX-License-Identifier: Apache-2.0

import { decrypt, encrypt } from '_src/shared/cryptography/keystore';
import { fromExportedKeypair } from '_src/shared/utils/from-exported-keypair';
import { type ExportedKeypair } from '@mysten/sui.js/cryptography';
import {
fromExportedKeypair,
type LegacyExportedKeyPair,
} from '_src/shared/utils/from-exported-keypair';

import {
Account,
Expand All @@ -14,8 +16,8 @@ import {
type SigningAccount,
} from './Account';

type SessionStorageData = { keyPair: ExportedKeypair };
type EncryptedData = { keyPair: ExportedKeypair };
type SessionStorageData = { keyPair: LegacyExportedKeyPair | string };
type EncryptedData = { keyPair: LegacyExportedKeyPair | string };

export interface ImportedAccountSerialized extends SerializedAccount {
type: 'imported';
Expand Down Expand Up @@ -43,7 +45,7 @@ export class ImportedAccount
readonly exportableKeyPair = true;

static async createNew(inputs: {
keyPair: ExportedKeypair;
keyPair: string;
password: string;
}): Promise<Omit<ImportedAccountSerialized, 'id'>> {
const keyPair = fromExportedKeypair(inputs.keyPair);
Expand Down Expand Up @@ -118,16 +120,16 @@ export class ImportedAccount
return this.generateSignature(data, keyPair);
}

async exportKeyPair(password: string): Promise<ExportedKeypair> {
async exportKeyPair(password: string): Promise<string> {
const { encrypted } = await this.getStoredData();
const { keyPair } = await decrypt<EncryptedData>(password, encrypted);
return keyPair;
return fromExportedKeypair(keyPair, true).getSecretKey();
}

async #getKeyPair() {
const ephemeralData = await this.getEphemeralValue();
if (ephemeralData) {
return fromExportedKeypair(ephemeralData.keyPair);
return fromExportedKeypair(ephemeralData.keyPair, true);
}
return null;
}
Expand Down
10 changes: 5 additions & 5 deletions apps/wallet/src/background/accounts/MnemonicAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import { fromExportedKeypair } from '_src/shared/utils/from-exported-keypair';
import { type ExportedKeypair, type Keypair } from '@mysten/sui.js/cryptography';
import { type Keypair } from '@mysten/sui.js/cryptography';

import { MnemonicAccountSource } from '../account-sources/MnemonicAccountSource';
import {
Expand Down Expand Up @@ -34,7 +34,7 @@ export function isMnemonicSerializedUiAccount(
return account.type === 'mnemonic-derived';
}

type SessionStorageData = { keyPair: ExportedKeypair };
type SessionStorageData = { keyPair: string };

export class MnemonicAccount
extends Account<MnemonicSerializedAccount, SessionStorageData>
Expand Down Expand Up @@ -93,7 +93,7 @@ export class MnemonicAccount
await mnemonicSource.unlock(password);
}
await this.setEphemeralValue({
keyPair: (await mnemonicSource.deriveKeyPair(derivationPath)).export(),
keyPair: (await mnemonicSource.deriveKeyPair(derivationPath)).getSecretKey(),
});
await this.onUnlocked();
}
Expand Down Expand Up @@ -138,11 +138,11 @@ export class MnemonicAccount
return this.getCachedData().then(({ sourceID }) => sourceID);
}

async exportKeyPair(password: string): Promise<ExportedKeypair> {
async exportKeyPair(password: string): Promise<string> {
const { derivationPath } = await this.getStoredData();
const mnemonicSource = await this.#getMnemonicSource();
await mnemonicSource.unlock(password);
return (await mnemonicSource.deriveKeyPair(derivationPath)).export();
return (await mnemonicSource.deriveKeyPair(derivationPath)).getSecretKey();
}

async #getKeyPair() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { deobfuscate, obfuscate } from '_src/shared/cryptography/keystore';
import { fromExportedKeypair } from '_src/shared/utils/from-exported-keypair';
import {
toSerializedSignature,
type ExportedKeypair,
type PublicKey,
type SerializedSignature,
} from '@mysten/sui.js/cryptography';
Expand Down Expand Up @@ -38,7 +37,7 @@ function serializeNetwork(network: NetworkEnvType): SerializedNetwork {
}

type CredentialData = {
ephemeralKeyPair: ExportedKeypair;
ephemeralKeyPair: string;
proofs?: PartialZkLoginSignature;
minEpoch: number;
maxEpoch: number;
Expand Down Expand Up @@ -278,7 +277,7 @@ export class ZkLoginAccount
const ephemeralValue = (await this.getEphemeralValue()) || {};
const activeNetwork = await networkEnv.getActiveNetwork();
const credentialsData: CredentialData = {
ephemeralKeyPair: ephemeralKeyPair.export(),
ephemeralKeyPair: ephemeralKeyPair.getSecretKey(),
minEpoch: Number(epoch),
maxEpoch,
network: activeNetwork,
Expand Down
11 changes: 7 additions & 4 deletions apps/wallet/src/background/legacy-accounts/LegacyVault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@ import {
toEntropy,
validateEntropy,
} from '_shared/utils/bip39';
import { fromExportedKeypair } from '_shared/utils/from-exported-keypair';
import { mnemonicToSeedHex, type ExportedKeypair, type Keypair } from '@mysten/sui.js/cryptography';
import {
fromExportedKeypair,
type LegacyExportedKeyPair,
} from '_shared/utils/from-exported-keypair';
import { mnemonicToSeedHex, type Keypair } from '@mysten/sui.js/cryptography';

import { getFromLocalStorage } from '../storage-utils';

type StoredData = string | { v: 1 | 2; data: string };

type V2DecryptedDataType = {
entropy: string;
importedKeypairs: ExportedKeypair[];
importedKeypairs: LegacyExportedKeyPair[];
qredoTokens?: Record<string, string>;
mnemonicSeedHex?: string;
};
Expand Down Expand Up @@ -53,7 +56,7 @@ export class LegacyVault {
mnemonicSeedHex: storedMnemonicSeedHex,
} = await decrypt<V2DecryptedDataType>(password, data.data);
entropy = toEntropy(entropySerialized);
keypairs = importedKeypairs.map(fromExportedKeypair);
keypairs = importedKeypairs.map((aKeyPair) => fromExportedKeypair(aKeyPair, true));
if (storedTokens) {
qredoTokens = new Map(Object.entries(storedTokens));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ async function makeMnemonicAccounts(password: string, vault: LegacyVault) {
async function makeImportedAccounts(password: string, vault: LegacyVault) {
return Promise.all(
vault.importedKeypairs.map((keyPair) =>
ImportedAccount.createNew({ password, keyPair: keyPair.export() }),
ImportedAccount.createNew({ password, keyPair: keyPair.getSecretKey() }),
),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { type AccountSourceSerializedUI } from '_src/background/account-sources/
import { type SerializedUIAccount } from '_src/background/accounts/Account';
import { type ZkLoginProvider } from '_src/background/accounts/zklogin/providers';
import { type Status } from '_src/background/legacy-accounts/storage-migration';
import { type ExportedKeypair, type SerializedSignature } from '@mysten/sui.js/cryptography';
import { type SerializedSignature } from '@mysten/sui.js/cryptography';

import { isBasePayload } from './BasePayload';
import type { Payload } from './Payload';
Expand All @@ -32,7 +32,7 @@ type MethodPayloads = {
unlockAccountSourceOrAccount: { id: string; password?: string };
createAccounts:
| { type: 'mnemonic-derived'; sourceID: string }
| { type: 'imported'; keyPair: ExportedKeypair; password: string }
| { type: 'imported'; keyPair: string; password: string }
| {
type: 'ledger';
accounts: { publicKey: string; derivationPath: string; address: string }[];
Expand Down Expand Up @@ -61,7 +61,7 @@ type MethodPayloads = {
setAutoLockMinutes: { minutes: number | null };
notifyUserActive: {};
getAccountKeyPair: { accountID: string; password: string };
getAccountKeyPairResponse: { accountID: string; keyPair: ExportedKeypair };
getAccountKeyPairResponse: { accountID: string; keyPair: string };
resetPassword: {
password: string;
recoveryData: PasswordRecoveryData[];
Expand Down
40 changes: 33 additions & 7 deletions apps/wallet/src/shared/utils/from-exported-keypair.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,44 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { type ExportedKeypair, type Keypair } from '@mysten/sui.js/cryptography';
import { type Keypair, type SignatureScheme } from '@mysten/sui.js/cryptography';
import {
decodeSuiPrivateKey,
LEGACY_PRIVATE_KEY_SIZE,
PRIVATE_KEY_SIZE,
} from '@mysten/sui.js/cryptography/keypair';
import { Ed25519Keypair } from '@mysten/sui.js/keypairs/ed25519';
import { Secp256k1Keypair } from '@mysten/sui.js/keypairs/secp256k1';
import { Secp256r1Keypair } from '@mysten/sui.js/keypairs/secp256r1';
import { fromB64 } from '@mysten/sui.js/utils';

const PRIVATE_KEY_SIZE = 32;
const LEGACY_PRIVATE_KEY_SIZE = 64;
export function fromExportedKeypair(keypair: ExportedKeypair): Keypair {
const secretKey = fromB64(keypair.privateKey);
/**
* Wallet stored data might contain imported accounts with their keys stored in the previous format.
* Using this type to type-check it.
*/
export type LegacyExportedKeyPair = {
schema: SignatureScheme;
privateKey: string;
};

switch (keypair.schema) {
export function fromExportedKeypair(
secret: LegacyExportedKeyPair | string,
legacySupport = false,
): Keypair {
let schema;
let secretKey;
if (typeof secret === 'object') {
if (!legacySupport) {
throw new Error('Invalid type of secret key. A string value was expected.');
}
secretKey = fromB64(secret.privateKey);
schema = secret.schema;
} else {
const decoded = decodeSuiPrivateKey(secret);
schema = decoded.schema;
secretKey = decoded.secretKey;
}
switch (schema) {
case 'ED25519':
let pureSecretKey = secretKey;
if (secretKey.length === LEGACY_PRIVATE_KEY_SIZE) {
Expand All @@ -25,6 +51,6 @@ export function fromExportedKeypair(keypair: ExportedKeypair): Keypair {
case 'Secp256r1':
return Secp256r1Keypair.fromSecretKey(secretKey);
default:
throw new Error(`Invalid keypair schema ${keypair.schema}`);
throw new Error(`Invalid keypair schema ${schema}`);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

import { type ZkLoginProvider } from '_src/background/accounts/zklogin/providers';
import { type Wallet } from '_src/shared/qredo-api';
import { type ExportedKeypair } from '@mysten/sui.js/cryptography';
import {
createContext,
useCallback,
Expand All @@ -19,7 +18,7 @@ export type AccountsFormValues =
| { type: 'new-mnemonic' }
| { type: 'import-mnemonic'; entropy: string }
| { type: 'mnemonic-derived'; sourceID: string }
| { type: 'imported'; keyPair: ExportedKeypair }
| { type: 'imported'; keyPair: string }
| {
type: 'ledger';
accounts: { publicKey: string; derivationPath: string; address: string }[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { z } from 'zod';
import { privateKeyValidation } from '../../helpers/validation/privateKeyValidation';
import { Form } from '../../shared/forms/Form';
import { TextAreaField } from '../../shared/forms/TextAreaField';
import Alert from '../alert';

const formSchema = z.object({
privateKey: privateKeyValidation,
Expand All @@ -29,12 +30,20 @@ export function ImportPrivateKeyForm({ onSubmit }: ImportPrivateKeyFormProps) {
const {
register,
formState: { isSubmitting, isValid },
watch,
} = form;
const navigate = useNavigate();

const privateKey = watch('privateKey');
const isHexadecimal = isValid && !privateKey.startsWith('suiprivkey');
return (
<Form className="flex flex-col h-full" form={form} onSubmit={onSubmit}>
<Form className="flex flex-col h-full gap-2" form={form} onSubmit={onSubmit}>
<TextAreaField label="Enter Private Key" rows={4} {...register('privateKey')} />
{isHexadecimal ? (
<Alert mode="warning">
Importing Hex encoded Private Key will soon be deprecated, please use Bech32 encoded
private key that starts with "suiprivkey" instead
</Alert>
) : null}
<div className="flex gap-2.5 mt-auto">
<Button variant="outline" size="tall" text="Cancel" onClick={() => navigate(-1)} />
<Button
Expand Down
Loading
Loading