Skip to content

Commit

Permalink
Merge branch 'master' into db/feat/automate-typedoc-link-regeneration
Browse files Browse the repository at this point in the history
  • Loading branch information
arboleya authored Aug 11, 2023
2 parents c2e41b7 + 9282391 commit cbb929f
Show file tree
Hide file tree
Showing 28 changed files with 11,988 additions and 5,358 deletions.
5 changes: 5 additions & 0 deletions .changeset/poor-pumpkins-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fuel-ts/wallet": minor
---

support encrypt and decrypt json wallets
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Wallet } from 'fuels';

describe(__filename, () => {
it('should successfully encrypt wallet', async () => {
// #region encrypting-and-decrypting-json-wallets-1
// #context import fs from 'fs';
// #context import { Wallet } from 'fuels';

const wallet = Wallet.generate();

const password = 'my-password';

const jsonWallet = await wallet.encrypt(password);

// #context fs.writeFileSync('secure-path/my-wallet.json', jsonWallet);
// #endregion encrypting-and-decrypting-json-wallets-1

expect(jsonWallet).toBeDefined();
});

it('should successfully decrypt a wallet', async () => {
const jsonWallet = await Wallet.generate().encrypt('my-password');
// #region encrypting-and-decrypting-json-wallets-2
// #context import fs from 'fs';
// #context import { Wallet } from 'fuels';

// #context const jsonWallet = fs.readFileSync('secure-path/my-wallet.json', 'utf-8');

const password = 'my-password';

const decryptedWallet = await Wallet.fromEncryptedJson(jsonWallet, password);

const myBalance = await decryptedWallet.getBalance();
// #endregion encrypting-and-decrypting-json-wallets-2

expect(myBalance).toBeDefined();
});
});
4 changes: 2 additions & 2 deletions apps/docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ export default defineConfig({
link: '/guide/wallets/checking-balances-and-coins',
},
{
text: 'Encrypting And Storing Wallets',
link: '/guide/wallets/encrypting-and-storing-wallets',
text: 'Encrypting and Decrypting JSON Wallets',
link: '/guide/wallets/encrypting-and-decrypting-json-wallets',
},
{
text: 'Mnemonic Wallet',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Encrypting and Decrypting JSON Wallet

JSON wallets are a standardized way of storing wallets securely. They follow a specific schema and are encrypted using a password. This makes it easier to manage multiple wallets and securely store them on disk. This guide will take you through the process of encrypting and decrypting JSON wallets using the Typescript SDK.

## Encrypting a Wallet

We will be calling `encrypt` from the `WalletUnlocked` instance which will take a password as the argument. It will encrypt the private key using a cipher and returns the JSON keystore wallet. You can then securely store this JSON wallet.

Here is an example of how you can accomplish this:

<<< @/../../docs-snippets/src/guide/wallets/encrypting-and-decrypting-json-wallets.test.ts#encrypting-and-decrypting-json-wallets-1{ts:line-numbers}

Please note that `encrypt` must be called within an instance of `WalletUnlocked`. This instance can only be achieved through passing a private key or mnemonic phrase to a locked wallet.

## Decrypting a Wallet

To decrypt the JSON wallet and retrieve your private key, you can call `fromEncryptedJson` on a `Wallet` instance. It takes the encrypted JSON wallet and the password as its arguments, and returns the decrypted wallet.

Here is an example:

<<< @/../../docs-snippets/src/guide/wallets/encrypting-and-decrypting-json-wallets.test.ts#encrypting-and-decrypting-json-wallets-2{ts:line-numbers}

In this example, `decryptedWallet` is an instance of `WalletUnlocked` class, now available for use.

## Important

Remember to securely store your encrypted JSON wallet and password. If you lose them, there will be no way to recover your wallet. For security reasons, avoid sharing your private key, encrypted JSON wallet or password with anyone.
19 changes: 0 additions & 19 deletions apps/docs/src/guide/wallets/encrypting-and-storing-wallets.md

This file was deleted.

3 changes: 2 additions & 1 deletion packages/crypto/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"license": "Apache-2.0",
"dependencies": {
"@ethersproject/bytes": "^5.7.0",
"@ethersproject/pbkdf2": "^5.7.0"
"@ethersproject/pbkdf2": "^5.7.0",
"ethereum-cryptography": "^2.1.2"
}
}
57 changes: 57 additions & 0 deletions packages/crypto/src/browser/encryptJsonWalletData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { CryptoApi } from '../types';

import { crypto } from './crypto';

export const encryptJsonWalletData: CryptoApi['encryptJsonWalletData'] = async (
data: Uint8Array,
key: Uint8Array,
iv: Uint8Array
): Promise<Uint8Array> => {
const subtle = crypto.subtle;
const keyBuffer = new Uint8Array(key.subarray(0, 16));
const ivBuffer = iv;
const dataBuffer = data;

const cryptoKey = await subtle.importKey(
'raw',
keyBuffer,
{ name: 'AES-CTR', length: 128 },
false,
['encrypt', 'decrypt']
);

const encrypted = (await subtle.encrypt(
{ name: 'AES-CTR', counter: ivBuffer, length: 128 },
cryptoKey,
dataBuffer
)) as ArrayBuffer;

return new Uint8Array(encrypted);
};

export const decryptJsonWalletData: CryptoApi['decryptJsonWalletData'] = async (
data: Uint8Array,
key: Uint8Array,
iv: Uint8Array
): Promise<Uint8Array> => {
const subtle = crypto.subtle;
const keyBuffer = new Uint8Array(key.subarray(0, 16)).buffer;
const ivBuffer = new Uint8Array(iv).buffer;
const dataBuffer = new Uint8Array(data).buffer;

const cryptoKey = await subtle.importKey(
'raw',
keyBuffer,
{ name: 'AES-CTR', length: 128 },
false,
['encrypt', 'decrypt']
);

const decrypted = (await subtle.decrypt(
{ name: 'AES-CTR', counter: ivBuffer, length: 128 },
cryptoKey,
dataBuffer
)) as ArrayBuffer;

return new Uint8Array(decrypted);
};
6 changes: 6 additions & 0 deletions packages/crypto/src/browser/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { scrypt, keccak256 } from '../shared';
import type { CryptoApi } from '../types';

import { decrypt, encrypt, keyFromPassword } from './aes-ctr';
import { bufferFromString } from './bufferFromString';
import { decryptJsonWalletData, encryptJsonWalletData } from './encryptJsonWalletData';
import { randomBytes } from './randomBytes';
import { stringFromBuffer } from './stringFromBuffer';

Expand All @@ -12,6 +14,10 @@ const api: CryptoApi = {
encrypt,
keyFromPassword,
randomBytes,
scrypt,
keccak256,
decryptJsonWalletData,
encryptJsonWalletData,
};

export default api;
4 changes: 4 additions & 0 deletions packages/crypto/src/index.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@ export const {
keyFromPassword,
randomBytes,
stringFromBuffer,
scrypt,
keccak256,
decryptJsonWalletData,
encryptJsonWalletData,
} = cryptoApi;
4 changes: 4 additions & 0 deletions packages/crypto/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@ export const {
keyFromPassword,
randomBytes,
stringFromBuffer,
scrypt,
keccak256,
decryptJsonWalletData,
encryptJsonWalletData,
} = cryptoApi;
17 changes: 17 additions & 0 deletions packages/crypto/src/node/encryptJsonWalletData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import crypto from 'crypto';

export async function encryptJsonWalletData(data: Uint8Array, key: Uint8Array, iv: Uint8Array) {
const cipher = await crypto.createCipheriv('aes-128-ctr', key.subarray(0, 16), iv);

const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);

return new Uint8Array(encrypted);
}

