Watch-on-LBRY/src/modules/crypto/index.ts
2022-05-02 12:12:11 +00:00

204 lines
No EOL
6.9 KiB
TypeScript

import path from 'path'
import type { DialogManager } from '../../components/dialogs'
import { getExtensionSettingsAsync, setExtensionSetting, ytUrlResolversSettings } from "../../settings"
import { getFileContent } from '../file'
async function generateKeys() {
const keys = await crypto.subtle.generateKey(
{
name: "RSASSA-PKCS1-v1_5",
// Consider using a 4096-bit key for systems that require long-term security
modulusLength: 384,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-1",
},
true,
["sign", "verify"]
)
return {
publicKey: await exportPublicKey(keys.publicKey),
privateKey: await exportPrivateKey(keys.privateKey)
}
}
async function exportPrivateKey(key: CryptoKey) {
const exported = await crypto.subtle.exportKey(
"pkcs8",
key
)
return Buffer.from(exported).toString('base64')
}
const publicKeyPrefix = `MEwwDQYJKoZIhvcNAQEBBQADOwAwOAIxA`
const publicKeySuffix = `IDAQAB` //`wIDAQAB` `WIDAQAB`
const publicKeyLength = 65
async function exportPublicKey(key: CryptoKey) {
const exported = await crypto.subtle.exportKey(
"spki",
key
)
const publicKey = Buffer.from(exported).toString('base64')
return publicKey.substring(publicKeyPrefix.length, publicKeyPrefix.length + publicKeyLength)
}
function importPrivateKey(base64: string) {
return crypto.subtle.importKey(
"pkcs8",
Buffer.from(base64, 'base64'),
{
name: "RSASSA-PKCS1-v1_5",
hash: "SHA-1",
},
true,
["sign"]
)
}
export async function sign(data: string, privateKey: string) {
return Buffer.from(await crypto.subtle.sign(
{ name: "RSASSA-PKCS1-v1_5" },
await importPrivateKey(privateKey),
await crypto.subtle.digest({ name: 'SHA-1' }, Buffer.from(data))
)).toString('base64')
}
export function resetProfileSettings() {
setExtensionSetting('publicKey', null)
setExtensionSetting('privateKey', null)
}
async function apiRequest<T extends object>(method: 'GET' | 'POST', pathname: string, data: T) {
const settings = await getExtensionSettingsAsync()
/* const urlResolverSettings = ytUrlResolversSettings[settings.urlResolver]
if (!urlResolverSettings.signRequest) throw new Error() */
const url = new URL(ytUrlResolversSettings.madiatorFinder.href/* urlResolverSettings.href */)
url.pathname = path.join(url.pathname, pathname)
url.searchParams.set('data', JSON.stringify(data))
if (true/* requiresSignature */) {
if (!settings.privateKey || !settings.publicKey)
throw new Error('There is no profile.')
url.searchParams.set('keys', JSON.stringify({
signature: await sign(url.searchParams.toString(), settings.privateKey!),
publicKey: settings.publicKey
}))
}
const respond = await fetch(url.href, { method })
if (respond.ok) return respond.json()
throw new Error((await respond.json()).message)
}
export async function generateProfileAndSetNickname(dialogManager: DialogManager, overwrite = false) {
let { publicKey, privateKey } = await getExtensionSettingsAsync()
let nickname
while (true) {
nickname = await dialogManager.prompt("Pick a nickname")
if (nickname) break
if (nickname === null) return
await dialogManager.alert("Invalid nickname")
}
try {
if (overwrite || !privateKey || !publicKey) {
resetProfileSettings()
await generateKeys().then((keys) => {
publicKey = keys.publicKey
privateKey = keys.privateKey
})
setExtensionSetting('publicKey', publicKey)
setExtensionSetting('privateKey', privateKey)
}
await apiRequest('POST', '/profile', { nickname })
await dialogManager.alert(`Your nickname has been set to ${nickname}`)
} catch (error: any) {
resetProfileSettings()
await dialogManager.alert(error.message)
}
}
export async function purgeProfile(dialogManager: DialogManager) {
try {
if (!await dialogManager.confirm("This will purge all of your online and offline profile data.\nStill wanna continue?")) return
await apiRequest('POST', '/profile/purge', {})
resetProfileSettings()
await dialogManager.alert(`Your profile has been purged`)
} catch (error: any) {
await dialogManager.alert(error.message)
}
}
export async function getProfile() {
let { publicKey, privateKey } = await getExtensionSettingsAsync()
return (await apiRequest('GET', '/profile', { publicKey })) as { nickname: string, score: number, publickKey: string }
}
export function friendlyPublicKey(publicKey: string | null) {
// This is copy paste of Madiator Finder's friendly public key
return `${publicKey?.substring(0, 32)}...`
}
function download(data: string, filename: string, type: string) {
const file = new Blob([data], { type: type })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
setTimeout(() => {
document.body.removeChild(a)
window.URL.revokeObjectURL(url)
})
}
// Using callback here because there is no good solution for detecting cancel event
export function inputKeyFile(callback: (file: File | null) => void) {
const input = document.createElement("input")
input.type = 'file'
input.accept = '.wol-keys.json'
input.click()
input.addEventListener("change", () => callback(input.files?.[0] ?? null))
}
interface ExportedProfileKeysFile {
publicKey: string
privateKey: string
}
export async function exportProfileKeysAsFile() {
const { publicKey, privateKey } = await getExtensionSettingsAsync()
const json = JSON.stringify({
publicKey,
privateKey
})
download(json, `watch-on-lbry-profile-export-${friendlyPublicKey(publicKey)}.wol-keys.json`, 'application/json')
}
export async function importProfileKeysFromFile(dialogManager: DialogManager, file: File) {
try {
let settings = await getExtensionSettingsAsync()
if (settings.publicKey && !await dialogManager.confirm(
"This will overwrite your old keypair." +
"\nStill wanna continue?\n\n" +
"NOTE: Without keypair you can't purge your data online.\n" +
"So if you wish to purge, please use purging instead."
)) return false
const json = await getFileContent(file)
if (!json) return false
const { publicKey, privateKey } = JSON.parse(json) as ExportedProfileKeysFile
setExtensionSetting('publicKey', publicKey)
setExtensionSetting('privateKey', privateKey)
return true
} catch (error: any) {
await dialogManager.alert(error.message)
return false
}
}