-
Notifications
You must be signed in to change notification settings - Fork 228
fix: scope the nostr permission presets to what each one describes #3600
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| import { EventKind } from "~/extension/providers/nostr/types"; | ||
| import { PermissionMethodNostr } from "~/types"; | ||
|
|
||
| /** | ||
| * Event kinds the "reasonable" preset auto-approves. | ||
| * | ||
| * Limited to posting and social kinds. Kinds that change who the account is | ||
| * (profile, contacts, relay list), speak privately as the user, or authenticate | ||
| * the user to a relay or a website are deliberately absent: those always ask. | ||
| */ | ||
| export const REASONABLE_PRESET_EVENT_KINDS: EventKind[] = [ | ||
| EventKind.Text, | ||
| EventKind.Repost, | ||
| EventKind.React, | ||
| EventKind.ZapRequest, | ||
| EventKind.MuteList, | ||
| EventKind.Bookmarks, | ||
| EventKind.LongNote, | ||
| EventKind.ProfileBadge, | ||
| EventKind.CreateBadge, | ||
| EventKind.AppData, | ||
| ]; | ||
|
|
||
| /** | ||
| * Event kinds that are never auto-approved by a preset. Each one either changes | ||
| * the account's identity or authenticates as the user, so each is confirmed | ||
| * individually. | ||
| */ | ||
| export const ALWAYS_CONFIRMED_EVENT_KINDS: EventKind[] = [ | ||
| EventKind.Metadata, | ||
| EventKind.Contacts, | ||
| EventKind.DM, | ||
| EventKind.RelayList, | ||
| EventKind.Authenticate, | ||
| EventKind.RemoteSign, | ||
| EventKind.HTTPAuth, | ||
| EventKind.UploadChunk, | ||
| ]; | ||
|
|
||
| /** Methods the "reasonable" preset auto-approves. */ | ||
| export const REASONABLE_PRESET_METHODS: PermissionMethodNostr[] = [ | ||
| PermissionMethodNostr.NOSTR_GETPUBLICKEY, | ||
| ]; | ||
|
|
||
| /** | ||
| * Reading the user's encrypted messages is never granted by a preset, not even | ||
| * by "I fully trust it" — every decryption is confirmed individually. | ||
| */ | ||
| export const NEVER_PRESET_GRANTED_METHODS: PermissionMethodNostr[] = [ | ||
| PermissionMethodNostr.NOSTR_DECRYPT, | ||
| ]; | ||
|
|
||
| /** Methods the "I fully trust it" preset auto-approves. */ | ||
| export const TRUST_FULLY_PRESET_METHODS: PermissionMethodNostr[] = | ||
| Object.values(PermissionMethodNostr).filter( | ||
| (method) => !NEVER_PRESET_GRANTED_METHODS.includes(method) | ||
| ); | ||
|
|
||
| /** | ||
| * Permission methods no preset grants any more, revoked once on upgrade. | ||
| * | ||
| * The revoke is unconditional: a permission row records only its method, not | ||
| * which preset or prompt created it, so a grant the user made deliberately | ||
| * through "don't ask again" is removed along with the preset's. It is limited | ||
| * to methods no preset grants today, so nobody ends up narrower than the | ||
| * preset they picked. `nostr/encrypt` is deliberately absent - "I fully trust | ||
| * it" still grants it, and revoking it would prompt those users for a | ||
| * permission that preset still covers. | ||
| */ | ||
| export const WITHDRAWN_PRESET_PERMISSIONS: string[] = [ | ||
| PermissionMethodNostr.NOSTR_DECRYPT, | ||
| ...ALWAYS_CONFIRMED_EVENT_KINDS.map( | ||
| (kind) => `${PermissionMethodNostr.NOSTR_SIGNMESSAGE}/${kind}` | ||
| ), | ||
| ]; | ||
108 changes: 108 additions & 0 deletions
108
src/extension/background-script/actions/nostr/__tests__/enable.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| import db from "~/extension/background-script/db"; | ||
| import { | ||
| ALWAYS_CONFIRMED_EVENT_KINDS, | ||
| REASONABLE_PRESET_EVENT_KINDS, | ||
| } from "~/common/utils/nostrPresets"; | ||
| import type { MessageAllowanceEnable, Sender } from "~/types"; | ||
| import { NostrPermissionPreset, PermissionMethodNostr } from "~/types"; | ||
|
|
||
| import enable from "../enable"; | ||
|
|
||
| console.error = jest.fn(); | ||
| console.info = jest.fn(); | ||
|
|
||
| let preset = NostrPermissionPreset.REASONABLE; | ||
| const openPromptMock = jest.fn(async (_message?: unknown) => ({ | ||
| data: { enabled: true, remember: true, preset }, | ||
| })); | ||
| jest.mock("~/common/lib/utils", () => ({ | ||
| openPrompt: (m: unknown) => openPromptMock(m), | ||
| })); | ||
|
|
||
| jest.mock("~/extension/background-script/state", () => ({ | ||
| getState: () => ({ | ||
| isUnlocked: jest.fn(async () => true), | ||
| getAccount: jest.fn(async () => ({ nostrPrivateKey: "11".repeat(32) })), | ||
| currentAccountId: "account-1", | ||
| settings: { browserNotifications: false }, | ||
| }), | ||
| })); | ||
|
|
||
| const HOST = "example.com"; | ||
| const sender = { origin: `https://${HOST}` } as Sender; | ||
|
|
||
| const connect = async () => { | ||
| await enable( | ||
| { | ||
| application: "LBE", | ||
| prompt: true, | ||
| action: "enable", | ||
| args: {}, | ||
| origin: { host: HOST, name: "Example", icon: "" }, | ||
| } as unknown as MessageAllowanceEnable, | ||
| sender | ||
| ); | ||
| return (await db.permissions.toArray()).map((p) => p.method); | ||
| }; | ||
|
|
||
| beforeEach(async () => { | ||
| await db.allowances.clear(); | ||
| await db.permissions.clear(); | ||
| openPromptMock.mockClear(); | ||
| }); | ||
|
|
||
| describe("nostr enable presets", () => { | ||
| test("the reasonable preset grants posting kinds only", async () => { | ||
| preset = NostrPermissionPreset.REASONABLE; | ||
| const granted = await connect(); | ||
|
|
||
| for (const kind of REASONABLE_PRESET_EVENT_KINDS) { | ||
| expect(granted).toContain( | ||
| `${PermissionMethodNostr.NOSTR_SIGNMESSAGE}/${kind}` | ||
| ); | ||
| } | ||
| expect(granted).toContain(PermissionMethodNostr.NOSTR_GETPUBLICKEY); | ||
| }); | ||
|
|
||
| test("the reasonable preset grants neither decryption nor encryption", async () => { | ||
| preset = NostrPermissionPreset.REASONABLE; | ||
| const granted = await connect(); | ||
|
|
||
| expect(granted).not.toContain(PermissionMethodNostr.NOSTR_DECRYPT); | ||
| expect(granted).not.toContain(PermissionMethodNostr.NOSTR_ENCRYPT); | ||
| }); | ||
|
|
||
| test("the reasonable preset grants no identity or authentication kind", async () => { | ||
| preset = NostrPermissionPreset.REASONABLE; | ||
| const granted = await connect(); | ||
|
|
||
| for (const kind of ALWAYS_CONFIRMED_EVENT_KINDS) { | ||
| expect(granted).not.toContain( | ||
| `${PermissionMethodNostr.NOSTR_SIGNMESSAGE}/${kind}` | ||
| ); | ||
| } | ||
| }); | ||
|
|
||
| test("no preset grants decryption, including trust fully", async () => { | ||
| preset = NostrPermissionPreset.TRUST_FULLY; | ||
| const granted = await connect(); | ||
|
|
||
| expect(granted).not.toContain(PermissionMethodNostr.NOSTR_DECRYPT); | ||
| expect(granted).toContain(PermissionMethodNostr.NOSTR_ENCRYPT); | ||
| }); | ||
|
|
||
| test("the paranoid preset grants nothing", async () => { | ||
| preset = NostrPermissionPreset.PARANOID; | ||
| const granted = await connect(); | ||
|
|
||
| expect(granted).toEqual([]); | ||
| }); | ||
|
|
||
| test("every granted permission is persisted before enable resolves", async () => { | ||
| preset = NostrPermissionPreset.REASONABLE; | ||
| // no waiting: the grants must be awaited, not left running after the return | ||
| const granted = await connect(); | ||
|
|
||
| expect(granted).toHaveLength(REASONABLE_PRESET_EVENT_KINDS.length + 1); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.