-
Notifications
You must be signed in to change notification settings - Fork 518
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Update cipher operation / add key storage (#39)
* 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
Showing
14 changed files
with
396 additions
and
214 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
165 changes: 90 additions & 75 deletions
165
packages/wallet-sdk/src/connector/scw/protocol/KeyManager.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
92
packages/wallet-sdk/src/connector/scw/protocol/Keymanager.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.