Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
54df79c
add NSE target and add keychain capability to NSE and Quiet
islathehut Feb 18, 2026
97f1731
Pass keys from backend to state manager and write to keychain
islathehut Feb 25, 2026
7700b4f
Properly store keys in ios keychain
islathehut Mar 4, 2026
76565d7
Don't store in state manager, simplify model that is sent to frontend…
islathehut Mar 4, 2026
12d20d2
Add comments
islathehut Mar 5, 2026
c4a2a23
Update CommunicationModule.swift
islathehut Mar 5, 2026
bb1e67e
Merge branch 'develop' into feat/3091-store-sigchain-data-on-keychain
islathehut Mar 5, 2026
62978af
Update KeychainHandler.swift
islathehut Mar 5, 2026
10f5549
Update CHANGELOG.md
islathehut Mar 5, 2026
dcc4bfc
Pass actual team name since updates can happen when creating a chain …
islathehut Mar 5, 2026
9689afe
Merge branch 'develop' into feat/3091-store-sigchain-data-on-keychain
islathehut Mar 10, 2026
406ef6b
Move update user profiles to a separate saga
islathehut Mar 11, 2026
f9391fc
Pass updated user profiles to native ios code
islathehut Mar 11, 2026
1497029
Persist user metadata in native storage
islathehut Mar 13, 2026
e68a238
Merge branch 'develop' into feat/3091-store-user-metadata-in-native
islathehut Mar 13, 2026
35cb3c1
Clean up logs and remove testing code
islathehut Mar 13, 2026
7189680
Fix tests
islathehut Mar 13, 2026
7476ffc
Update CHANGELOG.md
islathehut Mar 13, 2026
fabb2a7
Fix tests
islathehut Mar 13, 2026
dae7fe1
Merge branch 'develop' into feat/3091-store-sigchain-data-on-keychain
islathehut Mar 13, 2026
edc9d92
Update and move updateUserProfiles tests
islathehut Mar 13, 2026
06bb47d
Fix factories test
islathehut Mar 13, 2026
e7965b6
fix listener leak
adrastaea Mar 16, 2026
caa4a33
Merge branch 'develop' into feat/3091-store-user-metadata-in-native
islathehut Mar 16, 2026
461342d
Fix state manager stuff
islathehut Mar 16, 2026
0350734
add a default title and body so that message is not treated as silent
adrastaea Mar 23, 2026
f05c086
fix fcm sending token before js sets up
adrastaea Mar 23, 2026
5512964
fix first join token flush
adrastaea Mar 24, 2026
bbbbd4c
merge feat/3091-store-sigchain-data-on-keychain
adrastaea Mar 24, 2026
0cd3ae8
merge feat/3091-store-user-metadata-in-native
adrastaea Mar 24, 2026
f90554e
merge nse-test: NSE business logic + orbitDb helper
adrastaea Mar 24, 2026
a08be81
fix NSE UserDefaults app group + add NSE_JWT_SECRET env placeholder
adrastaea Mar 24, 2026
1e07df2
update qss submodule: NSE_JWT_SECRET + lint fixes
adrastaea Mar 24, 2026
8c45568
fix NSEMsgpack: map16 header + float64 timestamp to match msgpackr
adrastaea Mar 24, 2026
01009ae
fix NSE keychain read + qssUrl scheme (ws→http)
adrastaea Mar 24, 2026
e82de2e
fix ISO8601 fractional seconds + session 4 audit
adrastaea Mar 24, 2026
d2b16a4
session 5: loop complete, feature ready for testing
adrastaea Mar 24, 2026
420cacf
update auth submodule to match qss (397a62f)
adrastaea Mar 25, 2026
a98461f
working prototype
adrastaea Mar 25, 2026
a12cc56
Implement NSE sync timestamp handling and related socket events; impr…
adrastaea Mar 26, 2026
134181f
pass qss_endpoint to nse from backend
adrastaea Mar 26, 2026
1961e7e
avoid dropping key after writing, ensure qss url is shared at join
adrastaea Mar 30, 2026
34ec11b
use monotonic sequence for log pulling, pause qss when minimized
adrastaea Apr 1, 2026
efe168b
disconnect more gracefully
adrastaea Apr 1, 2026
6335287
fix socket lifecycle listeners
adrastaea Apr 1, 2026
4111269
rm noisy debugs
adrastaea Apr 1, 2026
f17ea88
cleanup dead code
adrastaea Apr 2, 2026
b48bf7b
refactor: replace hardcoded userDefaultsSuite string with constant
adrastaea Apr 2, 2026
7c89b36
Merge remote-tracking branch 'origin/develop' into decrypt-in-nse
adrastaea Apr 2, 2026
be4b988
minor pr cleaning
adrastaea Apr 2, 2026
0ea4782
more cleanup
adrastaea Apr 2, 2026
3b685c9
test fixes
adrastaea Apr 2, 2026
7916588
fix lodash dependency
adrastaea Apr 2, 2026
2953e0b
add some additional test coverage
adrastaea Apr 3, 2026
98de8af
tombstone notification tokens when leaving;
adrastaea Apr 6, 2026
5b5336d
dedupe keychain handling; breakout shared services;
adrastaea Apr 6, 2026
17056f5
fix tests; move qss url emit to nse to qss.service
adrastaea Apr 7, 2026
0ddc737
update qss submodule
adrastaea Apr 16, 2026
3a67515
add google-services.json to .gitignore
adrastaea Apr 16, 2026
e1be0ef
Merge remote-tracking branch 'origin/develop' into decrypt-in-nse
adrastaea Apr 16, 2026
55c50aa
update changelog
adrastaea Apr 16, 2026
ced2551
include platform when registering
adrastaea Apr 17, 2026
d3dc836
update qss pointer
adrastaea Apr 17, 2026
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: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ packages/.DS_Store
*.log
clangd/
**/.claude/settings.local.json
.claude/worktrees


