Skip to content

Commit

Permalink
Update cipher operation / add key storage (#39)
Browse files Browse the repository at this point in the history
* limit Message types

* code cleanup

* keystorage

* revert tsconfig change

* revert

* move

* adding tests

* Revert "Clean up WalletLink for WalletLinkConnector and SCWSession (#36)"

This reverts commit 9631822b7f8fff6c9888d5804daa33a06d487f79.

* import/export
  • Loading branch information
bangtoven committed Feb 29, 2024
1 parent f3f766c commit 126493b
Show file tree
Hide file tree
Showing 14 changed files with 396 additions and 214 deletions.
2 changes: 2 additions & 0 deletions packages/wallet-sdk/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export default {
'./src/provider/**/*.ts',
'./src/relay/**/*.ts',
'./src/components/**/*.tsx',
'./src/connector/**/*.ts',
'./src/transport/**/*.ts',
],
// The directory where Jest should output its coverage files
coverageDirectory: 'coverage',
Expand Down
165 changes: 90 additions & 75 deletions packages/wallet-sdk/src/connector/scw/protocol/KeyManager.ts
Original file line number Diff line number Diff line change
@@ -1,87 +1,102 @@
export abstract class KeyManager {
private privateKey: CryptoKey | null;
publicKey: CryptoKey | null;
private sharedSecret: CryptoKey | null;
import { hexStringToUint8Array, uint8ArrayToHex } from '../../../core/util';

constructor() {
this.privateKey = null;
this.publicKey = null;
this.sharedSecret = null;
}

async generateKeyPair() {
const keyPair = await crypto.subtle.generateKey(
{
name: 'ECDH',
namedCurve: 'P-256',
},
false,
['deriveKey']
);
export type EncryptedData = {
iv: Uint8Array;
cipherText: ArrayBuffer;
};

this.privateKey = keyPair.privateKey;
this.publicKey = keyPair.publicKey;
}
export async function generateKeyPair(): Promise<CryptoKeyPair> {
return crypto.subtle.generateKey(
{
name: 'ECDH',
namedCurve: 'P-256',
},
true,
['deriveKey']
);
}

async deriveSharedSecret(otherPublicKey: CryptoKey) {
if (!this.privateKey) {
throw new Error('Private key not set, call generateKeyPair() first');
}
const sharedSecret = await crypto.subtle.deriveKey(
{
name: 'ECDH',
public: otherPublicKey,
},
this.privateKey,
{
name: 'AES-GCM',
length: 256,
},
false,
['encrypt', 'decrypt']
);
export async function deriveSharedSecret(
ownPrivateKey: CryptoKey,
peerPublicKey: CryptoKey
): Promise<CryptoKey> {
return crypto.subtle.deriveKey(
{
name: 'ECDH',
public: peerPublicKey,
},
ownPrivateKey,
{
name: 'AES-GCM',
length: 256,
},
false,
['encrypt', 'decrypt']
);
}

this.sharedSecret = sharedSecret;
}
export async function encrypt(sharedSecret: CryptoKey, plainText: string): Promise<EncryptedData> {
const iv = crypto.getRandomValues(new Uint8Array(12));
const cipherText = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv,
},
sharedSecret,
new TextEncoder().encode(plainText)
);

async encrypt(plainText: string | undefined) {
if (!this.sharedSecret) {
throw new Error('Shared secret not set, call deriveSharedSecret() first');
}
const iv = crypto.getRandomValues(new Uint8Array(12));
const cipherText = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv,
},
this.sharedSecret,
new TextEncoder().encode(plainText)
);
return { iv, cipherText };
}

return { iv, cipherText };
}
export async function decrypt(
sharedSecret: CryptoKey,
{ iv, cipherText }: EncryptedData
): Promise<string> {
const plainText = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv,
},
sharedSecret,
cipherText
);

async decrypt({ iv, cipherText }: { iv: Uint8Array; cipherText: ArrayBuffer }) {
if (!this.sharedSecret) {
throw new Error('Shared secret not set, call deriveSharedSecret() first');
}
const plainText = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv,
},
this.sharedSecret,
cipherText
);
return new TextDecoder().decode(plainText);
}

return new TextDecoder().decode(plainText);
function getFormat(keyType: 'public' | 'private') {
switch (keyType) {
case 'public':
return 'spki';
case 'private':
return 'pkcs8';
}
}

reset() {
this.privateKey = null;
this.publicKey = null;
this.sharedSecret = null;
}
export async function exportKeyToHexString(
type: 'public' | 'private',
key: CryptoKey
): Promise<string> {
const format = getFormat(type);
const exported = await crypto.subtle.exportKey(format, key);
return uint8ArrayToHex(new Uint8Array(exported));
}

