diff --git a/app/components/button/google-code.vue b/app/components/button/google-code.vue new file mode 100644 index 0000000..5cf8fa8 --- /dev/null +++ b/app/components/button/google-code.vue @@ -0,0 +1,177 @@ + + + + + \ No newline at end of file diff --git a/app/components/button/google.vue b/app/components/button/google.vue index 1601ea0..1c801a8 100644 --- a/app/components/button/google.vue +++ b/app/components/button/google.vue @@ -24,7 +24,7 @@ authStore.setTokenCookie(data.token); authStore.setUserCookie(data.user); listStore.lists = data.lists - console.log(listStore.lists) //ok + //console.log(listStore.lists) //ok } catch (error) { console.error("Erreur côté WordPress:", error); } diff --git a/app/composables/crypto.ts b/app/composables/crypto.ts new file mode 100644 index 0000000..94a7351 --- /dev/null +++ b/app/composables/crypto.ts @@ -0,0 +1,199 @@ +import sodium from "libsodium-wrappers" + +export type KeyPair = { + publicKey: Uint8Array + privateKey: Uint8Array +} + +export type EncryptedPayload = { + nonce: Uint8Array + cipher: Uint8Array +} + +export type EncryptedPrivateKey = { + nonce: Uint8Array + cipher: Uint8Array + salt: Uint8Array +} + +export const useCryptoStore = () => { + + let ready = false + + const init = async (): Promise => { + if (!ready) { + await sodium.ready + ready = true + } + } + + /** + * USER KEY PAIR + */ + const generateUserKeyPair = (): KeyPair => { + + const pair = sodium.crypto_box_keypair() + + return { + publicKey: pair.publicKey, + privateKey: pair.privateKey + } + } + + /** + * MASTER KEY + */ + const generateMasterKey = (): Uint8Array => { + return sodium.randombytes_buf( + sodium.crypto_secretbox_KEYBYTES + ) + } + + /** + * ENCRYPT PRIVATE KEY + */ + const encryptPrivateKey = ( + privateKey: Uint8Array, + masterKey: Uint8Array + ): EncryptedPayload => { + + const nonce = sodium.randombytes_buf( + sodium.crypto_secretbox_NONCEBYTES + ) + + const cipher = sodium.crypto_secretbox_easy( + privateKey, + nonce, + masterKey + ) + + return { nonce, cipher } + } + + /** + * DECRYPT PRIVATE KEY + */ + const decryptPrivateKey = ( + encrypted: EncryptedPayload, + masterKey: Uint8Array + ): Uint8Array => { + + return sodium.crypto_secretbox_open_easy( + encrypted.cipher, + encrypted.nonce, + masterKey + ) + } + + /** + * LIST KEY + */ + const generateListKey = (): Uint8Array => { + + return sodium.randombytes_buf( + sodium.crypto_aead_xchacha20poly1305_ietf_KEYBYTES + ) + } + + /** + * ENCRYPT LIST DATA + */ + const encryptList = ( + data: string, + listKey: Uint8Array + ): EncryptedPayload => { + + const nonce = sodium.randombytes_buf( + sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + ) + + const cipher = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt( + sodium.from_string(data), + null, + null, + nonce, + listKey + ) + + return { nonce, cipher } + } + + /** + * DECRYPT LIST DATA + */ + const decryptList = ( + encrypted: EncryptedPayload, + listKey: Uint8Array + ): string => { + + const decrypted = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt( + null, + encrypted.cipher, + null, + encrypted.nonce, + listKey + ) + + return sodium.to_string(decrypted) + } + + /** + * ENCRYPT LIST KEY FOR USER + */ + const encryptListKeyForUser = ( + listKey: Uint8Array, + recipientPublicKey: Uint8Array, + senderPrivateKey: Uint8Array + ): EncryptedPayload => { + + const nonce = sodium.randombytes_buf( + sodium.crypto_box_NONCEBYTES + ) + + const cipher = sodium.crypto_box_easy( + listKey, + nonce, + recipientPublicKey, + senderPrivateKey + ) + + return { nonce, cipher } + } + + /** + * DECRYPT LIST KEY + */ + const decryptListKey = ( + encrypted: EncryptedPayload, + senderPublicKey: Uint8Array, + recipientPrivateKey: Uint8Array + ): Uint8Array => { + + return sodium.crypto_box_open_easy( + encrypted.cipher, + encrypted.nonce, + senderPublicKey, + recipientPrivateKey + ) + } + + return { + + init, + + generateUserKeyPair, + + generateMasterKey, + + encryptPrivateKey, + decryptPrivateKey, + + generateListKey, + + encryptList, + decryptList, + + encryptListKeyForUser, + decryptListKey + } +} \ No newline at end of file diff --git a/app/composables/useCounter.ts b/app/composables/useCounter.ts deleted file mode 100644 index 1d3c669..0000000 --- a/app/composables/useCounter.ts +++ /dev/null @@ -1,20 +0,0 @@ -export const useCounter = () => { - const counter = useState('counter', () => 0) - const increment = () => { - counter.value++ - } - - const decrement = () => { - counter.value-- - } - - const reset = () => { - counter.value=0 - } - return { - counter, - increment, - decrement, - reset - } -} \ No newline at end of file diff --git a/app/pages/login.vue b/app/pages/login.vue index 0a7e694..9093925 100644 --- a/app/pages/login.vue +++ b/app/pages/login.vue @@ -89,7 +89,7 @@ const handleFormSubmit = async() => {

