refactor: key rotation (#2383)

This commit is contained in:
Mo
2023-08-04 09:25:28 -05:00
committed by GitHub
parent a7f266bb68
commit 494436bdb6
65 changed files with 1354 additions and 1232 deletions

View File

@@ -4,8 +4,8 @@ import { WebSocketsService } from './../Services/Api/WebsocketsService'
import { MigrationService } from './../Services/Migration/MigrationService'
import { LegacyApiService } from './../Services/Api/ApiService'
import { FeaturesService } from '@Lib/Services/Features/FeaturesService'
import { SNPreferencesService } from './../Services/Preferences/PreferencesService'
import { SNProtectionService } from './../Services/Protection/ProtectionService'
import { PreferencesService } from './../Services/Preferences/PreferencesService'
import { ProtectionService } from './../Services/Protection/ProtectionService'
import { SessionManager } from './../Services/Session/SessionManager'
import { HttpService, HttpServiceInterface, UserRegistrationResponseBody } from '@standardnotes/api'
import { ApplicationIdentifier, compareVersions, ProtocolVersion, KeyParamsOrigination } from '@standardnotes/common'
@@ -32,7 +32,7 @@ import {
FeaturesClientInterface,
ItemManagerInterface,
SyncServiceInterface,
UserClientInterface,
UserServiceInterface,
MutatorClientInterface,
StatusServiceInterface,
AlertService,
@@ -74,7 +74,6 @@ import {
VaultUserServiceInterface,
VaultInviteServiceInterface,
NotificationServiceEvent,
VaultServiceEvent,
VaultLockServiceInterface,
} from '@standardnotes/services'
import {
@@ -297,7 +296,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
const uninstall = syncService.addEventObserver(syncEventCallback)
this.serviceObservers.push(uninstall)
const protectionService = this.dependencies.get<SNProtectionService>(TYPES.ProtectionService)
const protectionService = this.dependencies.get<ProtectionService>(TYPES.ProtectionService)
this.serviceObservers.push(
protectionService.addEventObserver((event) => {
if (event === ProtectionEvent.UnprotectedSessionBegan) {
@@ -329,7 +328,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
}),
)
const preferencesService = this.dependencies.get<SNPreferencesService>(TYPES.PreferencesService)
const preferencesService = this.dependencies.get<PreferencesService>(TYPES.PreferencesService)
this.serviceObservers.push(
preferencesService.addEventObserver(() => {
void this.notifyEvent(ApplicationEvent.PreferencesChanged)
@@ -1141,7 +1140,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
this.dependencies.get(TYPES.SharedVaultService),
NotificationServiceEvent.NotificationReceived,
)
this.events.addEventHandler(this.dependencies.get(TYPES.SharedVaultService), VaultServiceEvent.VaultRootKeyRotated)
this.events.addEventHandler(this.dependencies.get(TYPES.SharedVaultService), SyncEvent.ReceivedRemoteSharedVaults)
this.events.addEventHandler(
@@ -1267,8 +1265,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return this.dependencies.get<SyncServiceInterface>(TYPES.SyncService)
}
public get user(): UserClientInterface {
return this.dependencies.get<UserClientInterface>(TYPES.UserService)
public get user(): UserServiceInterface {
return this.dependencies.get<UserServiceInterface>(TYPES.UserService)
}
public get settings(): SettingsService {

File diff suppressed because it is too large Load Diff

View File

@@ -153,6 +153,9 @@ export const TYPES = {
IsVaultOwner: Symbol.for('IsVaultOwner'),
RemoveItemsFromMemory: Symbol.for('RemoveItemsFromMemory'),
ReencryptTypeAItems: Symbol.for('ReencryptTypeAItems'),
DecryptErroredPayloads: Symbol.for('DecryptErroredPayloads'),
GetKeyPairs: Symbol.for('GetKeyPairs'),
ChangeVaultStorageMode: Symbol.for('ChangeVaultStorageMode'),
// Mappers
SessionStorageMapper: Symbol.for('SessionStorageMapper'),

View File

@@ -1,4 +1,4 @@
import { SNPreferencesService } from '../Preferences/PreferencesService'
import { PreferencesService } from '../Preferences/PreferencesService'
import { GenericItem, Environment, Platform } from '@standardnotes/models'
import {
InternalEventBusInterface,
@@ -68,7 +68,7 @@ describe('featuresService', () => {
features = {} as jest.Mocked<FeaturesService>
prefs = {} as jest.Mocked<SNPreferencesService>
prefs = {} as jest.Mocked<PreferencesService>
prefs.addEventObserver = jest.fn()
alerts = {} as jest.Mocked<AlertService>

View File

@@ -20,7 +20,7 @@ import {
StorageServiceInterface,
SubscriptionManagerInterface,
SyncServiceInterface,
UserClientInterface,
UserServiceInterface,
UserService,
} from '@standardnotes/services'
import { LegacyApiService, SessionManager } from '../Api'
@@ -37,7 +37,7 @@ describe('FeaturesService', () => {
let apiService: LegacyApiServiceInterface
let webSocketsService: WebSocketsService
let settingsService: SettingsClientInterface
let userService: UserClientInterface
let userService: UserServiceInterface
let syncService: SyncServiceInterface
let alertService: AlertService
let sessionManager: SessionsClientInterface

View File

@@ -43,7 +43,7 @@ import {
ItemManagerInterface,
SyncServiceInterface,
SessionsClientInterface,
UserClientInterface,
UserServiceInterface,
SubscriptionManagerInterface,
AccountEvent,
SubscriptionManagerEvent,
@@ -76,7 +76,7 @@ export class FeaturesService
private api: LegacyApiServiceInterface,
sockets: WebSocketsService,
private settings: SettingsClientInterface,
private user: UserClientInterface,
private user: UserServiceInterface,
private sync: SyncServiceInterface,
private alerts: AlertService,
private sessions: SessionsClientInterface,

View File

@@ -14,7 +14,7 @@ import {
MutatorClientInterface,
SyncServiceInterface,
} from '@standardnotes/services'
import { SNProtectionService } from '../Protection'
import { ProtectionService } from '../Protection'
import { ContentType } from '@standardnotes/domain-core'
export class ListedService extends AbstractService implements ListedClientInterface {
@@ -23,7 +23,7 @@ export class ListedService extends AbstractService implements ListedClientInterf
private itemManager: ItemManager,
private settingsService: SettingsService,
private httpSerivce: DeprecatedHttpService,
private protectionService: SNProtectionService,
private protectionService: ProtectionService,
private mutator: MutatorClientInterface,
private sync: SyncServiceInterface,
protected override internalEventBus: InternalEventBusInterface,

View File

@@ -56,6 +56,14 @@ describe('mutator service', () => {
return mutatorService.insertItem(note)
}
describe('insertItem', () => {
it('should throw if attempting to insert already inserted item', async () => {
const note = await insertNote('hello')
expect(mutatorService.insertItem(note)).rejects.toThrow()
})
})
describe('note modifications', () => {
it('pinning should not update timestamps', async () => {
const note = await insertNote('hello')

View File

@@ -358,6 +358,11 @@ export class MutatorService extends AbstractService implements MutatorClientInte
}
public async insertItem<T extends DecryptedItemInterface>(item: DecryptedItemInterface, setDirty = true): Promise<T> {
const existingItem = this.itemManager.findItem<T>(item.uuid)
if (existingItem) {
throw Error('Attempting to insert item that already exists')
}
if (setDirty) {
const mutator = CreateDecryptedMutatorForItem(item, MutationType.UpdateUserTimestamps)
const dirtiedPayload = mutator.getResult()

View File

@@ -17,7 +17,7 @@ import {
} from '@standardnotes/services'
import { ContentType } from '@standardnotes/domain-core'
export class SNPreferencesService
export class PreferencesService
extends AbstractService<PreferencesServiceEvent>
implements PreferenceServiceInterface, InternalEventHandlerInterface
{

View File

@@ -1,6 +1,6 @@
import { ChallengeService } from '../Challenge'
import { DiskStorageService } from '../Storage/DiskStorageService'
import { SNProtectionService } from './ProtectionService'
import { ProtectionService } from './ProtectionService'
import {
InternalEventBus,
InternalEventBusInterface,
@@ -28,10 +28,10 @@ describe('protectionService', () => {
let challengeService: ChallengeService
let storageService: DiskStorageService
let internalEventBus: InternalEventBusInterface
let protectionService: SNProtectionService
let protectionService: ProtectionService
const createService = () => {
return new SNProtectionService(encryptionService, mutator, challengeService, storageService, internalEventBus)
return new ProtectionService(encryptionService, mutator, challengeService, storageService, internalEventBus)
}
const createFile = (name: string, isProtected?: boolean) => {

View File

@@ -74,7 +74,7 @@ export const ProtectionSessionDurations = [
* like viewing a protected note, as well as managing how long that
* authentication should be valid for.
*/
export class SNProtectionService
export class ProtectionService
extends AbstractService<ProtectionEvent>
implements ProtectionsClientInterface, InternalEventHandlerInterface
{

View File

@@ -32,8 +32,9 @@ import {
ApplicationEvent,
ApplicationStageChangedEventPayload,
ApplicationStage,
GetKeyPairs,
} from '@standardnotes/services'
import { Base64String, PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { Base64String, PureCryptoInterface } from '@standardnotes/sncrypto-common'
import {
SessionBody,
ErrorTag,
@@ -103,6 +104,7 @@ export class SessionManager
private sessionStorageMapper: MapperInterface<Session, Record<string, unknown>>,
private legacySessionStorageMapper: MapperInterface<LegacySession, Record<string, unknown>>,
private workspaceIdentifier: string,
private _getKeyPairs: GetKeyPairs,
protected override internalEventBus: InternalEventBusInterface,
) {
super(internalEventBus)
@@ -215,11 +217,15 @@ export class SessionManager
}
public getPublicKey(): string {
return this.encryptionService.getKeyPair().publicKey
const keys = this._getKeyPairs.execute()
return keys.getValue().encryption.publicKey
}
public getSigningPublicKey(): string {
return this.encryptionService.getSigningKeyPair().publicKey
const keys = this._getKeyPairs.execute()
return keys.getValue().signing.publicKey
}
public get userUuid(): string {
@@ -625,15 +631,7 @@ export class SessionManager
newEmail: parameters.newEmail,
})
let oldKeyPair: PkcKeyPair | undefined
let oldSigningKeyPair: PkcKeyPair | undefined
try {
oldKeyPair = this.encryptionService.getKeyPair()
oldSigningKeyPair = this.encryptionService.getSigningKeyPair()
} catch (error) {
void error
}
const oldKeys = this._getKeyPairs.execute()
const processedResponse = await this.processChangeCredentialsResponse(
rawResponse,
@@ -644,13 +642,12 @@ export class SessionManager
if (!isErrorResponse(rawResponse)) {
if (InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)) {
const eventData: UserKeyPairChangedEventData = {
previous:
oldKeyPair && oldSigningKeyPair
? {
encryption: oldKeyPair,
signing: oldSigningKeyPair,
}
: undefined,
previous: !oldKeys.isFailed()
? {
encryption: oldKeys.getValue().encryption,
signing: oldKeys.getValue().signing,
}
: undefined,
current: {
encryption: parameters.newRootKey.encryptionKeyPair,
signing: parameters.newRootKey.signingKeyPair,

View File

@@ -9,6 +9,7 @@ export const VaultTests = {
'vaults/importing.test.js',
'vaults/asymmetric-messages.test.js',
'vaults/keypair-change.test.js',
'vaults/key-sharing.test.js',
'vaults/signatures.test.js',
'vaults/shared_vaults.test.js',
'vaults/invites.test.js',

View File

@@ -149,16 +149,24 @@ export class AppContext {
return this.application.asymmetric
}
get keyPair() {
return this.application.dependencies.get(TYPES.GetKeyPairs).execute().getValue().encryption
}
get signingKeyPair() {
return this.application.dependencies.get(TYPES.GetKeyPairs).execute().getValue().signing
}
get publicKey() {
return this.sessions.getPublicKey()
return this.keyPair.publicKey
}
get signingPublicKey() {
return this.sessions.getSigningPublicKey()
return this.signingKeyPair.publicKey
}
get privateKey() {
return this.encryption.getKeyPair().privateKey
return this.keyPair.privateKey
}
ignoreChallenges() {
@@ -453,34 +461,6 @@ export class AppContext {
return this.resolveWhenAsyncFunctionCompletes(objectToSpy, 'shareContactWithVaults')
}
resolveWhenSharedVaultKeyRotationInvitesGetSent(targetVault) {
return new Promise((resolve) => {
const objectToSpy = this.sharedVaults
sinon.stub(objectToSpy, 'handleVaultRootKeyRotatedEvent').callsFake(async (vault) => {
objectToSpy.handleVaultRootKeyRotatedEvent.restore()
const result = await objectToSpy.handleVaultRootKeyRotatedEvent(vault)
if (vault.systemIdentifier === targetVault.systemIdentifier) {
resolve()
}
return result
})
})
}
resolveWhenSharedVaultChangeInvitesAreSent(sharedVaultUuid) {
return new Promise((resolve) => {
const objectToSpy = this.sharedVaults
sinon.stub(objectToSpy, 'handleVaultRootKeyRotatedEvent').callsFake(async (vault) => {
objectToSpy.handleVaultRootKeyRotatedEvent.restore()
const result = await objectToSpy.handleVaultRootKeyRotatedEvent(vault)
if (vault.sharing.sharedVaultUuid === sharedVaultUuid) {
resolve()
}
return result
})
})
}
awaitUserPrefsSingletonCreation() {
const preferences = this.application.preferences.preferences
if (preferences) {

View File

@@ -1,4 +1,5 @@
import { AppContext } from './AppContext.js'
import * as Collaboration from './Collaboration.js'
export class VaultsContext extends AppContext {
constructor(params) {
@@ -6,17 +7,22 @@ export class VaultsContext extends AppContext {
}
async changeVaultName(vault, nameAndDesc) {
const sendDataChangePromise = this.resolveWhenAsyncFunctionCompletes(
this.sharedVaults._sendVaultDataChangeMessage,
'execute',
)
await this.vaults.changeVaultNameAndDescription(vault, {
name: nameAndDesc.name,
description: nameAndDesc.description,
})
}
await this.awaitPromiseOrThrow(sendDataChangePromise, undefined, 'Waiting for vault data change message to process')
getKeyPair() {
const result = this.application.dependencies.get(TYPES.GetKeyPairs).execute()
return result.getValue().encryption
}
getSigningKeyPair() {
const result = this.application.dependencies.get(TYPES.GetKeyPairs).execute()
return result.getValue().signing
}
async changePassword(password) {
@@ -51,4 +57,28 @@ export class VaultsContext extends AppContext {
async runAnyRequestToPreventRefreshTokenFromExpiring() {
await this.asymmetric.getInboundMessages()
}
async createSharedPasswordVault(password) {
const privateVault = await this.vaults.createUserInputtedPasswordVault({
name: 'Our Vault',
userInputtedPassword: password,
storagePreference: KeySystemRootKeyStorageMode.Ephemeral,
})
const note = await this.createSyncedNote('foo', 'bar')
await this.vaults.moveItemToVault(privateVault, note)
const sharedVault = await this.sharedVaults.convertVaultToSharedVault(privateVault)
console.log('createSharedPasswordVault > sharedVault:', sharedVault)
const { thirdPartyContext, deinitThirdPartyContext } = await Collaboration.inviteNewPartyToSharedVault(
this,
sharedVault,
)
await Collaboration.acceptAllInvites(thirdPartyContext)
return { sharedVault, thirdPartyContext, deinitThirdPartyContext }
}
}

View File

@@ -158,7 +158,8 @@ export async function registerOldUser({ application, email, password, version })
mode: SyncMode.DownloadFirst,
...syncOptions,
})
await application.encryption.decryptErroredPayloads()
await application.dependencies.get(TYPES.DecryptErroredPayloads).execute()
}
export function createStorageItemPayload(contentType) {

View File

@@ -1,3 +1,5 @@
import * as Factory from './lib/factory.js'
describe.skip('session invalidation tests to revisit', function () {
this.timeout(Factory.TwentySecondTimeout)

View File

@@ -189,12 +189,7 @@ describe('asymmetric messages', function () {
contactContext.lockSyncing()
const promise = context.resolveWhenAsyncFunctionCompletes(
context.sharedVaults._notifyVaultUsersOfKeyRotation,
'execute',
)
await context.vaults.rotateVaultRootKey(sharedVault)
await promise
const firstPartySpy = sinon.spy(context.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage')
const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage')
@@ -333,8 +328,8 @@ describe('asymmetric messages', function () {
it('sender keypair changed message should be signed using old key pair', async () => {
const { contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context)
const oldKeyPair = context.encryption.getKeyPair()
const oldSigningKeyPair = context.encryption.getSigningKeyPair()
const oldKeyPair = context.keyPair
const oldSigningKeyPair = context.signingKeyPair
await context.changePassword('new password')
@@ -360,8 +355,8 @@ describe('asymmetric messages', function () {
await context.changePassword('new password')
const newKeyPair = context.encryption.getKeyPair()
const newSigningKeyPair = context.encryption.getSigningKeyPair()
const newKeyPair = context.keyPair
const newSigningKeyPair = context.signingKeyPair
await contactContext.syncAndAwaitMessageProcessing()
@@ -413,12 +408,12 @@ describe('asymmetric messages', function () {
const usecase = context.application.dependencies.get(TYPES.ResendAllMessages)
const result = await usecase.execute({
keys: {
encryption: context.encryption.getKeyPair(),
signing: context.encryption.getSigningKeyPair(),
encryption: context.keyPair,
signing: context.signingKeyPair,
},
previousKeys: {
encryption: context.encryption.getKeyPair(),
signing: context.encryption.getSigningKeyPair(),
encryption: context.keyPair,
signing: context.signingKeyPair,
},
})

View File

@@ -31,8 +31,8 @@ describe('shared vault crypto', function () {
let recreatedContext = await Factory.createVaultsContextWithRealCrypto(appIdentifier)
await recreatedContext.launch()
expect(recreatedContext.encryption.getKeyPair()).to.not.be.undefined
expect(recreatedContext.encryption.getSigningKeyPair()).to.not.be.undefined
expect(recreatedContext.keyPair).to.not.be.undefined
expect(recreatedContext.signingKeyPair).to.not.be.undefined
await recreatedContext.deinit()
})

View File

@@ -28,7 +28,11 @@ describe('shared vault invites', function () {
const contact = await Collaboration.createTrustedContactForUserOfContext(context, contactContext)
const vaultInvite = (
await context.vaultInvites.inviteContactToSharedVault(sharedVault, contact, SharedVaultUserPermission.PERMISSIONS.Write)
await context.vaultInvites.inviteContactToSharedVault(
sharedVault,
contact,
SharedVaultUserPermission.PERMISSIONS.Write,
)
).getValue()
expect(vaultInvite).to.not.be.undefined
@@ -100,7 +104,11 @@ describe('shared vault invites', function () {
/** Sync the contact context so that they wouldn't naturally receive changes made before this point */
await contactContext.sync()
await context.vaultInvites.inviteContactToSharedVault(sharedVault, contact, SharedVaultUserPermission.PERMISSIONS.Write)
await context.vaultInvites.inviteContactToSharedVault(
sharedVault,
contact,
SharedVaultUserPermission.PERMISSIONS.Write,
)
/** Contact should now sync and expect to find note */
const promise = contactContext.awaitNextSyncSharedVaultFromScratchEvent()
@@ -214,31 +222,4 @@ describe('shared vault invites', function () {
await deinitContactContext()
})
it('sharing a vault with user inputted and ephemeral password should share the key as synced for the recipient', async () => {
const privateVault = await context.vaults.createUserInputtedPasswordVault({
name: 'My Private Vault',
userInputtedPassword: 'password',
storagePreference: KeySystemRootKeyStorageMode.Ephemeral,
})
const note = await context.createSyncedNote('foo', 'bar')
await context.vaults.moveItemToVault(privateVault, note)
const sharedVault = await context.sharedVaults.convertVaultToSharedVault(privateVault)
const { thirdPartyContext, deinitThirdPartyContext } = await Collaboration.inviteNewPartyToSharedVault(
context,
sharedVault,
)
await Collaboration.acceptAllInvites(thirdPartyContext)
const contextNote = thirdPartyContext.items.findItem(note.uuid)
expect(contextNote).to.not.be.undefined
expect(contextNote.title).to.equal('foo')
expect(contextNote.text).to.equal(note.text)
await deinitThirdPartyContext()
})
})

View File

@@ -110,7 +110,7 @@ describe('vault key management', function () {
await context.vaultLocks.lockNonPersistentVault(vault)
await Factory.expectThrowsAsync(
() => context.vaults.changeVaultOptions({ vault }),
() => context.vaults.changeVaultKeyOptions({ vault }),
'Attempting to change vault options on a locked vault',
)
})
@@ -206,7 +206,7 @@ describe('vault key management', function () {
})
})
describe('changeVaultOptions', () => {
describe('changeVaultKeyOptions', () => {
describe('change storage type', () => {
it('should not be able to change randomized vault from synced to local', async () => {
const vault = await context.vaults.createRandomizedVault({
@@ -214,13 +214,13 @@ describe('vault key management', function () {
description: 'test vault description',
})
const result = await context.vaults.changeVaultOptions({
const result = await context.vaults.changeVaultKeyOptions({
vault,
newStorageMode: KeySystemRootKeyStorageMode.Local,
})
expect(result.isFailed()).to.be.true
expect(result.getError()).to.equal('Vault uses randomized password and cannot change its storage preference')
expect(result.getError()).to.equal('Cannot change storage mode to non-synced for randomized vault')
})
it('should not be able to change randomized vault from synced to ephemeral', async () => {
@@ -229,13 +229,13 @@ describe('vault key management', function () {
description: 'test vault description',
})
const result = await context.vaults.changeVaultOptions({
const result = await context.vaults.changeVaultKeyOptions({
vault,
newStorageMode: KeySystemRootKeyStorageMode.Local,
newStorageMode: KeySystemRootKeyStorageMode.Ephemeral,
})
expect(result.isFailed()).to.be.true
expect(result.getError()).to.equal('Vault uses randomized password and cannot change its storage preference')
expect(result.getError()).to.equal('Cannot change storage mode to non-synced for randomized vault')
})
it('should change user password vault from synced to local', async () => {
@@ -248,7 +248,7 @@ describe('vault key management', function () {
let syncedKeys = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier)
const result = await context.vaults.changeVaultOptions({
const result = await context.vaults.changeVaultKeyOptions({
vault,
newStorageMode: KeySystemRootKeyStorageMode.Local,
})
@@ -272,7 +272,7 @@ describe('vault key management', function () {
let syncedKeys = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier)
const result = await context.vaults.changeVaultOptions({
const result = await context.vaults.changeVaultKeyOptions({
vault,
newStorageMode: KeySystemRootKeyStorageMode.Ephemeral,
})
@@ -299,7 +299,7 @@ describe('vault key management', function () {
let syncedKeys = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier)
const result = await context.vaults.changeVaultOptions({
const result = await context.vaults.changeVaultKeyOptions({
vault,
newStorageMode: KeySystemRootKeyStorageMode.Synced,
})
@@ -326,7 +326,7 @@ describe('vault key management', function () {
let syncedKeys = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier)
const result = await context.vaults.changeVaultOptions({
const result = await context.vaults.changeVaultKeyOptions({
vault,
newStorageMode: KeySystemRootKeyStorageMode.Ephemeral,
})
@@ -351,9 +351,9 @@ describe('vault key management', function () {
description: 'test vault description',
})
const result = await context.vaults.changeVaultOptions({
const result = await context.vaults.changeVaultKeyOptions({
vault,
newPasswordType: {
newPasswordOptions: {
passwordType: KeySystemPasswordType.UserInputted,
},
})
@@ -370,9 +370,9 @@ describe('vault key management', function () {
const rootKeysBeforeChange = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier)
expect(rootKeysBeforeChange.length).to.equal(1)
const result = await context.vaults.changeVaultOptions({
const result = await context.vaults.changeVaultKeyOptions({
vault,
newPasswordType: {
newPasswordOptions: {
passwordType: KeySystemPasswordType.UserInputted,
userInputtedPassword: 'test password',
},
@@ -394,9 +394,9 @@ describe('vault key management', function () {
storagePreference: KeySystemRootKeyStorageMode.Local,
})
const result = await context.vaults.changeVaultOptions({
const result = await context.vaults.changeVaultKeyOptions({
vault,
newPasswordType: {
newPasswordOptions: {
passwordType: KeySystemPasswordType.Randomized,
},
})
@@ -404,7 +404,7 @@ describe('vault key management', function () {
expect(result.isFailed()).to.be.false
const rootKeysAfterChange = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier)
expect(rootKeysAfterChange.length).to.equal(1)
expect(rootKeysAfterChange.length).to.equal(2)
const storedKey = context.keys.getRootKeyFromStorageForVault(vault.systemIdentifier)
expect(storedKey).to.be.undefined
@@ -421,9 +421,9 @@ describe('vault key management', function () {
storagePreference: KeySystemRootKeyStorageMode.Local,
})
const result = await context.vaults.changeVaultOptions({
const result = await context.vaults.changeVaultKeyOptions({
vault,
newPasswordType: {
newPasswordOptions: {
passwordType: KeySystemPasswordType.Randomized,
},
newStorageMode: KeySystemRootKeyStorageMode.Local,
@@ -431,7 +431,7 @@ describe('vault key management', function () {
expect(result.isFailed()).to.be.true
expect(result.getError()).to.equal('Vault uses randomized password and cannot change its storage preference')
expect(result.getError()).to.equal('Cannot change storage mode to non-synced for randomized vault')
})
})
})

View File

@@ -32,9 +32,7 @@ describe('vault key rotation', function () {
const callSpy = sinon.spy(context.keys, 'queueVaultItemsKeysForReencryption')
const syncSpy = context.spyOnFunctionResult(context.application.sync, 'payloadsByPreparingForServer')
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await context.vaults.rotateVaultRootKey(sharedVault)
await promise
await syncSpy
expect(callSpy.callCount).to.equal(1)
@@ -95,12 +93,10 @@ describe('vault key rotation', function () {
contactContext.lockSyncing()
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await context.vaults.rotateVaultRootKey(sharedVault)
await promise
const outboundMessages = (await context.asymmetric.getOutboundMessages()).getValue()
const expectedMessages = ['root key change', 'vault metadata change']
const expectedMessages = ['root key change']
expect(outboundMessages.length).to.equal(expectedMessages.length)
const message = outboundMessages[0]
@@ -122,9 +118,7 @@ describe('vault key rotation', function () {
contactContext.lockSyncing()
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await context.vaults.rotateVaultRootKey(sharedVault)
await promise
const rootKey = context.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)
@@ -150,9 +144,7 @@ describe('vault key rotation', function () {
const previousPrimaryItemsKey = contactContext.keys.getPrimaryKeySystemItemsKey(sharedVault.systemIdentifier)
expect(previousPrimaryItemsKey).to.not.be.undefined
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await context.vaults.rotateVaultRootKey(sharedVault)
await promise
contactContext.unlockSyncing()
await contactContext.syncAndAwaitMessageProcessing()
@@ -175,9 +167,7 @@ describe('vault key rotation', function () {
expect(originalOutboundInvites.length).to.equal(1)
const originalInviteMessage = originalOutboundInvites[0].encrypted_message
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await context.vaults.rotateVaultRootKey(sharedVault)
await promise
const updatedOutboundInvites = await context.vaultInvites.getOutboundInvites()
expect(updatedOutboundInvites.length).to.equal(1)
@@ -211,19 +201,15 @@ describe('vault key rotation', function () {
contactContext.lockSyncing()
const firstPromise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await context.vaults.rotateVaultRootKey(sharedVault)
await firstPromise
const asymmetricMessageAfterFirstChange = (await context.asymmetric.getOutboundMessages()).getValue()
const expectedMessages = ['root key change', 'vault metadata change']
const expectedMessages = ['root key change']
expect(asymmetricMessageAfterFirstChange.length).to.equal(expectedMessages.length)
const messageAfterFirstChange = asymmetricMessageAfterFirstChange[0]
const secondPromise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await context.vaults.rotateVaultRootKey(sharedVault)
await secondPromise
const asymmetricMessageAfterSecondChange = (await context.asymmetric.getOutboundMessages()).getValue()
expect(asymmetricMessageAfterSecondChange.length).to.equal(expectedMessages.length)
@@ -245,9 +231,7 @@ describe('vault key rotation', function () {
)
contactContext.lockSyncing()
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await context.vaults.rotateVaultRootKey(sharedVault)
await promise
const acceptMessage = sinon.spy(contactContext.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage')

View File

@@ -0,0 +1,79 @@
import * as Factory from '../lib/factory.js'
import * as Collaboration from '../lib/Collaboration.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('vault key sharing', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createVaultsContextWithRealCrypto()
await context.launch()
await context.register()
})
it('sharing a vault with user inputted and ephemeral password should share the key as synced for the recipient', async () => {
const privateVault = await context.vaults.createUserInputtedPasswordVault({
name: 'My Private Vault',
userInputtedPassword: 'password',
storagePreference: KeySystemRootKeyStorageMode.Ephemeral,
})
const note = await context.createSyncedNote('foo', 'bar')
await context.vaults.moveItemToVault(privateVault, note)
const sharedVault = await context.sharedVaults.convertVaultToSharedVault(privateVault)
const { thirdPartyContext, deinitThirdPartyContext } = await Collaboration.inviteNewPartyToSharedVault(
context,
sharedVault,
)
await Collaboration.acceptAllInvites(thirdPartyContext)
const rootKey = thirdPartyContext.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)
expect(rootKey).to.not.be.undefined
const contextNote = thirdPartyContext.items.findItem(note.uuid)
expect(contextNote).to.not.be.undefined
expect(contextNote.title).to.equal('foo')
expect(contextNote.text).to.equal(note.text)
await deinitThirdPartyContext()
})
it('should send key change message when vault password is changed', async () => {
const { sharedVault, thirdPartyContext, deinitThirdPartyContext } = await context.createSharedPasswordVault(
'test password',
)
const rootKeyBeforeChange = thirdPartyContext.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)
await context.vaults.changeVaultKeyOptions({
vault: sharedVault,
newPasswordOptions: {
passwordType: KeySystemPasswordType.UserInputted,
userInputtedPassword: 'new password',
},
})
await thirdPartyContext.syncAndAwaitMessageProcessing()
const rootKeyAfterChange = thirdPartyContext.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)
expect(rootKeyBeforeChange.itemsKey).to.not.equal(rootKeyAfterChange.itemsKey)
await deinitThirdPartyContext()
})
})

View File

@@ -82,13 +82,14 @@ describe('keypair change', function () {
contactContext.lockSyncing()
const previousKeyPair = context.encryption.getKeyPair()
const previousSigningKeyPair = context.encryption.getSigningKeyPair()
const previousKeyPair = context.keyPair
const previousSigningKeyPair = context.signingKeyPair
await context.changePassword('new_password')
sinon.stub(context.encryption, 'getKeyPair').returns(previousKeyPair)
sinon.stub(context.encryption, 'getSigningKeyPair').returns(previousSigningKeyPair)
sinon
.stub(context.application.dependencies.get(TYPES.GetKeyPairs), 'execute')
.returns(Result.ok({ encryption: previousKeyPair, signing: previousSigningKeyPair }))
await context.changeVaultName(sharedVault, {
name: 'New Name',

View File

@@ -28,11 +28,11 @@ describe('public key cryptography', function () {
})
it('should create keypair during registration', () => {
expect(sessions.getPublicKey()).to.not.be.undefined
expect(encryption.getKeyPair().privateKey).to.not.be.undefined
expect(context.publicKey).to.not.be.undefined
expect(context.keyPair.privateKey).to.not.be.undefined
expect(sessions.getSigningPublicKey()).to.not.be.undefined
expect(encryption.getSigningKeyPair().privateKey).to.not.be.undefined
expect(context.signingPublicKey).to.not.be.undefined
expect(context.signingKeyPair.privateKey).to.not.be.undefined
})
it('should populate keypair during sign in', async () => {
@@ -46,33 +46,33 @@ describe('public key cryptography', function () {
recreatedContext.password = password
await recreatedContext.signIn()
expect(recreatedContext.sessions.getPublicKey()).to.not.be.undefined
expect(recreatedContext.encryption.getKeyPair().privateKey).to.not.be.undefined
expect(recreatedContext.publicKey).to.not.be.undefined
expect(recreatedContext.privateKey).to.not.be.undefined
expect(recreatedContext.sessions.getSigningPublicKey()).to.not.be.undefined
expect(recreatedContext.encryption.getSigningKeyPair().privateKey).to.not.be.undefined
expect(recreatedContext.signingPublicKey).to.not.be.undefined
expect(recreatedContext.signingKeyPair.privateKey).to.not.be.undefined
await recreatedContext.deinit()
})
it('should rotate keypair during password change', async () => {
const oldPublicKey = sessions.getPublicKey()
const oldPrivateKey = encryption.getKeyPair().privateKey
const oldPublicKey = context.publicKey
const oldPrivateKey = context.privateKey
const oldSigningPublicKey = sessions.getSigningPublicKey()
const oldSigningPrivateKey = encryption.getSigningKeyPair().privateKey
const oldSigningPublicKey = context.signingPublicKey
const oldSigningPrivateKey = context.signingKeyPair.privateKey
await context.changePassword('new_password')
expect(sessions.getPublicKey()).to.not.be.undefined
expect(encryption.getKeyPair().privateKey).to.not.be.undefined
expect(sessions.getPublicKey()).to.not.equal(oldPublicKey)
expect(encryption.getKeyPair().privateKey).to.not.equal(oldPrivateKey)
expect(context.publicKey).to.not.be.undefined
expect(context.keyPair.privateKey).to.not.be.undefined
expect(context.publicKey).to.not.equal(oldPublicKey)
expect(context.keyPair.privateKey).to.not.equal(oldPrivateKey)
expect(sessions.getSigningPublicKey()).to.not.be.undefined
expect(encryption.getSigningKeyPair().privateKey).to.not.be.undefined
expect(sessions.getSigningPublicKey()).to.not.equal(oldSigningPublicKey)
expect(encryption.getSigningKeyPair().privateKey).to.not.equal(oldSigningPrivateKey)
expect(context.signingPublicKey).to.not.be.undefined
expect(context.signingKeyPair.privateKey).to.not.be.undefined
expect(context.signingPublicKey).to.not.equal(oldSigningPublicKey)
expect(context.signingKeyPair.privateKey).to.not.equal(oldSigningPrivateKey)
})
it('should allow option to enable collaboration for previously signed in accounts', async () => {