Merge pull request #27 from mcndt/migration-subtlecrypto
Migration subtlecrypto
This commit is contained in:
commit
f84ddba528
2
plugin
2
plugin
@ -1 +1 @@
|
||||
Subproject commit e207051e611f271944124192a8fd6e217f2fd419
|
||||
Subproject commit a6d4bc71fb93049516ee107c31c3bde5aa26e597
|
@ -0,0 +1,15 @@
|
||||
-- RedefineTables
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_EncryptedNote" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"insert_time" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"expire_time" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"ciphertext" TEXT NOT NULL,
|
||||
"hmac" TEXT NOT NULL,
|
||||
"crypto_version" TEXT NOT NULL DEFAULT 'v1'
|
||||
);
|
||||
INSERT INTO "new_EncryptedNote" ("ciphertext", "expire_time", "hmac", "id", "insert_time") SELECT "ciphertext", "expire_time", "hmac", "id", "insert_time" FROM "EncryptedNote";
|
||||
DROP TABLE "EncryptedNote";
|
||||
ALTER TABLE "new_EncryptedNote" RENAME TO "EncryptedNote";
|
||||
PRAGMA foreign_key_check;
|
||||
PRAGMA foreign_keys=ON;
|
@ -16,6 +16,7 @@ model EncryptedNote {
|
||||
expire_time DateTime @default(now())
|
||||
ciphertext String
|
||||
hmac String
|
||||
crypto_version String @default("v1")
|
||||
}
|
||||
|
||||
model event {
|
||||
|
@ -33,6 +33,9 @@ export class NotePostRequest {
|
||||
@ValidateIf((o) => o.plugin_version != null)
|
||||
@Matches("^[0-9]+\\.[0-9]+\\.[0-9]+$")
|
||||
plugin_version: string | undefined;
|
||||
|
||||
@Matches("^v[0-9]+$")
|
||||
crypto_version: string = "v1";
|
||||
}
|
||||
|
||||
export async function postNoteController(
|
||||
@ -75,6 +78,7 @@ export async function postNoteController(
|
||||
ciphertext: notePostRequest.ciphertext as string,
|
||||
hmac: notePostRequest.hmac as string,
|
||||
expire_time: addDays(new Date(), EXPIRE_WINDOW_DAYS),
|
||||
crypto_version: notePostRequest.crypto_version,
|
||||
} as EncryptedNote;
|
||||
|
||||
// Store note object
|
||||
|
@ -15,6 +15,8 @@ const MALFORMED_VERSION = "v1.0.0";
|
||||
const VALID_USER_ID = "f06536e7df6857fc";
|
||||
const MALFORMED_ID_WRONG_CRC = "f06536e7df6857fd";
|
||||
const MALFORMED_ID_WRONG_LENGTH = "0";
|
||||
const VALID_CRYPTO_VERSION = "v99";
|
||||
const MALFORMED_CRYPTO_VERSION = "32";
|
||||
|
||||
const MOCK_NOTE_ID = "1234";
|
||||
|
||||
@ -36,7 +38,6 @@ const TEST_PAYLOADS: TestParams[] = [
|
||||
{
|
||||
payload: {
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
|
||||
hmac: VALID_HMAC,
|
||||
user_id: VALID_USER_ID,
|
||||
plugin_version: VALID_VERSION,
|
||||
@ -120,6 +121,28 @@ const TEST_PAYLOADS: TestParams[] = [
|
||||
},
|
||||
expectedStatus: 400,
|
||||
},
|
||||
// Request with valid ciphertext, hmac, user id, plugin version, and crypto version
|
||||
{
|
||||
payload: {
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
hmac: VALID_HMAC,
|
||||
user_id: VALID_USER_ID,
|
||||
plugin_version: VALID_VERSION,
|
||||
crypto_version: VALID_CRYPTO_VERSION,
|
||||
},
|
||||
expectedStatus: 200,
|
||||
},
|
||||
// Request with malformed crypto version
|
||||
{
|
||||
payload: {
|
||||
ciphertext: VALID_CIPHERTEXT,
|
||||
hmac: VALID_HMAC,
|
||||
user_id: VALID_USER_ID,
|
||||
plugin_version: VALID_VERSION,
|
||||
crypto_version: MALFORMED_CRYPTO_VERSION,
|
||||
},
|
||||
expectedStatus: 400,
|
||||
},
|
||||
];
|
||||
|
||||
describe("note.post.controller", () => {
|
||||
@ -161,6 +184,19 @@ describe("note.post.controller", () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Validate DAO calls
|
||||
if (expectedStatus === 200) {
|
||||
expect(mockNoteDao.createNote).toHaveBeenCalledTimes(1);
|
||||
expect(mockNoteDao.createNote).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
ciphertext: payload.ciphertext,
|
||||
hmac: payload.hmac,
|
||||
crypto_version: payload.crypto_version || "v1",
|
||||
expire_time: expect.any(Date),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Validate Write events
|
||||
expect(mockEventLogger.writeEvent).toHaveBeenCalledOnce();
|
||||
if (expectedStatus === 200) {
|
||||
|
@ -1,25 +1,39 @@
|
||||
import { expect, it } from 'vitest';
|
||||
import decrypt from './decrypt';
|
||||
import { expect, describe, it, vi } from 'vitest';
|
||||
import { webcrypto } from 'crypto';
|
||||
import { decrypt_v1, decrypt_v2 } from './decrypt';
|
||||
|
||||
const TEST_NOTE = {
|
||||
vi.stubGlobal('crypto', {
|
||||
subtle: webcrypto.subtle
|
||||
});
|
||||
|
||||
const TEST_NOTE_V1 = {
|
||||
ciphertext: 'U2FsdGVkX1+r+nJffb6piMq1hPFSBSkf9/sgXj/UalA=',
|
||||
hmac: '7bfd5b0e96a0ed7ea43091d3e26f7c487bcebf8ba06175a4d4fc4d8466ba37f6'
|
||||
};
|
||||
const TEST_KEY = 'mgyUwoFwhlb1cnjhYYSrkY9_7hZKcRHQJs5l8wYB3Vk';
|
||||
const TEST_PLAINTEXT = 'You did it!';
|
||||
const TEST_KEY_V1 = 'mgyUwoFwhlb1cnjhYYSrkY9_7hZKcRHQJs5l8wYB3Vk';
|
||||
const TEST_PLAINTEXT_V1 = 'You did it!';
|
||||
|
||||
it('Should return plaintext with the correct key', () => {
|
||||
decrypt({ ...TEST_NOTE, key: TEST_KEY }).then((plaintext) => {
|
||||
expect(plaintext).toContain(TEST_PLAINTEXT);
|
||||
});
|
||||
const TEST_NOTE_V2 = {
|
||||
ciphertext: '7u2HlkxEfptYF0KTIkSLHBbNumP58XjfjEuLb2qG0tw=',
|
||||
hmac: '6SDEr9vCn4qM0u6+yFt/e+8Z1LLCNcCTw4GB4aNVMXM='
|
||||
};
|
||||
const TEST_KEY_V2 = 'fzrpzrhjyeBgZNJTlIQ5GmduQ+AywMUFPY9ZisP6A9c=';
|
||||
const TEST_PLAINTEXT_V2 = 'This is the test data.';
|
||||
|
||||
describe.each([
|
||||
{ decrypt_func: decrypt_v1, note: TEST_NOTE_V1, key: TEST_KEY_V1, plaintext: TEST_PLAINTEXT_V1 },
|
||||
{ decrypt_func: decrypt_v2, note: TEST_NOTE_V2, key: TEST_KEY_V2, plaintext: TEST_PLAINTEXT_V2 }
|
||||
])('decrypt', ({ decrypt_func, note, key, plaintext }) => {
|
||||
it('Should return plaintext with the correct key', async () => {
|
||||
const test_plaintext = await decrypt_func({ ...note, key: key });
|
||||
expect(test_plaintext).toContain(plaintext);
|
||||
});
|
||||
|
||||
it('Should throw with the wrong key', async () => {
|
||||
await expect(decrypt({ ...TEST_NOTE, key: '' })).rejects.toThrow('Failed HMAC check');
|
||||
await expect(decrypt_v1({ ...note, key: '' })).rejects.toThrow('Failed HMAC check');
|
||||
});
|
||||
|
||||
it('Should throw with the wrong HMAC', async () => {
|
||||
await expect(decrypt({ ...TEST_NOTE, hmac: '', key: TEST_KEY })).rejects.toThrow(
|
||||
'Failed HMAC check'
|
||||
);
|
||||
await expect(decrypt_v1({ ...note, hmac: '', key: key })).rejects.toThrow('Failed HMAC check');
|
||||
});
|
||||
});
|
||||
|
@ -1,7 +1,22 @@
|
||||
// TODO: should be same source code as used in the plugin!!
|
||||
|
||||
import { AES, enc, HmacSHA256 } from 'crypto-js';
|
||||
|
||||
// TODO: should be same source code as used in the plugin!!
|
||||
export default async function decrypt(cryptData: {
|
||||
export async function decrypt(
|
||||
cryptData: { ciphertext: string; hmac: string; key: string },
|
||||
version: string
|
||||
): Promise<string> {
|
||||
console.debug(`decrypting with crypto suite ${version}`);
|
||||
if (version === 'v1') {
|
||||
return decrypt_v1(cryptData);
|
||||
}
|
||||
if (version === 'v2') {
|
||||
return decrypt_v2(cryptData);
|
||||
}
|
||||
throw new Error(`Unsupported crypto version: ${version}`);
|
||||
}
|
||||
|
||||
export async function decrypt_v1(cryptData: {
|
||||
ciphertext: string;
|
||||
hmac: string;
|
||||
key: string;
|
||||
@ -15,3 +30,49 @@ export default async function decrypt(cryptData: {
|
||||
const md = AES.decrypt(cryptData.ciphertext, cryptData.key).toString(enc.Utf8);
|
||||
return md;
|
||||
}
|
||||
|
||||
export async function decrypt_v2(cryptData: {
|
||||
ciphertext: string;
|
||||
hmac: string;
|
||||
key: string;
|
||||
}): Promise<string> {
|
||||
const secret = base64ToArrayBuffer(cryptData.key);
|
||||
const ciphertext_buf = base64ToArrayBuffer(cryptData.ciphertext);
|
||||
const hmac_buf = base64ToArrayBuffer(cryptData.hmac);
|
||||
|
||||
const is_authentic = await window.crypto.subtle.verify(
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
await _getSignKey(secret),
|
||||
hmac_buf,
|
||||
ciphertext_buf
|
||||
);
|
||||
|
||||
if (!is_authentic) {
|
||||
throw Error('Failed HMAC check');
|
||||
}
|
||||
|
||||
const md = await window.crypto.subtle.decrypt(
|
||||
{ name: 'AES-CBC', iv: new Uint8Array(16) },
|
||||
await _getAesKey(secret),
|
||||
ciphertext_buf
|
||||
);
|
||||
return new TextDecoder().decode(md);
|
||||
}
|
||||
|
||||
function _getAesKey(secret: ArrayBuffer): Promise<CryptoKey> {
|
||||
return window.crypto.subtle.importKey('raw', secret, { name: 'AES-CBC', length: 256 }, false, [
|
||||
'encrypt',
|
||||
'decrypt'
|
||||
]);
|
||||
}
|
||||
|
||||
function _getSignKey(secret: ArrayBuffer): Promise<CryptoKey> {
|
||||
return window.crypto.subtle.importKey('raw', secret, { name: 'HMAC', hash: 'SHA-256' }, false, [
|
||||
'sign',
|
||||
'verify'
|
||||
]);
|
||||
}
|
||||
|
||||
function base64ToArrayBuffer(base64: string): ArrayBuffer {
|
||||
return Uint8Array.from(window.atob(base64), (c) => c.charCodeAt(0));
|
||||
}
|
||||
|
@ -4,4 +4,5 @@ export type EncryptedNote = {
|
||||
expire_time: Date;
|
||||
ciphertext: string;
|
||||
hmac: string;
|
||||
crypto_version: string;
|
||||
};
|
||||
|
@ -17,7 +17,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import decrypt from '$lib/crypto/decrypt';
|
||||
import { decrypt } from '$lib/crypto/decrypt';
|
||||
import MarkdownRenderer from '$lib/components/MarkdownRenderer.svelte';
|
||||
import LogoMarkdown from 'svelte-icons/io/IoLogoMarkdown.svelte';
|
||||
import IconEncrypted from 'svelte-icons/md/MdLockOutline.svelte';
|
||||
@ -35,9 +35,8 @@
|
||||
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
// Decrypt note
|
||||
const key = location.hash.slice(1);
|
||||
decrypt({ ...note, key })
|
||||
decrypt({ ...note, key }, note.crypto_version)
|
||||
.then((value) => (plaintext = value))
|
||||
.catch(() => (decryptFailed = true));
|
||||
}
|
||||
|
7
webapp/src/types/index.d.ts
vendored
Normal file
7
webapp/src/types/index.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
// Solution for adding crypto definitions: https://stackoverflow.com/questions/71525466/property-subtle-does-not-exist-on-type-typeof-webcrypto
|
||||
|
||||
declare module "crypto" {
|
||||
namespace webcrypto {
|
||||
const subtle: SubtleCrypto;
|
||||
}
|
||||
}
|
@ -9,7 +9,8 @@
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"types": ["vitest/globals", "@testing-library/jest-dom"]
|
||||
"types": ["vitest/globals", "@testing-library/jest-dom"],
|
||||
"typeRoots": ["./node_modules/@types", "./src/types"]
|
||||
},
|
||||
"paths": {
|
||||
"$lib": ["src/lib"],
|
||||
|
Loading…
x
Reference in New Issue
Block a user