mirror of
https://github.com/TeamPiped/Piped.git
synced 2024-08-14 23:57:27 +00:00
Merge a5c0d8007c
into a055ae6686
This commit is contained in:
commit
d26ce27691
9 changed files with 220 additions and 19 deletions
|
@ -21,6 +21,7 @@
|
|||
"linkify-html": "4.1.1",
|
||||
"linkifyjs": "4.1.1",
|
||||
"mux.js": "6.3.0",
|
||||
"pako": "2.1.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"shaka-player": "4.5.0",
|
||||
"stream-browserify": "3.0.0",
|
||||
|
|
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
|
@ -38,6 +38,9 @@ dependencies:
|
|||
mux.js:
|
||||
specifier: 6.3.0
|
||||
version: 6.3.0
|
||||
pako:
|
||||
specifier: 2.1.0
|
||||
version: 2.1.0
|
||||
qrcode:
|
||||
specifier: ^1.5.3
|
||||
version: 1.5.3
|
||||
|
@ -4194,6 +4197,10 @@ packages:
|
|||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/pako@2.1.0:
|
||||
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
|
||||
dev: false
|
||||
|
||||
/parent-module@1.0.1:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
65
src/App.vue
65
src/App.vue
|
@ -9,7 +9,7 @@
|
|||
</router-view>
|
||||
</div>
|
||||
|
||||
<FooterComponent />
|
||||
<FooterComponent :config="config" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -19,6 +19,10 @@ import FooterComponent from "./components/FooterComponent.vue";
|
|||
|
||||
const darkModePreference = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
|
||||
import { generateKey, encodeArrayToBase64, decodeBase64ToArray, decryptAESGCM } from "./utils/encryptionUtils";
|
||||
import { decompressGzip } from "./utils/compressionUtils";
|
||||
import { state } from "./utils/store";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NavBar,
|
||||
|
@ -27,6 +31,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
theme: "dark",
|
||||
config: null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
|
@ -35,6 +40,15 @@ export default {
|
|||
this.setTheme();
|
||||
});
|
||||
|
||||
this.fetchJson(this.authApiUrl() + "/config")
|
||||
.then(config => {
|
||||
this.config = config;
|
||||
state.config = config;
|
||||
})
|
||||
.then(() => {
|
||||
this.onConfigLoaded();
|
||||
});
|
||||
|
||||
if ("indexedDB" in window) {
|
||||
const request = indexedDB.open("piped-db", 5);
|
||||
request.onupgradeneeded = ev => {
|
||||
|
@ -115,6 +129,55 @@ export default {
|
|||
const themeColor = document.querySelector("meta[name='theme-color']");
|
||||
themeColor.setAttribute("content", currentColor[this.theme]);
|
||||
},
|
||||
async onConfigLoaded() {
|
||||
if (this.config.s3Enabled && this.authenticated) {
|
||||
if (this.getPreferenceBoolean("syncPreferences", false, false)) {
|
||||
var e2e2_b64_key = this.getPreferenceString("e2ee_key", null, false);
|
||||
if (!e2e2_b64_key) {
|
||||
const key = new Uint8Array(await generateKey());
|
||||
const encoded = encodeArrayToBase64(key);
|
||||
this.setPreference("e2ee_key", encoded);
|
||||
e2e2_b64_key = encoded;
|
||||
}
|
||||
|
||||
const statResult = await this.fetchJson(
|
||||
this.authApiUrl() + "/storage/stat",
|
||||
{
|
||||
file: "pipedpref",
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: this.getAuthToken(),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (statResult.status === "exists") {
|
||||
const data = await fetch(this.authApiUrl() + "/storage/get?file=pipedpref", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: this.getAuthToken(),
|
||||
},
|
||||
}).then(resp => resp.arrayBuffer());
|
||||
|
||||
const cryptoKey = decodeBase64ToArray(e2e2_b64_key).buffer;
|
||||
|
||||
const decrypted = await decryptAESGCM(data, cryptoKey);
|
||||
|
||||
const decompressed = await decompressGzip(new Uint8Array(decrypted));
|
||||
|
||||
const localStorageJson = JSON.parse(decompressed);
|
||||
|
||||
// import into localStorage
|
||||
for (var key in localStorageJson) {
|
||||
if (Object.prototype.hasOwnProperty.call(localStorageJson, key)) {
|
||||
localStorage[key] = localStorageJson[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -29,6 +29,12 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
config: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
donationHref: null,
|
||||
|
@ -36,16 +42,13 @@ export default {
|
|||
privacyPolicyHref: null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.fetchConfig();
|
||||
},
|
||||
methods: {
|
||||
async fetchConfig() {
|
||||
this.fetchJson(this.apiUrl() + "/config").then(config => {
|
||||
this.donationHref = config?.donationUrl;
|
||||
this.statusPageHref = config?.statusPageUrl;
|
||||
this.privacyPolicyHref = config?.privacyPolicyUrl;
|
||||
});
|
||||
watch: {
|
||||
config: {
|
||||
handler() {
|
||||
this.donationHref = this.config?.donationUrl;
|
||||
this.statusPageHref = this.config?.statusPageUrl;
|
||||
this.privacyPolicyHref = this.config?.privacyPolicyUrl;
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -380,6 +380,9 @@
|
|||
<script>
|
||||
import CountryMap from "@/utils/CountryMaps/en.json";
|
||||
import ConfirmModal from "./ConfirmModal.vue";
|
||||
import { state } from "../utils/store";
|
||||
import { encryptAESGCM, decodeBase64ToArray } from "../utils/encryptionUtils";
|
||||
import { compressGzip } from "../utils/compressionUtils";
|
||||
export default {
|
||||
components: {
|
||||
ConfirmModal,
|
||||
|
@ -614,6 +617,53 @@ export default {
|
|||
localStorage.setItem("hideWatched", this.hideWatched);
|
||||
localStorage.setItem("mobileChapterLayout", this.mobileChapterLayout);
|
||||
|
||||
const config = state.config;
|
||||
|
||||
const key = this.getPreferenceString("e2ee_key", null, false);
|
||||
|
||||
if (config.s3Enabled && this.authenticated && key) {
|
||||
const statResult = await this.fetchJson(
|
||||
this.authApiUrl() + "/storage/stat",
|
||||
{
|
||||
file: "pipedpref",
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: this.getAuthToken(),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const etag = statResult.etag;
|
||||
|
||||
// export localStorage to JSON
|
||||
const localStorageJson = {};
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
localStorageJson[key] = localStorage.getItem(key);
|
||||
}
|
||||
|
||||
const importedKey = decodeBase64ToArray(key).buffer;
|
||||
|
||||
const data = await compressGzip(JSON.stringify(localStorageJson));
|
||||
|
||||
const encrypted = await encryptAESGCM(data, importedKey);
|
||||
|
||||
await this.fetchJson(
|
||||
this.authApiUrl() + "/storage/put",
|
||||
{},
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: this.getAuthToken(),
|
||||
"x-file-name": "pipedpref",
|
||||
"x-last-etag": etag,
|
||||
},
|
||||
body: encrypted,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldReload) window.location.reload();
|
||||
}
|
||||
},
|
||||
|
|
16
src/main.js
16
src/main.js
|
@ -125,10 +125,10 @@ const mixin = {
|
|||
if (!disableAlert) alert(this.$t("info.local_storage"));
|
||||
}
|
||||
},
|
||||
getPreferenceBoolean(key, defaultVal) {
|
||||
getPreferenceBoolean(key, defaultVal, allowQuery = true) {
|
||||
var value;
|
||||
if (
|
||||
(value = new URLSearchParams(window.location.search).get(key)) !== null ||
|
||||
(allowQuery && (value = new URLSearchParams(window.location.search).get(key)) !== null) ||
|
||||
(this.testLocalStorage && (value = localStorage.getItem(key)) !== null)
|
||||
) {
|
||||
switch (String(value).toLowerCase()) {
|
||||
|
@ -142,29 +142,29 @@ const mixin = {
|
|||
}
|
||||
} else return defaultVal;
|
||||
},
|
||||
getPreferenceString(key, defaultVal) {
|
||||
getPreferenceString(key, defaultVal, allowQuery = true) {
|
||||
var value;
|
||||
if (
|
||||
(value = new URLSearchParams(window.location.search).get(key)) !== null ||
|
||||
(allowQuery && (value = new URLSearchParams(window.location.search).get(key)) !== null) ||
|
||||
(this.testLocalStorage && (value = localStorage.getItem(key)) !== null)
|
||||
) {
|
||||
return value;
|
||||
} else return defaultVal;
|
||||
},
|
||||
getPreferenceNumber(key, defaultVal) {
|
||||
getPreferenceNumber(key, defaultVal, allowQuery = true) {
|
||||
var value;
|
||||
if (
|
||||
(value = new URLSearchParams(window.location.search).get(key)) !== null ||
|
||||
(allowQuery && (value = new URLSearchParams(window.location.search).get(key)) !== null) ||
|
||||
(this.testLocalStorage && (value = localStorage.getItem(key)) !== null)
|
||||
) {
|
||||
const num = Number(value);
|
||||
return isNaN(num) ? defaultVal : num;
|
||||
} else return defaultVal;
|
||||
},
|
||||
getPreferenceJSON(key, defaultVal) {
|
||||
getPreferenceJSON(key, defaultVal, allowQuery = true) {
|
||||
var value;
|
||||
if (
|
||||
(value = new URLSearchParams(window.location.search).get(key)) !== null ||
|
||||
(allowQuery && (value = new URLSearchParams(window.location.search).get(key)) !== null) ||
|
||||
(this.testLocalStorage && (value = localStorage.getItem(key)) !== null)
|
||||
) {
|
||||
return JSON.parse(value);
|
||||
|
|
38
src/utils/compressionUtils.js
Normal file
38
src/utils/compressionUtils.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
export const compressGzip = async data => {
|
||||
// Firefox does not support CompressionStream yet
|
||||
if (typeof CompressionStream !== "undefined") {
|
||||
let bytes = new TextEncoder().encode(data);
|
||||
// eslint-disable-next-line no-undef
|
||||
const cs = new CompressionStream("gzip");
|
||||
const writer = cs.writable.getWriter();
|
||||
writer.write(bytes);
|
||||
writer.close();
|
||||
const compAb = await new Response(cs.readable).arrayBuffer();
|
||||
bytes = new Uint8Array(compAb);
|
||||
|
||||
return bytes;
|
||||
} else {
|
||||
const pako = await import("pako");
|
||||
return pako.gzip(data);
|
||||
}
|
||||
};
|
||||
|
||||
export const decompressGzip = async compressedData => {
|
||||
// Firefox does not support DecompressionStream yet
|
||||
if (typeof DecompressionStream !== "undefined") {
|
||||
// eslint-disable-next-line no-undef
|
||||
const ds = new DecompressionStream("gzip");
|
||||
const writer = ds.writable.getWriter();
|
||||
writer.write(compressedData);
|
||||
writer.close();
|
||||
const decompAb = await new Response(ds.readable).arrayBuffer();
|
||||
const bytes = new Uint8Array(decompAb);
|
||||
|
||||
return new TextDecoder().decode(bytes);
|
||||
} else {
|
||||
const pako = await import("pako");
|
||||
const inflated = pako.inflate(compressedData, { to: "string" });
|
||||
|
||||
return inflated;
|
||||
}
|
||||
};
|
36
src/utils/encryptionUtils.js
Normal file
36
src/utils/encryptionUtils.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
// These functions accept and return Uint8Arrays
|
||||
|
||||
export async function encryptAESGCM(plaintext, key) {
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const algorithm = { name: "AES-GCM", iv: iv };
|
||||
const keyMaterial = await crypto.subtle.importKey("raw", key, algorithm, false, ["encrypt"]);
|
||||
const ciphertext = await crypto.subtle.encrypt(algorithm, keyMaterial, plaintext);
|
||||
|
||||
return new Uint8Array([...iv, ...new Uint8Array(ciphertext)]);
|
||||
}
|
||||
|
||||
export async function decryptAESGCM(ciphertextArray, key) {
|
||||
const iv = new Uint8Array(ciphertextArray.slice(0, 12));
|
||||
const algorithm = { name: "AES-GCM", iv: iv };
|
||||
const keyMaterial = await crypto.subtle.importKey("raw", key, algorithm, false, ["decrypt"]);
|
||||
const decrypted = await crypto.subtle.decrypt(algorithm, keyMaterial, new Uint8Array(ciphertextArray.slice(12)));
|
||||
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
export async function generateKey() {
|
||||
const algorithm = { name: "AES-GCM", length: 256 };
|
||||
const key = await crypto.subtle.generateKey(algorithm, true, ["encrypt", "decrypt"]);
|
||||
|
||||
return await crypto.subtle.exportKey("raw", key);
|
||||
}
|
||||
|
||||
export function encodeArrayToBase64(array) {
|
||||
const chars = String.fromCharCode.apply(null, array);
|
||||
return btoa(chars);
|
||||
}
|
||||
|
||||
export function decodeBase64ToArray(base64) {
|
||||
const chars = atob(base64);
|
||||
return new Uint8Array(chars.split("").map(c => c.charCodeAt(0)));
|
||||
}
|
3
src/utils/store.js
Normal file
3
src/utils/store.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { reactive } from "vue";
|
||||
|
||||
export const state = reactive({});
|
Loading…
Add table
Add a link
Reference in a new issue