export async function decryptJsonWalletData(data: Uint8Array, key: Uint8Array, iv: Uint8Array) {
const decipher = crypto.createDecipheriv('aes-128-ctr', key.subarray(0, 16), iv);

const decrypted = await Buffer.concat([decipher.update(data), decipher.final()]);

return new Uint8Array(decrypted);
}
6 changes: 6 additions & 0 deletions packages/crypto/src/node/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { scrypt, keccak256 } from '../shared';
import type { CryptoApi } from '../types';

import { decrypt, encrypt, keyFromPassword } from './aes-ctr';
import { bufferFromString } from './bufferFromString';
import { decryptJsonWalletData, encryptJsonWalletData } from './encryptJsonWalletData';
import { randomBytes } from './randomBytes';
import { stringFromBuffer } from './stringFromBuffer';

Expand All @@ -12,6 +14,10 @@ const api: CryptoApi = {
encrypt,
keyFromPassword,
randomBytes,
scrypt,
keccak256,
decryptJsonWalletData,
encryptJsonWalletData,
};

export default api;
2 changes: 2 additions & 0 deletions packages/crypto/src/shared/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './scrypt';
export * from './keccak256';
20 changes: 20 additions & 0 deletions packages/crypto/src/shared/keccak256.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as ethereumCryptography from 'ethereum-cryptography/keccak';

import { bufferFromString } from '..';

import { keccak256 } from './keccak256';

