Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ async window.nostr.signEvent(event): Event // returns the full event object sign
async window.nostr.nip04.encrypt(pubkey, plaintext): string // returns ciphertext+iv as specified in nip04
async window.nostr.nip04.decrypt(pubkey, ciphertext): string // takes ciphertext+iv as specified in nip04
async window.nostr.nip44.encrypt(pubkey, plaintext): string // takes pubkey, plaintext, returns ciphertext as specified in nip-44
async window.nostr.nip44.decrypt(pubkey, ciphertext): string // takes pubkey, ciphertext, returns plaintext as specified in nip-44
async window.nostr.nip44.decrypt(pubkey, ciphertext, options?: {platforms?: string[]}): string // takes pubkey, ciphertext, and optional platforms; returns plaintext as specified in nip-44
```

This extension is Chromium-only. For a maintained Firefox fork, see [nos2x-fox](https://diegogurpegui.com/nos2x-fox/).
Expand Down
86 changes: 83 additions & 3 deletions extension/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,16 @@ async function handleContentScriptMessage({type, params, host}) {
// do the operation before asking (because we'll show the encryption/decryption results in the popup
const finalResult = await performOperation(type, params)

if (finalResult?.error?.code === 'nip44-decrypt-guard') {
releasePromptMutex()
return {error: {message: finalResult.error.message}}
}

let allowed = await getPermissionStatus(
host,
type,
type === 'signEvent' ? params.event : undefined
type === 'signEvent' ? params.event : undefined,
type === 'nip44.decrypt' ? params.platforms : undefined
)

if (allowed === true) {
Expand Down Expand Up @@ -182,11 +188,57 @@ async function handleContentScriptMessage({type, params, host}) {
}
}

if (
type === 'nip44.decrypt' &&
params.platforms &&
typeof finalResult === 'string'
) {
try {
assertDecryptedMatchesPlatforms(finalResult, params.platforms)
} catch (err) {
return {error: {message: err.message}}
}
}

// the call was authorized, so we just return the result we had from before
return finalResult
}
}

function assertDecryptedMatchesPlatforms(plaintext, platforms) {
const mismatch = new Error(
'website is trying to decrypt a message from a different platform'
)
let parsed
try {
parsed = JSON.parse(plaintext)
} catch (_) {
throw mismatch
}
if (
typeof parsed !== 'object' ||
parsed === null ||
Array.isArray(parsed) ||
!Object.prototype.hasOwnProperty.call(parsed, 'tags') ||
!Array.isArray(parsed.tags)
) {
throw mismatch
}
if (
!parsed.tags.every(
tag => Array.isArray(tag) && tag.every(item => typeof item === 'string')
)
) {
throw mismatch
}
let matches = parsed.tags.some(
tag =>
tag[0] === 'platforms' &&
tag.slice(1).some(item => platforms.includes(item))
)
if (!matches) throw mismatch
}

async function performOperation(type, params) {
let results = await browser.storage.local.get('private_key')
if (!results || !results.private_key) {
Expand Down Expand Up @@ -220,9 +272,37 @@ async function performOperation(type, params) {
return nip44.v2.encrypt(plaintext, key)
}
case 'nip44.decrypt': {
const {peer, ciphertext} = params
const {peer, ciphertext, platforms} = params
if (platforms !== undefined) {
if (
!Array.isArray(platforms) ||
!platforms.every(p => typeof p === 'string')
) {
return {
error: {
message: 'platforms must be an array of strings',
code: 'nip44-decrypt-guard'
}
}
}
if (platforms.length === 0) {
return {
error: {
message: 'platforms must not be empty',
code: 'nip44-decrypt-guard'
}
}
}
if (platforms.length > 20 || platforms.reduce((acc, p) => acc + p.length, 0) > 1000) {
return {
error: {
message: 'platforms must be less than 20 and the total length of the platforms must be less than 1000',
code: 'nip44-decrypt-guard'
}
}
}
}
const key = getSharedSecret(sk, peer)

return nip44.v2.decrypt(ciphertext, key)
}
}
Expand Down
45 changes: 44 additions & 1 deletion extension/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,32 @@ function matchConditions(conditions, event) {
return true
}

export async function getPermissionStatus(host, type, event) {
function matchDecryptPlatforms(conditions, platforms) {
if (conditions?.platforms) {
if (!platforms || !platforms.length) return false
return platforms.every(platform =>
conditions.platforms.includes(platform)
)
}

return true
}

export function getPermissionName(type, params) {
if (type === 'nip44.decrypt') {
let platforms = params?.platforms
if (Array.isArray(platforms) && platforms.length > 0) {
return `decrypt events for ${
platforms.length === 1 ? 'platform' : 'platforms'
}: ${platforms.map(platform => JSON.stringify(platform)).join(', ')}`
}
return 'decrypt all of your messages'
}

return PERMISSION_NAMES[type]
}

export async function getPermissionStatus(host, type, event, platforms) {
let {policies} = await browser.storage.local.get('policies')

let answers = [true, false]
Expand All @@ -40,6 +65,12 @@ export async function getPermissionStatus(host, type, event) {
// or it will end up returning undefined at the end
continue
}
} else if (type === 'nip44.decrypt') {
if (matchDecryptPlatforms(conditions, platforms)) {
return accept
} else {
continue
}
} else {
return accept // may be true or false
}
Expand All @@ -64,6 +95,18 @@ export async function updatePermission(host, type, accept, conditions) {
conditions.kinds[kind] = true
})
}
if (existingConditions.platforms && conditions.platforms) {
existingConditions.platforms.forEach(platform => {
if (!conditions.platforms.includes(platform)) {
conditions.platforms.push(platform)
}
})
if (conditions.platforms.length > 50) {
// storing up to 50 platforms is more than enough
console.warn('platforms list is too long, truncating to 50')
conditions.platforms = conditions.platforms.slice(-50)
}
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions extension/nostr-provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ window.nostr = {
return window.nostr._call('nip44.encrypt', {peer, plaintext})
},

async decrypt(peer, ciphertext) {
return window.nostr._call('nip44.decrypt', {peer, ciphertext})
async decrypt(peer, ciphertext, options) {
return window.nostr._call('nip44.decrypt', {peer, ciphertext, platforms: options?.platforms})
}
},

Expand Down
4 changes: 3 additions & 1 deletion extension/options.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,9 @@ function Options() {
<td>
{conditions.kinds
? `kinds: ${Object.keys(conditions.kinds).join(', ')}`
: 'always'}
: conditions.platforms
? `platforms: ${conditions.platforms.map(platform => JSON.stringify(platform)).join(', ')}`
: 'always'}
</td>
<td>
{new Date(created_at * 1000)
Expand Down
32 changes: 29 additions & 3 deletions extension/prompt.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import browser from 'webextension-polyfill'
import {createRoot} from 'react-dom/client'
import React from 'react'

import {PERMISSION_NAMES} from './common'
import {getPermissionName} from './common'

function Prompt() {
let qs = new URLSearchParams(location.search)
Expand All @@ -19,13 +19,22 @@ function Prompt() {
params = null
}

let platforms =
type === 'nip44.decrypt' && Array.isArray(params?.platforms)
? params.platforms
: null
let platformConditions = platforms?.length
? {platforms}
: null

return (
<>
<b style={{display: 'block', textAlign: 'center', fontSize: '200%'}}>
{host}
</b>{' '}
<p style={{margin: 0}}>
is requesting your permission to <b>{PERMISSION_NAMES[type]}:</b>
is requesting your permission to{' '}
<b>{getPermissionName(type, params)}:</b>
</p>
{params && (
<div style={{width: '100%', maxHeight: '200px', overflowY: 'scroll'}}>
Expand Down Expand Up @@ -63,7 +72,7 @@ function Prompt() {
gap: '0.5rem'
}}
>
{event?.kind === undefined && (
{event?.kind === undefined && !platformConditions && (
<button
style={{marginTop: '5px'}}
onClick={authorizeHandler(
Expand All @@ -85,6 +94,15 @@ function Prompt() {
authorize kind {event.kind} forever
</button>
)}
{platformConditions && (
<button
style={{marginTop: '5px'}}
onClick={authorizeHandler(true, platformConditions)}
>
authorize {platforms.length === 1 ? 'platform' : 'platforms'}{' '}
{platforms.map(platform => JSON.stringify(platform)).join(', ')} forever
</button>
)}
<button style={{marginTop: '5px'}} onClick={authorizeHandler(true)}>
authorize just this
</button>
Expand All @@ -107,6 +125,14 @@ function Prompt() {
>
reject kind {event.kind} forever
</button>
) : platformConditions ? (
<button
style={{marginTop: '5px'}}
onClick={authorizeHandler(false, platformConditions)}
>
reject {platforms.length === 1 ? 'platform' : 'platforms'}{' '}
{platforms.map(platform => JSON.stringify(platform)).join(', ')} forever
</button>
) : (
<button
style={{marginTop: '5px'}}
Expand Down