{{ $t('loginGoogle') }}

- + diff --git a/app/repositories/lists.repository.ts b/app/repositories/lists.repository.ts index 53fbf88..8c0782b 100644 --- a/app/repositories/lists.repository.ts +++ b/app/repositories/lists.repository.ts @@ -1,5 +1,5 @@ import type { $Fetch } from 'ofetch'; -import type { List } from '~/types/lists' +import type { List, WPListEncrypted } from '~/types/lists' export default class ListRepository { private fetcher: $Fetch; @@ -17,6 +17,23 @@ export default class ListRepository { method: 'POST', body: data }); + } + async uploadKeys(data: string) { + console.log('uploadEncryptedPrivateKey : data is') + console.log(data) + + return await this.fetcher('/lists/PrivKeyCipher', { + method: 'POST', + body: data + }) + } + + async update(data: WPListEncrypted) { + const string = JSON.stringify(data); + return await this.fetcher('/lists/' + data.id, { + method: 'PUT', + body: string + }) } } \ No newline at end of file diff --git a/app/stores/lists.ts b/app/stores/lists.ts index e4ac0d6..0c3765c 100644 --- a/app/stores/lists.ts +++ b/app/stores/lists.ts @@ -1,11 +1,18 @@ import { defineStore } from 'pinia' -import type { List } from '~/types/lists' +import type { List, WPListEncrypted } from '~/types/lists' +import {useCryptoStore} from '~/composables/crypto' +import sodium from "libsodium-wrappers" +const crypto = useCryptoStore() + export const useListStore = defineStore('lists', { state: () => ({ lists: [] as List[], loading: false as boolean, + masterKey: null as Uint8Array | null, + publicKey: new Uint8Array() as Uint8Array, + privateKey: new Uint8Array() as Uint8Array, }), actions: { @@ -15,8 +22,11 @@ export const useListStore = defineStore('lists', { resetLists(){ this.lists = [] }, + setMasterKey(key: Uint8Array){ + this.masterKey = key + }, - async fetchLists() { + async fetchLists() { // On récupère notre plugin API injecté const { $api } = useNuxtApp(); this.loading = true; @@ -33,7 +43,107 @@ export const useListStore = defineStore('lists', { } finally { this.loading = false; } - } + }, + + async processRawData(rawData: [WPListEncrypted]){ + const { $api } = useNuxtApp(); + await crypto.init() + + if (rawData && rawData.length > 0) { + const item = rawData[0]; + if (item && this.masterKey){ + if (this.isWPListEncrypted(item)) { + if (item.content_cipher === "initialize-me"){ + /* 1- Création de la paire de clés (TODO : à déporter ailleurs (Key Store ?)) */ + const { publicKey, privateKey } = crypto.generateUserKeyPair() + this.publicKey = publicKey; + this.privateKey = privateKey; + + // On les envoie au BO // + /* Chiffrage de la private avec la master : */ + const privKeyCipher = crypto.encryptPrivateKey(this.privateKey, this.masterKey) + + /* empaquetage */ + const payload = { + 'user_id': item.user_id, // pour vérification au BO + 'private_key_cipher': privKeyCipher, + 'public_key': publicKey, + } + + /* Et on envoie ! */ + await $api.lists.uploadKeys(JSON.stringify(payload)) + + // On crée la liste // + /* D'abord la clé AES de la liste */ + const key = crypto.generateListKey(); + + /* Puis la liste */ + const user_id = Number(item.user_id) + + const data: List = { + id: Number(item.id), + user_id: user_id, + aesKey: key, + list_title: 'THE VERY première liste', + list_type: 'basic', + content: '{[\'vide\']}', + is_open: true, + created_at: Date.now(), + updated_at: Date.now(), + } + this.lists[0] = data + + /* Et on envoie au BO */ + this.syncData(data, this.publicKey, this.privateKey) + } + else{ + console.log('coucou : ' + rawData); + } + } + } + } + + }, + + async syncData(decryptedData: List, userPublicKey: Uint8Array, userPrivateKey: Uint8Array) { + const { $api } = useNuxtApp(); + + const aesKey = decryptedData.aesKey; + await crypto.init(); + + // 1. On prépare les données (en excluant la clé elle-même du contenu) + const { aesKey: _, ...pureData } = decryptedData; + const stringData = JSON.stringify(pureData); + + // 2. Chiffrement du contenu par la clé AES (XChaCha20) + const encryptedContent = crypto.encryptList(stringData, aesKey); + + // 3. Chiffrement de la clé AES par la clé Publique (Asymétrique) + // On utilise ici ta fonction de l'étape 11 + const encryptedKeyPayload = crypto.encryptListKeyForUser( + aesKey, + userPublicKey, + userPrivateKey + ); + + // 4. On empaquette le tout pour le Back-Office + const dataToBO: WPListEncrypted = { + id: decryptedData.id.toString(), + user_id: decryptedData.user_id.toString(), + + // La clé AES verrouillée pour l'utilisateur + key_cipher: sodium.to_base64(encryptedKeyPayload.cipher), + key_nonce: sodium.to_base64(encryptedKeyPayload.nonce), + + // Le contenu de la liste + content_cipher: sodium.to_base64(encryptedContent.cipher), + content_nonce: sodium.to_base64(encryptedContent.nonce), + created_at: null, + updated_at: null, + }; + + await $api.lists.update(dataToBO) + }, // async updateList(id, title, content) { // const config = useRuntimeConfig(); @@ -57,6 +167,26 @@ export const useListStore = defineStore('lists', { // console.error("Erreur lors de la récupération des listes:", error); // } // } + + isWPListEncrypted(obj: any): obj is WPListEncrypted { + console.log(obj !== null) + console.log(typeof obj) + console.log(typeof obj.id) + console.log(typeof obj.key_cipher ) + console.log(typeof obj.key_nonce ) + console.log(typeof obj.content_cipher) + console.log(typeof obj.content_nonce) + + return ( + obj !== null && + typeof obj === 'object' && + typeof obj.id === 'string' && + typeof obj.key_cipher === 'string' && + typeof obj.key_nonce === 'string' && + typeof obj.content_cipher === 'string' && + typeof obj.content_nonce === 'string' + ); + } } }) \ No newline at end of file diff --git a/app/types/lists.ts b/app/types/lists.ts index 73df651..3bc102d 100644 --- a/app/types/lists.ts +++ b/app/types/lists.ts @@ -1,11 +1,24 @@ +export interface WPListEncrypted { + id: string; + user_id: string; + key_cipher: string; // La "data" est cachée là-dedans sous forme de string chiffrée + key_nonce: string; + content_cipher: string; + content_nonce: string; + created_at: number | null; // Souvent gardé en clair pour le tri côté front + updated_at: number | null; +} + export interface List { id: number; + user_id: number; + aesKey: Uint8Array; list_title: string; list_type: string; - encrypted_content: string; // JSON string + content: string; // JSON string is_open: boolean; - created_at: string; - updated_at: string; + created_at: number; + updated_at: number; } export interface taskItem { diff --git a/package-lock.json b/package-lock.json index 3f3758f..0f21246 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@nuxtjs/i18n": "^10.2.1", "@nuxtjs/tailwindcss": "^6.14.0", "@pinia/nuxt": "^0.11.3", + "libsodium-wrappers": "^0.8.2", "nuxt": "^4.2.2", "nuxt-vue3-google-signin": "^0.0.13", "pinia": "^3.0.4", @@ -8736,6 +8737,21 @@ "node": ">= 0.8.0" } }, + "node_modules/libsodium": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.8.2.tgz", + "integrity": "sha512-TsnGYMoZtpweT+kR+lOv5TVsnJ/9U0FZOsLFzFOMWmxqOAYXjX3fsrPAW+i1LthgDKXJnI9A8dWEanT1tnJKIw==", + "license": "ISC" + }, + "node_modules/libsodium-wrappers": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.8.2.tgz", + "integrity": "sha512-VFLmfxkxo+U9q60tjcnSomQBRx2UzlRjKWJqvB4K1pUqsMQg4cu3QXA2nrcsj9A1qRsnJBbi2Ozx1hsiDoCkhw==", + "license": "ISC", + "dependencies": { + "libsodium": "^0.8.0" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", diff --git a/package.json b/package.json index 843133a..5f41146 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@nuxtjs/i18n": "^10.2.1", "@nuxtjs/tailwindcss": "^6.14.0", "@pinia/nuxt": "^0.11.3", + "libsodium-wrappers": "^0.8.2", "nuxt": "^4.2.2", "nuxt-vue3-google-signin": "^0.0.13", "pinia": "^3.0.4",