Skip to content

Commit

Permalink
fix!: Disable legacy triplesec support for mnemonic encryption
Browse files Browse the repository at this point in the history
BREAKING CHANGE:
Support for encrypting/decrypting mnemonics using `triplesec` has been removed.
  • Loading branch information
janniks committed Jun 10, 2024
1 parent 58d13e2 commit 5750984
Show file tree
Hide file tree
Showing 10 changed files with 29 additions and 188 deletions.
8 changes: 8 additions & 0 deletions .github/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- [`serialize` methods](#serialize-methods)
- [Asset Helper Methods](#asset-helper-methods)
- [CLI](#cli)
- [Triplesec](#triplesec)
- [Stacks.js (\<=4.x.x) → (5.x.x)](#stacksjs-4xx--5xx)
- [Breaking Changes](#breaking-changes-1)
- [Buffer to Uint8Array](#buffer-to-uint8array)
Expand All @@ -32,6 +33,7 @@
- The `serializeXyz` methods were changed to return `string` (hex-encoded) instead of `Uint8Array`. Compatible `serializeXzyBytes` methods were added to ease the migration. [Read more...](#serialize-methods)
- The `AssetInfo` type was renamed to `Asset` for accuracy. The `Asset` helper methods were also renamed to to remove the `Info` suffix. [Read more...](#asset-helper-methods)
- Remove legacy CLI methods. [Read more...](#cli)
- Disable legacy `triplesec` mnemonic encryption support. [Read more...](#triplesec)

### Stacks Network

Expand Down Expand Up @@ -190,6 +192,12 @@ The following interfaces and methods were renamed:

- Removed the `authenticator` method for legacy Blockstack authentication.

### Triplesec

Support for encrypting/decrypting mnemonics with `triplesec` was removed.
This impacts the methods: `decrypt`, `decryptMnemonic`, and `decryptLegacy`.
Make sure to update your code to if mnemonics are stored somewhere encrypted using the legacy method.

## Stacks.js (&lt;=4.x.x) → (5.x.x)

### Breaking Changes
Expand Down
71 changes: 5 additions & 66 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"lint:fix": "eslint \"packages/**/src/**/*.{ts,tsx}\" -f unix --fix",
"lint:prettier": "prettier --check \"packages/**/src/**/*.{ts,tsx|!(d.ts)}\" *.js --ignore-path .gitignore",
"lint:prettier:fix": "prettier --write \"packages/**/src/**/*.{ts,tsx|!(d.ts)}\" *.js --ignore-path .gitignore",
"madge": "madge --circular --extensions ts --exclude '(triplesec|d).ts' packages/",
"madge": "madge --circular --extensions ts --exclude 'd.ts' packages/",
"pack": "lerna run pack",
"test": "lerna run test",
"typecheck": "lerna run typecheck --parallel --no-bail --stream",
Expand Down
4 changes: 1 addition & 3 deletions packages/encryption/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,14 @@
"@types/bs58check": "^2.1.0",
"@types/elliptic": "^6.4.12",
"@types/sha.js": "^2.4.0",
"@types/triplesec": "^3.0.0",
"bitcoinjs-lib": "^5.2.0",
"bs58check": "^2.1.2",
"crypto-browserify": "^3.12.0",
"elliptic": "^6.5.4",
"jsontokens": "^4.0.1",
"process": "^0.11.10",
"rimraf": "^3.0.2",
"stream-browserify": "^3.0.0",
"triplesec": "^4.0.3"
"stream-browserify": "^3.0.0"
},
"sideEffects": false,
"typings": "dist/index.d.ts",
Expand Down
67 changes: 8 additions & 59 deletions packages/encryption/src/wallet.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,16 @@
// https://github.com/paulmillr/scure-bip39
// Secure, audited & minimal implementation of BIP39 mnemonic phrases.
import { validateMnemonic, mnemonicToEntropy, entropyToMnemonic } from '@scure/bip39';
import { entropyToMnemonic, mnemonicToEntropy, validateMnemonic } from '@scure/bip39';
// Word lists not imported by default as that would increase bundle sizes too much as in case of bitcoinjs/bip39
// Use default english world list similiar to bitcoinjs/bip39
// Backward compatible with bitcoinjs/bip39 dependency
// Very small in size as compared to bitcoinjs/bip39 wordlist
// Reference: https://github.com/paulmillr/scure-bip39
import { wordlist } from '@scure/bip39/wordlists/english';
import { randomBytes, GetRandomBytes } from './cryptoRandom';
import { createSha2Hash } from './sha2Hash';
import { bytesToHex, concatBytes, equals, hexToBytes } from '@stacks/common';
import { createCipher } from './aesCipher';
import { createPbkdf2 } from './pbkdf2';
import { TriplesecDecryptSignature } from './cryptoUtils';
import {
bytesToHex,
bytesToUtf8,
concatBytes,
equals,
hexToBytes,
utf8ToBytes,
} from '@stacks/common';
import { GetRandomBytes, randomBytes } from './cryptoRandom';
import { hmacSha256 } from './ec';
import { createPbkdf2 } from './pbkdf2';
import { createSha2Hash } from './sha2Hash';

/**
* Encrypt a raw mnemonic phrase to be password protected
Expand Down Expand Up @@ -67,8 +57,7 @@ export async function encryptMnemonic(
const hmacPayload = concatBytes(salt, cipherText);
const hmacDigest = hmacSha256(macKey, hmacPayload);

const payload = concatBytes(salt, hmacDigest, cipherText);
return payload;
return concatBytes(salt, hmacDigest, cipherText);
}

// Used to distinguish bad password during decrypt vs invalid format
Expand Down Expand Up @@ -122,57 +111,17 @@ async function decryptMnemonicBytes(dataBytes: Uint8Array, password: string): Pr
return mnemonic;
}

/**
* Decrypt legacy triplesec keys
* @param {Uint8Array} dataBytes - The encrypted key
* @param {String} password - Password for data
* @return {Promise<BuUint8Arrayffer>} Decrypted seed
* @ignore
*/
function decryptLegacy(
dataBytes: Uint8Array,
password: string,
triplesecDecrypt?: TriplesecDecryptSignature
): Promise<Uint8Array> {
return new Promise<Uint8Array>((resolve, reject) => {
if (!triplesecDecrypt) {
reject(new Error('The `triplesec.decrypt` function must be provided'));
}
triplesecDecrypt!(
{
key: utf8ToBytes(password),
data: dataBytes,
},
(err, plaintextBytes) => {
if (!err) {
resolve(plaintextBytes!);
} else {
reject(err);
}
}
);
});
}

/**
* Decrypt an encrypted mnemonic phrase with a password.
* Legacy triplesec encrypted payloads are also supported.
* @param data - Bytes or hex-encoded string of the encrypted mnemonic
* @param password - Password for data
* @return {string} the raw mnemonic phrase
* @ignore
*/
export async function decryptMnemonic(
data: string | Uint8Array,
password: string,
triplesecDecrypt?: TriplesecDecryptSignature
password: string
): Promise<string> {
const dataBytes = typeof data === 'string' ? hexToBytes(data) : data;
try {
return await decryptMnemonicBytes(dataBytes, password);
} catch (error) {
if (error instanceof PasswordError) throw error;
const data = await decryptLegacy(dataBytes, password, triplesecDecrypt);
return bytesToUtf8(data);
}
return await decryptMnemonicBytes(dataBytes, password);
}
39 changes: 4 additions & 35 deletions packages/encryption/tests/encryption.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,28 +494,11 @@ test('encryptMnemonic & decryptMnemonic', async () => {
'0579649dfbc9e664073ba614fac180d3dc237b21eba57f9aee5702ba819fe17a0752c4dc7' +
'94884c9e75eb60da875f778bbc1aaca1bd373ea3';

const legacyPhrase =
'vivid oxygen neutral wheat find thumb cigar wheel ' + 'board kiwi portion business';
const legacyPassword = 'supersecret';
const legacyEncrypted =
'1c94d7de0000000304d583f007c71e6e5fef354c046e8c64b1' +
'adebd6904dcb007a1222f07313643873455ab2a3ab3819e99d518cc7d33c18bde02494aa' +
'74efc35a8970b2007b2fc715f6067cee27f5c92d020b1806b0444994aab80050a6732131' +
'd2947a51bacb3952fb9286124b3c2b3196ff7edce66dee0dbd9eb59558e0044bddb3a78f' +
'48a66cf8d78bb46bb472bd2d5ec420c831fc384293252459524ee2d668869f33c586a944' +
'67d0ce8671260f4cc2e87140c873b6ca79fb86c6d77d134d7beb2018845a9e71e6c7ecde' +
'dacd8a676f1f873c5f9c708cc6070642d44d2505aa9cdba26c50ad6f8d3e547fb0cba710' +
'a7f7be54ff7ea7e98a809ddee5ef85f6f259b3a17a8d8dbaac618b80fe266a1e63ec19e4' +
'76bee9177b51894e';

// eslint-disable-next-line @typescript-eslint/no-var-requires
const { triplesecDecrypt } = require('../../wallet-sdk/src/triplesec');

// Test encryption -> decryption. Can't be done with hard-coded values
// due to random salt.
await encryptMnemonic(rawPhrase, rawPassword)
.then(
encoded => decryptMnemonic(bytesToHex(encoded), rawPassword, triplesecDecrypt),
encoded => decryptMnemonic(bytesToHex(encoded), rawPassword),
err => {
fail(`Should encrypt mnemonic phrase, instead errored: ${err}`);
}
Expand All @@ -542,26 +525,12 @@ test('encryptMnemonic & decryptMnemonic', async () => {

// // Test decryption with mocked randomBytes generator to use same salt
try {
const decoded = await decryptMnemonic(
hexToBytes(encryptedPhrase),
rawPassword,
triplesecDecrypt
);
const decoded = await decryptMnemonic(hexToBytes(encryptedPhrase), rawPassword);
expect(decoded === rawPhrase).toEqual(true);
} catch (err) {
fail(`Should have decrypted phrase with deterministic salt, instead errored: ${err}`);
throw `Should have decrypted phrase with deterministic salt, instead errored: ${err}`;
}

// // Test valid input (No salt, so it's the same every time)
await decryptMnemonic(legacyEncrypted, legacyPassword, triplesecDecrypt).then(
decoded => {
expect(decoded).toBe(legacyPhrase);
},
err => {
fail(`Should decrypt legacy encrypted phrase, instead errored: ${err}`);
}
);

const errorCallback = jest.fn();

// // Invalid inputs
Expand All @@ -571,7 +540,7 @@ test('encryptMnemonic & decryptMnemonic', async () => {

expect(errorCallback).toHaveBeenCalledTimes(1);

await decryptMnemonic(preEncryptedPhrase, 'incorrect password', triplesecDecrypt).then(() => {
await decryptMnemonic(preEncryptedPhrase, 'incorrect password').then(() => {
fail('Should have thrown on incorrect password for decryption');
}, errorCallback);

Expand Down
2 changes: 0 additions & 2 deletions packages/wallet-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,8 @@
"@stacks/profile": "^6.13.1",
"@stacks/storage": "^6.13.1",
"@stacks/transactions": "^6.13.1",
"buffer": "^6.0.3",
"c32check": "^2.0.0",
"jsontokens": "^4.0.1",
"triplesec": "^4.0.3",
"zone-file": "^2.0.0-beta.3"
},
"devDependencies": {
Expand Down
6 changes: 2 additions & 4 deletions packages/wallet-sdk/src/encryption.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { encryptMnemonic, decryptMnemonic } from '@stacks/encryption';
import { triplesecDecrypt } from './triplesec';
import { decryptMnemonic, encryptMnemonic } from '@stacks/encryption';

/**
* Decrypt an encrypted mnemonic phrase with a password.
* Legacy triplesec encrypted payloads are also supported.
* @param data - Uint8Array or hex-encoded string of the encrypted mnemonic
* @param password - Password for data
* @return the raw mnemonic phrase
*/
export function decrypt(dataBytes: Uint8Array | string, password: string): Promise<string> {
return decryptMnemonic(dataBytes, password, triplesecDecrypt);
return decryptMnemonic(dataBytes, password);
}

/**
Expand Down
1 change: 0 additions & 1 deletion packages/wallet-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,4 @@ export * from './models/profile';
export * from './models/wallet-config';
export * from './models/legacy-wallet-config';
export * from './encryption';
export * from './triplesec';
export * from './utils';
Loading

0 comments on commit 5750984

Please sign in to comment.