public abstract storeSessionData(data: unknown): void;
export async function importKeyFromHexString(
type: 'public' | 'private',
hexString: string
): Promise<CryptoKey> {
const format = getFormat(type);
const arrayBuffer = hexStringToUint8Array(hexString).buffer;
return await crypto.subtle.importKey(
format,
arrayBuffer,
{
name: 'ECDH',
namedCurve: 'P-256',
},
true,
type === 'private' ? ['deriveKey'] : []
);
}
92 changes: 27 additions & 65 deletions packages/wallet-sdk/src/connector/scw/protocol/Keymanager.test.ts
Original file line number Diff line number Diff line change
@@ -1,101 +1,63 @@
import { KeyManager } from './KeyManager';

class TestKeyManager extends KeyManager {
storeSessionData<T>(data: T): T {
return data;
}
}

describe('KeyManager', () => {
let keyManager: KeyManager;

beforeEach(() => {
keyManager = new TestKeyManager();
});
import { decrypt, deriveSharedSecret, encrypt, generateKeyPair } from './KeyManager';

describe('SCWCipher', () => {
describe('generateKeyPair', () => {
it('should generate a unique key pair on each call', async () => {
await keyManager.generateKeyPair();
const firstPublicKey = keyManager.publicKey;

await keyManager.generateKeyPair();
const secondPublicKey = keyManager.publicKey;
const firstPublicKey = (await generateKeyPair()).publicKey;
const secondPublicKey = (await generateKeyPair()).publicKey;

expect(firstPublicKey).not.toBe(secondPublicKey);
});
});

describe('deriveSharedSecret', () => {
it('should derive a shared secret successfully', async () => {
await keyManager.generateKeyPair();

const otherKeyManager = new TestKeyManager();
await otherKeyManager.generateKeyPair();
const ownKeyPair = await generateKeyPair();
const peerKeyPair = await generateKeyPair();

await keyManager.deriveSharedSecret(otherKeyManager.publicKey!);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-next-line
expect(keyManager.sharedSecret).toBeDefined();
const sharedSecret = await deriveSharedSecret(ownKeyPair.privateKey, peerKeyPair.publicKey);
expect(sharedSecret).toBeDefined();
});
});

describe('encrypt and decrypt', () => {
it('should encrypt and decrypt a message successfully', async () => {
await keyManager.generateKeyPair();

const otherKeyManager = new TestKeyManager();
await otherKeyManager.generateKeyPair();
const ownKeyPair = await generateKeyPair();
const peerKeyPair = await generateKeyPair();

await keyManager.deriveSharedSecret(otherKeyManager.publicKey!);
await otherKeyManager.deriveSharedSecret(keyManager.publicKey!);
const sharedSecret = await deriveSharedSecret(ownKeyPair.privateKey, peerKeyPair.publicKey);
const sharedSecretDerivedByPeer = await deriveSharedSecret(
peerKeyPair.privateKey,
ownKeyPair.publicKey
);

const plaintext = 'This is a secret message';
const encryptedMessage = await keyManager.encrypt(plaintext);
const decryptedMessage = await otherKeyManager.decrypt(encryptedMessage);
const encryptedMessage = await encrypt(sharedSecret, plaintext);
const decryptedText = await decrypt(sharedSecretDerivedByPeer, encryptedMessage);

expect(decryptedMessage).toBe(plaintext);
expect(decryptedText).toBe(plaintext);
});

it('should throw an error when decrypting with a different shared secret', async () => {
await keyManager.generateKeyPair();

const otherKeyManager = new TestKeyManager();
await otherKeyManager.generateKeyPair();
const ownKeyPair = await generateKeyPair();
const peerKeyPair = await generateKeyPair();

await keyManager.deriveSharedSecret(otherKeyManager.publicKey!);
const sharedSecret = await deriveSharedSecret(ownKeyPair.privateKey, peerKeyPair.publicKey);

const plaintext = 'This is a secret message';

const encryptedMessage = await keyManager.encrypt(plaintext);
const encryptedMessage = await encrypt(sharedSecret, plaintext);

// generate new keypair on otherKeyManager and use it to derive different shared secret
await otherKeyManager.generateKeyPair();
await keyManager.deriveSharedSecret(otherKeyManager.publicKey!);
const sharedSecretDerivedByPeer = await deriveSharedSecret(
peerKeyPair.privateKey,
peerKeyPair.publicKey
);

// Attempting to decrypt with a different shared secret
await expect(keyManager.decrypt(encryptedMessage)).rejects.toThrow(
await expect(decrypt(sharedSecretDerivedByPeer, encryptedMessage)).rejects.toThrow(
'Unsupported state or unable to authenticate data'
);
});
});

describe('reset', () => {
it('should reset keyManager state completely', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-next-line
keyManager.privateKey = {} as CryptoKey;
keyManager.publicKey = {} as CryptoKey;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-next-line
keyManager.sharedSecret = {} as CryptoKey;
keyManager.reset();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-next-line
expect(keyManager.privateKey).toBeNull();
expect(keyManager.publicKey).toBeNull();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-next-line
expect(keyManager.sharedSecret).toBeNull();
});
});
});
Loading

0 comments on commit 126493b

Please sign in to comment.