# Yarn
Expand All @@ -22,3 +23,4 @@ clangd/

# Firebase
GoogleService-Info.plist
google-services.json
2 changes: 1 addition & 1 deletion 3rd-party/qss
Submodule qss updated 41 files
+2 −1 .dockerignore
+1 −0 .gitignore
+1 −1 .npmrc
+2 −2 Dockerfile
+3 −3 app/.env.dev
+3 −3 app/.env.local
+5 −0 app/.env.local.docker
+9 −6 app/docker-compose.quiet.yml
+13 −1 app/src/client/cli/prompts/push.prompt.ts
+47 −0 app/src/migrations/Migration20260401170000.ts
+2 −0 app/src/nest/app/app.module.ts
+11 −0 app/src/nest/communities/storage/entities/log-sync.entity.ts
+252 −37 app/src/nest/communities/storage/log-entry-sync.storage.service.spec.ts
+207 −26 app/src/nest/communities/storage/log-entry-sync.storage.service.ts
+183 −72 app/src/nest/communities/sync/log-entry-sync.service.spec.ts
+178 −12 app/src/nest/communities/sync/log-entry-sync.service.ts
+1 −0 app/src/nest/communities/types.ts
+200 −0 app/src/nest/nse-auth/nse-auth.controller.spec.ts
+91 −0 app/src/nest/nse-auth/nse-auth.controller.ts
+21 −0 app/src/nest/nse-auth/nse-auth.module.ts
+293 −0 app/src/nest/nse-auth/nse-auth.service.spec.ts
+355 −0 app/src/nest/nse-auth/nse-auth.service.ts
+46 −0 app/src/nest/nse-auth/nse-jwt-auth.guard.ts
+203 −28 app/src/nest/qps/push/push.service.spec.ts
+119 −51 app/src/nest/qps/push/push.service.ts
+116 −0 app/src/nest/qps/qps.service.spec.ts
+82 −27 app/src/nest/qps/qps.service.ts
+57 −16 app/src/nest/qps/ucan/ucan.service.spec.ts
+12 −3 app/src/nest/qps/ucan/ucan.service.ts
+2 −0 app/src/nest/qps/ucan/ucan.types.ts
+1 −0 app/src/nest/storage/postgres/const.ts
+9 −4 app/src/nest/utils/config/env_vars.ts
+18 −4 app/src/nest/websocket/handlers/log-entry-sync.handler.ts
+16 −3 app/src/nest/websocket/handlers/qps.handler.spec.ts
+1 −0 app/src/nest/websocket/handlers/qps.handler.ts
+9 −1 app/src/nest/websocket/handlers/types/log-entry-sync.types.ts
+1 −0 app/src/nest/websocket/handlers/types/qps.types.ts
+1 −4 app/test/e2e-tests/communities/communities.complete.e2e.spec.ts
+1 −1 app/tsconfig.build.json
+6,492 −12,150 pnpm-lock.yaml
+4 −4 tools/push-notification-test/README.md
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Features