describe('keccak256', () => {
afterEach(jest.restoreAllMocks);

it('hashes using keccak256', () => {
const data = bufferFromString('hashedKey');

const mock = jest.spyOn(ethereumCryptography, 'keccak256').mockImplementationOnce(() => data);

const hashedKey = keccak256(data);

expect(mock).toBeCalledTimes(1);
expect(hashedKey).toEqual(data);
});
});
3 changes: 3 additions & 0 deletions packages/crypto/src/shared/keccak256.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { keccak256 as keccak } from 'ethereum-cryptography/keccak';

export const keccak256 = (data: Uint8Array): Uint8Array => keccak(data);
35 changes: 35 additions & 0 deletions packages/crypto/src/shared/scrypt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as ethereumCryptography from 'ethereum-cryptography/scrypt';

import { bufferFromString } from '..';
import type { IScryptParams } from '../types';

import { scrypt } from './scrypt';

describe('scrypt', () => {
afterEach(jest.restoreAllMocks);

it('hashes using scrypt', () => {
const mockedHashedKey = bufferFromString('hashedKey');

const mock = jest
.spyOn(ethereumCryptography, 'scryptSync')
.mockImplementationOnce(() => mockedHashedKey);

const password = bufferFromString('password');
const salt = bufferFromString('salt');

const params: IScryptParams = {
dklen: 32,
n: 2,
p: 4,
password,
r: 2,
salt,
};

const hashedKey = scrypt(params);

expect(mock).toBeCalledTimes(1);
expect(hashedKey).toEqual(mockedHashedKey);
});
});
11 changes: 11 additions & 0 deletions packages/crypto/src/shared/scrypt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { scryptSync as ethCryScrypt } from 'ethereum-cryptography/scrypt';

import type { IScryptParams } from '../types';

export const scrypt = (params: IScryptParams): Uint8Array => {
const { password, salt, n, p, r, dklen } = params;

const derivedKey = ethCryScrypt(password, salt, n, r, p, dklen);

return derivedKey;
};
13 changes: 13 additions & 0 deletions packages/crypto/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ export interface Keystore {
salt: string;
}

export interface IScryptParams {
password: Uint8Array;
salt: Uint8Array;
n: number;
p: number;
r: number;
dklen: number;
}

export type Encoding = 'utf-8' | 'base64' | 'hex';

export interface CryptoApi {
Expand All @@ -13,4 +22,8 @@ export interface CryptoApi {
keyFromPassword(password: string, saltBuffer: Uint8Array): Uint8Array;
stringFromBuffer(buffer: Uint8Array, encoding?: Encoding): string;
randomBytes(length: number): Uint8Array;
scrypt(params: IScryptParams): Uint8Array;
keccak256(data: Uint8Array): Uint8Array;
encryptJsonWalletData(data: Uint8Array, key: Uint8Array, iv: Uint8Array): Promise<Uint8Array>;
decryptJsonWalletData(data: Uint8Array, key: Uint8Array, iv: Uint8Array): Promise<Uint8Array>;
}
18 changes: 18 additions & 0 deletions packages/crypto/test/encryptJsonWalletData.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { envs } from './envs';

describe('encryptJsonWalletData', () => {
it.each(envs)(
'should encrypt and decrypt json wallet data correctly in %s environment',
async ({ encryptJsonWalletData, decryptJsonWalletData, randomBytes }) => {
const testData = new Uint8Array([104, 101, 108, 108, 111]);
const testKey = randomBytes(16);
const testIv = randomBytes(16);

const encryptedData = await encryptJsonWalletData(testData, testKey, testIv);
expect(encryptedData).not.toEqual(testData); // ensure data was encrypted

const decryptedData = await decryptJsonWalletData(encryptedData, testKey, testIv);
expect(decryptedData).toEqual(testData); // ensure data was decrypted correctly
}
);
});
10 changes: 6 additions & 4 deletions packages/wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,13 @@
"@fuel-ts/providers": "workspace:*",
"@fuel-ts/signer": "workspace:*",
"@fuel-ts/transactions": "workspace:*",
"@fuels/vm-asm": "0.34.1"
"@fuel-ts/utils": "workspace:*",
"@fuels/vm-asm": "0.34.1",
"@fuel-ts/crypto": "workspace:*",
"uuid": "^9.0.0"
},
"devDependencies": {
"@fuel-ts/address": "workspace:*",
"@fuel-ts/crypto": "workspace:*",
"@fuel-ts/testcases": "workspace:*"
"@fuel-ts/testcases": "workspace:*",
"@types/uuid": "^9.0.1"
}
}
Loading

0 comments on commit cbb929f

Please sign in to comment.