* Adds ios push notification support [#3087](https://github.com/TryQuiet/quiet/issues/3087)

### Fixes

### Chores
Expand Down Expand Up @@ -34,7 +36,8 @@
* Self-assign the member role when joining with QSS [#3058](https://github.com/TryQuiet/quiet/issues/3058)
* Use LFA-based identity in OrbitDB
* Requests iOS notification permissions when app launches [#3079](https://github.com/TryQuiet/quiet/issues/3079)
* Adds push notification service [#3086](https://github.com/TryQuiet/quiet/issues/3086)
* Store LFA keys in IOS keychain for notifications [#3091](https://github.com/TryQuiet/quiet/issues/3091)
* Store user metadata in IOS native storage for notifications [#3091](https://github.com/TryQuiet/quiet/issues/3091)

## [6.6.2]

Expand Down
23 changes: 18 additions & 5 deletions packages/backend/src/nest/auth/services/crypto/crypto.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import {
import { ChainServiceBase } from '../chainServiceBase'
import { SigChain } from '../../sigchain'
import { asymmetric, Keyset, Member, SignedEnvelope, EncryptStreamTeamPayload } from '@localfirst/auth'
import { KeyMap } from '@localfirst/auth/team/selectors'
import { DEFAULT_SEARCH_OPTIONS, MemberSearchOptions } from '../members/types'
import { createLogger } from '../../../common/logger'
import { KeyMetadata } from '3rd-party/auth/packages/crdx/dist'
import { KeyMetadata } from '@localfirst/crdx'

const logger = createLogger('auth:cryptoService')

Expand All @@ -36,6 +37,22 @@ class CryptoService extends ChainServiceBase {
})
}

public getPublicKeysForAllMembers(includeSelf: boolean = false): Keyset[] {
const members = this.sigChain.users.getAllUsers()
const keysByMember = []
for (const member of members) {
if (member.userId === this.sigChain.context.user.userId && !includeSelf) {
continue
}
keysByMember.push(member.keys)
}
return keysByMember
}

public getAllKeys(): KeyMap {
return this.sigChain.team!.allKeys()
}

public sign(message: any): SignedEnvelope {
return this.sigChain.team!.sign(message)
}
Expand Down Expand Up @@ -118,12 +135,10 @@ class CryptoService extends ChainServiceBase {
): DecryptedPayload<T> {
let contents: T
if (typeof encrypted.contents === 'string') {
logger.debug('Converting Base64 string to Buffer')
encrypted.contents = Buffer.from(encrypted.contents, 'base64')
}
// Handle numeric array case (JSON-encoded Uint8Array)
else if (Array.isArray(encrypted.contents)) {
logger.debug('Converting numeric array to Buffer')
encrypted.contents = Buffer.from(encrypted.contents)
}
// Handle Node.js Buffer JSON representation ({"type":"Buffer","data":[...]})
Expand All @@ -133,12 +148,10 @@ class CryptoService extends ChainServiceBase {
(encrypted.contents as any).type === 'Buffer' &&
Array.isArray((encrypted.contents as any).data)
) {
logger.debug('Converting JSON Buffer representation to Buffer')
encrypted.contents = Buffer.from((encrypted.contents as any).data)
}
// Handle object with numeric keys (parsed JSON representation)
else if (encrypted.contents && typeof encrypted.contents === 'object' && !Buffer.isBuffer(encrypted.contents)) {
logger.debug('Converting object with numeric keys to Buffer')
const nums = Object.keys(encrypted.contents)
.filter(key => /^\d+$/.test(key))
.map(key => (encrypted.contents as any)[key] as number)
Expand Down
101 changes: 101 additions & 0 deletions packages/backend/src/nest/auth/sigchain.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { LocalDbService } from '../local-db/local-db.service'
import { LocalDbModule } from '../local-db/local-db.module'
import { TestModule } from '../common/test.module'
import { SigChainModule } from './sigchain.service.module'
import { SigChain } from './sigchain'
import { SocketEvents } from '@quiet/types'
import waitForExpect from 'wait-for-expect'

const logger = createLogger('auth:sigchainManager.spec')

Expand Down Expand Up @@ -91,3 +94,101 @@ describe('SigChainService', () => {
expect(sigChainService.getChain({ teamName: TEAM_NAME2 })).toBeDefined()
})
})

describe('SigChainService - listener lifecycle', () => {
let module: TestingModule
let sigChainService: SigChainService
let localDbService: LocalDbService

beforeAll(async () => {
module = await Test.createTestingModule({
imports: [TestModule, SigChainModule, LocalDbModule],
}).compile()
sigChainService = await module.resolve(SigChainService)
localDbService = await module.resolve(LocalDbService)
await localDbService.open()
})

afterAll(async () => {
await localDbService.close()
await module.close()
})

it('does not accumulate listeners on chains when switching active chain', async () => {
const chainA: SigChain = await sigChainService.createChain('leakA', 'alice', true)
// chainA is active: one listener attached
expect(chainA.listenerCount('updated')).toBe(1)

const chainB: SigChain = await sigChainService.createChain('leakB', 'bob', true)
// Active switched A → B. detachSocketListeners(A) must have removed A's listener.
expect(chainA.listenerCount('updated')).toBe(0)
expect(chainB.listenerCount('updated')).toBe(1)

sigChainService.setActiveChain('leakA')
// Active switched B → A. detachSocketListeners(B) must have removed B's listener,
// and attachSocketListeners(A) adds exactly one to A.
expect(chainA.listenerCount('updated')).toBe(1)
expect(chainB.listenerCount('updated')).toBe(0)
})

it('does not emit iOS-native key or device events on non-ios platforms', async () => {
const emitSpy = jest.spyOn(sigChainService.serverIoProvider.io, 'emit')

await sigChainService.createChain('desktopOnly', 'alice', true)

expect(emitSpy.mock.calls.filter(([event]) => event === SocketEvents.KEYS_UPDATED)).toHaveLength(0)
expect(emitSpy.mock.calls.filter(([event]) => event === SocketEvents.DEVICE_CREDENTIALS_UPDATED)).toHaveLength(0)
})

it('emits new keys to iOS once and does not resend already-stored keys', async () => {
const originalPlatform = process.platform
Object.defineProperty(process, 'platform', { value: 'ios' })

try {
const emitSpy = jest.spyOn(sigChainService.serverIoProvider.io, 'emit')
const chain = await sigChainService.createChain('iosKeys', 'alice', true)
const teamId = chain.team!.id

await waitForExpect(async () => {
const keyCalls = emitSpy.mock.calls.filter(([event]) => event === SocketEvents.KEYS_UPDATED)
expect(keyCalls).toHaveLength(1)
expect((keyCalls[0][1] as { keys: unknown[] }).keys.length).toBeGreaterThan(0)
const storedKeys = await localDbService.getKeysStoredInKeychain(teamId)
expect(storedKeys).toHaveLength((keyCalls[0][1] as { keys: unknown[] }).keys.length)
})

const storedKeysAfterFirstUpdate = await localDbService.getKeysStoredInKeychain(teamId)

chain.emit('updated')

await new Promise(resolve => setTimeout(resolve, 25))

expect(emitSpy.mock.calls.filter(([event]) => event === SocketEvents.KEYS_UPDATED)).toHaveLength(1)
expect(await localDbService.getKeysStoredInKeychain(teamId)).toEqual(storedKeysAfterFirstUpdate)
} finally {
Object.defineProperty(process, 'platform', { value: originalPlatform })
}
})

it('emits device credentials for the NSE on ios', async () => {
const originalPlatform = process.platform
Object.defineProperty(process, 'platform', { value: 'ios' })

try {
const emitSpy = jest.spyOn(sigChainService.serverIoProvider.io, 'emit')
const chain = await sigChainService.createChain('iosDeviceCredentials', 'alice', true)

await waitForExpect(() => {
const deviceCalls = emitSpy.mock.calls.filter(([event]) => event === SocketEvents.DEVICE_CREDENTIALS_UPDATED)
expect(deviceCalls).toHaveLength(1)
expect(deviceCalls[0][1]).toEqual({
deviceId: chain.device.deviceId,
teamId: chain.team!.id,
signingPrivateKey: chain.device.keys.signature.secretKey,
})
})
} finally {
Object.defineProperty(process, 'platform', { value: originalPlatform })
}
})
})
Loading
Loading