feat: add snjs package

This commit is contained in:
Karol Sójko
2022-07-06 14:04:18 +02:00
parent 321a055bae
commit 0e40469e2f
296 changed files with 46109 additions and 187 deletions

View File

@@ -0,0 +1,316 @@
import FakeWebCrypto from './fake_web_crypto.js'
import * as Applications from './Applications.js'
import * as Utils from './Utils.js'
import { createNotePayload } from './Items.js'
UuidGenerator.SetGenerator(new FakeWebCrypto().generateUUID)
const MaximumSyncOptions = {
checkIntegrity: true,
awaitAll: true,
}
export class AppContext {
constructor({ identifier, crypto, email, password, passcode } = {}) {
if (!identifier) {
identifier = `${Math.random()}`
}
if (!email) {
email = UuidGenerator.GenerateUuid()
}
if (!password) {
password = UuidGenerator.GenerateUuid()
}
if (!passcode) {
passcode = 'mypasscode'
}
this.identifier = identifier
this.crypto = crypto
this.email = email
this.password = password
this.passcode = passcode
}
enableLogging() {
const syncService = this.application.syncService
const payloadManager = this.application.payloadManager
syncService.getServiceName = () => {
return `${this.identifier}—SyncService`
}
payloadManager.getServiceName = () => {
return `${this.identifier}-PayloadManager`
}
syncService.loggingEnabled = true
payloadManager.loggingEnabled = true
}
async initialize() {
this.application = await Applications.createApplication(
this.identifier,
undefined,
undefined,
undefined,
this.crypto || new FakeWebCrypto(),
)
}
ignoreChallenges() {
this.ignoringChallenges = true
}
resumeChallenges() {
this.ignoringChallenges = false
}
disableIntegrityAutoHeal() {
this.application.syncService.emitOutOfSyncRemotePayloads = () => {
console.warn('Integrity self-healing is disabled for this test')
}
}
disableKeyRecovery() {
this.application.keyRecoveryService.beginKeyRecovery = () => {
console.warn('Key recovery is disabled for this test')
}
}
handleChallenge = (challenge) => {
if (this.ignoringChallenges) {
this.application.challengeService.cancelChallenge(challenge)
return
}
const responses = []
const accountPassword = this.passwordToUseForAccountPasswordChallenge || this.password
for (const prompt of challenge.prompts) {
if (prompt.validation === ChallengeValidation.LocalPasscode) {
responses.push(CreateChallengeValue(prompt, this.passcode))
} else if (prompt.validation === ChallengeValidation.AccountPassword) {
responses.push(CreateChallengeValue(prompt, accountPassword))
} else if (prompt.validation === ChallengeValidation.ProtectionSessionDuration) {
responses.push(CreateChallengeValue(prompt, 0))
} else if (prompt.placeholder === 'Email') {
responses.push(CreateChallengeValue(prompt, this.email))
} else if (prompt.placeholder === 'Password') {
responses.push(CreateChallengeValue(prompt, accountPassword))
} else if (challenge.heading.includes('account password')) {
responses.push(CreateChallengeValue(prompt, accountPassword))
} else {
console.log('Unhandled challenge', challenge)
throw Error(`Unhandled custom challenge in Factory.createAppContext`)
}
}
this.application.submitValuesForChallenge(challenge, responses)
}
signIn() {
const strict = false
const ephemeral = false
const mergeLocal = true
const awaitSync = true
return this.application.signIn(this.email, this.password, strict, ephemeral, mergeLocal, awaitSync)
}
register() {
return this.application.register(this.email, this.password)
}
receiveServerResponse({ retrievedItems }) {
const response = new ServerSyncResponse({
data: {
retrieved_items: retrievedItems,
},
})
return this.application.syncService.handleSuccessServerResponse({ payloadsSavedOrSaving: [] }, response)
}
resolveWhenKeyRecovered(uuid) {
return new Promise((resolve) => {
this.application.keyRecoveryService.addEventObserver((_eventName, keys) => {
if (Uuids(keys).includes(uuid)) {
resolve()
}
})
})
}
async awaitSignInEvent() {
return new Promise((resolve) => {
this.application.userService.addEventObserver((eventName) => {
if (eventName === AccountEvent.SignedInOrRegistered) {
resolve()
}
})
})
}
async restart() {
const id = this.application.identifier
await Utils.safeDeinit(this.application)
const newApplication = await Applications.createAndInitializeApplication(id)
this.application = newApplication
return newApplication
}
syncWithIntegrityCheck() {
return this.application.sync.sync({ checkIntegrity: true, awaitAll: true })
}
awaitNextSucessfulSync() {
return new Promise((resolve) => {
const removeObserver = this.application.syncService.addEventObserver((event) => {
if (event === SyncEvent.SyncCompletedWithAllItemsUploadedAndDownloaded) {
removeObserver()
resolve()
}
})
})
}
awaitNextSyncEvent(eventName) {
return new Promise((resolve) => {
const removeObserver = this.application.syncService.addEventObserver((event, data) => {
if (event === eventName) {
removeObserver()
resolve(data)
}
})
})
}
async launch({ awaitDatabaseLoad = true, receiveChallenge } = { awaitDatabaseLoad: true }) {
await this.application.prepareForLaunch({
receiveChallenge: receiveChallenge || this.handleChallenge,
})
await this.application.launch(awaitDatabaseLoad)
}
async deinit() {
await Utils.safeDeinit(this.application)
}
async sync(options) {
await this.application.sync.sync(options || { awaitAll: true })
}
async maximumSync() {
await this.sync(MaximumSyncOptions)
}
async changePassword(newPassword) {
await this.application.changePassword(this.password, newPassword)
this.password = newPassword
}
findItem(uuid) {
return this.application.items.findItem(uuid)
}
findPayload(uuid) {
return this.application.payloadManager.findPayload(uuid)
}
get itemsKeys() {
return this.application.items.getDisplayableItemsKeys()
}
disableSyncingOfItems(uuids) {
const originalImpl = this.application.items.getDirtyItems
this.application.items.getDirtyItems = function () {
const result = originalImpl.apply(this)
return result.filter((i) => !uuids.includes(i.uuid))
}
}
disableKeyRecoveryServerSignIn() {
this.application.keyRecoveryService.performServerSignIn = () => {
console.warn('application.keyRecoveryService.performServerSignIn has been stubbed with an empty implementation')
}
}
preventKeyRecoveryOfKeys(ids) {
const originalImpl = this.application.keyRecoveryService.handleUndecryptableItemsKeys
this.application.keyRecoveryService.handleUndecryptableItemsKeys = function (keys) {
const filtered = keys.filter((k) => !ids.includes(k.uuid))
originalImpl.apply(this, [filtered])
}
}
respondToAccountPasswordChallengeWith(password) {
this.passwordToUseForAccountPasswordChallenge = password
}
spyOnChangedItems(callback) {
this.application.items.addObserver(ContentType.Any, ({ changed, unerrored }) => {
callback([...changed, ...unerrored])
})
}
async createSyncedNote(title, text) {
const payload = createNotePayload(title, text)
const item = await this.application.items.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
await this.application.items.setItemDirty(item)
await this.application.syncService.sync(MaximumSyncOptions)
const note = this.application.items.findItem(payload.uuid)
return note
}
async deleteItemAndSync(item) {
await this.application.mutator.deleteItem(item)
}
async changeNoteTitle(note, title) {
return this.application.items.changeNote(note, (mutator) => {
mutator.title = title
})
}
async changeNoteTitleAndSync(note, title) {
await this.changeNoteTitle(note, title)
await this.sync()
return this.findItem(note.uuid)
}
findNoteByTitle(title) {
return this.application.items.getDisplayableNotes().find((note) => note.title === title)
}
get noteCount() {
return this.application.items.getDisplayableNotes().length
}
async createConflictedNotes(otherContext) {
const note = await this.createSyncedNote()
await otherContext.sync()
await this.changeNoteTitleAndSync(note, 'title-1')
await otherContext.changeNoteTitleAndSync(note, 'title-2')
await this.sync()
return {
original: note,
conflict: this.findNoteByTitle('title-2'),
}
}
}

View File

@@ -0,0 +1,71 @@
import WebDeviceInterface from './web_device_interface.js'
import FakeWebCrypto from './fake_web_crypto.js'
import * as Defaults from './Defaults.js'
export function createApplicationWithOptions({ identifier, environment, platform, host, crypto, device }) {
if (!device) {
device = new WebDeviceInterface()
device.environment = environment
}
return new SNApplication({
environment: environment || Environment.Web,
platform: platform || Platform.MacWeb,
deviceInterface: device,
crypto: crypto || new FakeWebCrypto(),
alertService: {
confirm: async () => true,
alert: async () => {},
blockingDialog: () => () => {},
},
identifier: identifier || `${Math.random()}`,
defaultHost: host || Defaults.getDefaultHost(),
appVersion: Defaults.getAppVersion(),
webSocketUrl: Defaults.getDefaultWebSocketUrl(),
})
}
export function createApplication(identifier, environment, platform, host, crypto) {
return createApplicationWithOptions({ identifier, environment, platform, host, crypto })
}
export function createApplicationWithFakeCrypto(identifier, environment, platform, host) {
return createApplication(identifier, environment, platform, host, new FakeWebCrypto())
}
export function createApplicationWithRealCrypto(identifier, environment, platform, host) {
return createApplication(identifier, environment, platform, host, new SNWebCrypto())
}
export async function createAppWithRandNamespace(environment, platform) {
const namespace = Math.random().toString(36).substring(2, 15)
return createApplication(namespace, environment, platform)
}
export async function createInitAppWithFakeCrypto(environment, platform) {
const namespace = Math.random().toString(36).substring(2, 15)
return createAndInitializeApplication(namespace, environment, platform, undefined, new FakeWebCrypto())
}
export async function createInitAppWithRealCrypto(environment, platform) {
const namespace = Math.random().toString(36).substring(2, 15)
return createAndInitializeApplication(namespace, environment, platform, undefined, new SNWebCrypto())
}
export async function createAndInitializeApplication(namespace, environment, platform, host, crypto) {
const application = createApplication(namespace, environment, platform, host, crypto)
await initializeApplication(application)
return application
}
export async function initializeApplication(application) {
await application.prepareForLaunch({
receiveChallenge: (challenge) => {
console.warn('Factory received potentially unhandled challenge', challenge)
if (challenge.reason !== ChallengeReason.Custom) {
throw Error("Factory application shouldn't have challenges")
}
},
})
await application.launch(true)
}

View File

@@ -0,0 +1,15 @@
export function getDefaultHost() {
return 'http://localhost:3123'
}
export function getDefaultMockedEventServiceUrl() {
return 'http://localhost:3124'
}
export function getDefaultWebSocketUrl() {
return undefined
}
export function getAppVersion() {
return '1.2.3'
}

View File

@@ -0,0 +1,72 @@
import * as Utils from './Utils.js'
const MaximumSyncOptions = {
checkIntegrity: true,
awaitAll: true,
}
export function createItemParams(contentType) {
const params = {
uuid: Utils.generateUuid(),
content_type: contentType,
content: {
title: 'hello',
text: 'world',
},
}
return params
}
export function createNoteParams({ title, text, dirty = true } = {}) {
const params = {
uuid: Utils.generateUuid(),
content_type: ContentType.Note,
dirty: dirty,
dirtyIndex: dirty ? getIncrementedDirtyIndex() : undefined,
content: FillItemContent({
title: title || 'hello',
text: text || 'world',
}),
}
return params
}
export function createTagParams({ title, dirty = true, uuid = undefined } = {}) {
const params = {
uuid: uuid || Utils.generateUuid(),
content_type: ContentType.Tag,
dirty: dirty,
dirtyIndex: dirty ? getIncrementedDirtyIndex() : undefined,
content: FillItemContent({
title: title || 'thoughts',
}),
}
return params
}
export function createRelatedNoteTagPairPayload({ noteTitle, noteText, tagTitle, dirty = true } = {}) {
const noteParams = createNoteParams({
title: noteTitle,
text: noteText,
dirty,
})
const tagParams = createTagParams({ title: tagTitle, dirty })
tagParams.content.references = [
{
uuid: noteParams.uuid,
content_type: noteParams.content_type,
},
]
noteParams.content.references = []
return [new DecryptedPayload(noteParams), new DecryptedPayload(tagParams)]
}
export async function createSyncedNoteWithTag(application) {
const payloads = createRelatedNoteTagPairPayload()
await application.itemManager.emitItemsFromPayloads(payloads)
return application.sync.sync(MaximumSyncOptions)
}
export function createNotePayload(title, text = undefined, dirty = true) {
return new DecryptedPayload(createNoteParams({ title, text, dirty }))
}

View File

@@ -0,0 +1,34 @@
import FakeWebCrypto from './fake_web_crypto.js'
export async function safeDeinit(application) {
if (application.dealloced) {
console.warn(
'Attempting to deinit already deinited application. Check the test case to find where you are double deiniting.',
)
return
}
await application.diskStorageService.awaitPersist()
/** Limit waiting to 1s */
await Promise.race([sleep(1), application.syncService?.awaitCurrentSyncs()])
await application.prepareForDeinit()
application.deinit(DeinitMode.Soft, DeinitSource.SignOut)
}
export async function sleep(seconds) {
console.warn(`Test sleeping for ${seconds}s`)
return new Promise((resolve, reject) => {
setTimeout(function () {
resolve()
}, seconds * 1000)
})
}
export function generateUuid() {
const crypto = new FakeWebCrypto()
return crypto.generateUUID()
}

View File

@@ -0,0 +1,492 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable no-undef */
import FakeWebCrypto from './fake_web_crypto.js'
import { AppContext } from './AppContext.js'
import * as Applications from './Applications.js'
import * as Defaults from './Defaults.js'
import * as Utils from './Utils.js'
import { createItemParams, createNoteParams, createTagParams } from './Items.js'
export const TenSecondTimeout = 10_000
export const TwentySecondTimeout = 20_000
export const ThirtySecondTimeout = 30_000
export const syncOptions = {
checkIntegrity: true,
awaitAll: true,
}
export async function createAndInitSimpleAppContext(
{ registerUser, environment } = {
registerUser: false,
environment: Environment.Web,
},
) {
const application = await createInitAppWithFakeCrypto(environment)
const email = UuidGenerator.GenerateUuid()
const password = UuidGenerator.GenerateUuid()
const newPassword = randomString()
if (registerUser) {
await registerUserToApplication({
application,
email,
password,
})
}
return {
application,
email,
password,
newPassword,
}
}
export async function createAppContextWithFakeCrypto(identifier, email, password) {
return createAppContext({ identifier, crypto: new FakeWebCrypto(), email, password })
}
export async function createAppContextWithRealCrypto(identifier) {
return createAppContext({ identifier, crypto: new SNWebCrypto() })
}
export async function createAppContext({ identifier, crypto, email, password } = {}) {
const context = new AppContext({ identifier, crypto, email, password })
await context.initialize()
return context
}
export function disableIntegrityAutoHeal(application) {
application.syncService.emitOutOfSyncRemotePayloads = () => {
console.warn('Integrity self-healing is disabled for this test')
}
}
export async function safeDeinit(application) {
return Utils.safeDeinit(application)
}
export function getDefaultHost() {
return Defaults.getDefaultHost()
}
export async function publishMockedEvent(eventType, eventPayload) {
await fetch(`${Defaults.getDefaultMockedEventServiceUrl()}/events`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
eventType,
eventPayload,
}),
})
}
export function createApplicationWithFakeCrypto(identifier, environment, platform, host) {
return Applications.createApplicationWithFakeCrypto(identifier, environment, platform, host)
}
export function createApplicationWithRealCrypto(identifier, environment, platform, host) {
return Applications.createApplicationWithRealCrypto(identifier, environment, platform, host)
}
export async function createAppWithRandNamespace(environment, platform) {
return Applications.createAppWithRandNamespace(environment, platform)
}
export async function createInitAppWithFakeCrypto(environment, platform) {
return Applications.createInitAppWithFakeCrypto(environment, platform)
}
export async function createInitAppWithFakeCryptoWithOptions({ environment, platform, identifier }) {
const application = Applications.createApplicationWithOptions({ identifier, environment, platform })
await Applications.initializeApplication(application)
return application
}
export async function createInitAppWithRealCrypto(environment, platform) {
return Applications.createInitAppWithRealCrypto(environment, platform)
}
export async function createAndInitializeApplication(namespace, environment, platform, host, crypto) {
return Applications.createAndInitializeApplication(namespace, environment, platform, host, crypto)
}
export async function initializeApplication(application) {
return Applications.initializeApplication(application)
}
export function registerUserToApplication({ application, email, password, ephemeral, mergeLocal = true }) {
if (!email) email = Utils.generateUuid()
if (!password) password = Utils.generateUuid()
return application.register(email, password, ephemeral, mergeLocal)
}
export async function setOldVersionPasscode({ application, passcode, version }) {
const identifier = await application.protocolService.crypto.generateUUID()
const operator = application.protocolService.operatorManager.operatorForVersion(version)
const key = await operator.createRootKey(identifier, passcode, KeyParamsOrigination.PasscodeCreate)
await application.protocolService.setNewRootKeyWrapper(key)
await application.userService.rewriteItemsKeys()
await application.syncService.sync(syncOptions)
}
/**
* Using application.register will always use latest version of protocol.
* To use older version, use this method.
*/
export async function registerOldUser({ application, email, password, version }) {
if (!email) email = Utils.generateUuid()
if (!password) password = Utils.generateUuid()
const operator = application.protocolService.operatorManager.operatorForVersion(version)
const accountKey = await operator.createRootKey(email, password, KeyParamsOrigination.Registration)
const response = await application.userApiService.register(email, accountKey.serverPassword, accountKey.keyParams)
/** Mark all existing items as dirty. */
await application.itemManager.changeItems(application.itemManager.items, (m) => {
m.dirty = true
})
await application.sessionManager.handleSuccessAuthResponse(response, accountKey)
application.notifyEvent(ApplicationEvent.SignedIn)
await application.syncService.sync({
mode: SyncMode.DownloadFirst,
...syncOptions,
})
await application.protocolService.decryptErroredPayloads()
}
export function createStorageItemPayload(contentType) {
return new DecryptedPayload(createItemParams(contentType))
}
export function createNotePayload(title, text = undefined, dirty = true) {
return new DecryptedPayload(createNoteParams({ title, text, dirty }))
}
export function createNote(title, text = undefined, dirty = true) {
return new SNNote(new DecryptedPayload(createNoteParams({ title, text, dirty })))
}
export function createStorageItemTagPayload(tagParams = {}) {
return new DecryptedPayload(createTagParams(tagParams))
}
export function itemToStoragePayload(item) {
return new DecryptedPayload(item)
}
export function createMappedNote(application, title, text, dirty = true) {
const payload = createNotePayload(title, text, dirty)
return application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
}
export async function createMappedTag(application, tagParams = {}) {
const payload = createStorageItemTagPayload(tagParams)
return application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
}
export async function createSyncedNote(application, title, text) {
const payload = createNotePayload(title, text)
const item = await application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
await application.itemManager.setItemDirty(item)
await application.syncService.sync(syncOptions)
const note = application.items.findItem(payload.uuid)
return note
}
export async function getStoragePayloadsOfType(application, type) {
const rawPayloads = await application.diskStorageService.getAllRawPayloads()
return rawPayloads
.filter((rp) => rp.content_type === type)
.map((rp) => {
return new CreatePayload(rp)
})
}
export async function createManyMappedNotes(application, count) {
const createdNotes = []
for (let i = 0; i < count; i++) {
const note = await createMappedNote(application)
await application.itemManager.setItemDirty(note)
createdNotes.push(note)
}
return createdNotes
}
export async function loginToApplication({
application,
email,
password,
ephemeral,
strict = false,
mergeLocal = true,
awaitSync = true,
}) {
return application.signIn(email, password, strict, ephemeral, mergeLocal, awaitSync)
}
export async function awaitFunctionInvokation(object, functionName) {
return new Promise((resolve) => {
const original = object[functionName]
object[functionName] = async function () {
const result = original.apply(this, arguments)
resolve(result)
return result
}
})
}
/**
* Signing out of an application deinits it.
* A new one must be created.
*/
export async function signOutApplicationAndReturnNew(application) {
const isRealCrypto = application.crypto instanceof SNWebCrypto
await application.user.signOut()
if (isRealCrypto) {
return createInitAppWithRealCrypto()
} else {
return createInitAppWithFakeCrypto()
}
}
export async function signOutAndBackIn(application, email, password) {
const isRealCrypto = application.crypto instanceof SNWebCrypto
await application.user.signOut()
const newApplication = isRealCrypto ? await createInitAppWithRealCrypto() : await createInitAppWithFakeCrypto()
await this.loginToApplication({
application: newApplication,
email,
password,
})
return newApplication
}
export async function restartApplication(application) {
const id = application.identifier
await safeDeinit(application)
const newApplication = await createAndInitializeApplication(id)
return newApplication
}
export async function storagePayloadCount(application) {
const payloads = await application.diskStorageService.getAllRawPayloads()
return payloads.length
}
/**
* The number of seconds between changes before a server creates a new revision.
* Controlled via docker/syncing-server-js.env
*/
export const ServerRevisionFrequency = 1.1
export function yesterday() {
return new Date(new Date().setDate(new Date().getDate() - 1))
}
export function dateToMicroseconds(date) {
return date.getTime() * 1_000
}
export function tomorrow() {
return new Date(new Date().setDate(new Date().getDate() + 1))
}
export async function sleep(seconds) {
return Utils.sleep(seconds)
}
export function shuffleArray(a) {
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[a[i], a[j]] = [a[j], a[i]]
}
return a
}
export function randomString(length = 10) {
let result = ''
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
const charactersLength = characters.length
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength))
}
return result
}
export function generateUuidish() {
return this.randomString(32)
}
export function randomArrayValue(array) {
return array[Math.floor(Math.random() * array.length)]
}
export async function expectThrowsAsync(method, errorMessage) {
let error = null
try {
await method()
} catch (err) {
error = err
}
const expect = chai.expect
expect(error).to.be.an('Error')
if (errorMessage) {
expect(error.message)
.to.be.a('string')
.and.satisfy((msg) => msg.startsWith(errorMessage))
}
}
export function ignoreChallenges(application) {
application.setLaunchCallback({
receiveChallenge() {
/** no-op */
},
})
}
export function handlePasswordChallenges(application, password) {
application.setLaunchCallback({
receiveChallenge: (challenge) => {
const values = challenge.prompts.map((prompt) =>
CreateChallengeValue(
prompt,
prompt.validation === ChallengeValidation.ProtectionSessionDuration
? UnprotectedAccessSecondsDuration.OneMinute
: password,
),
)
application.submitValuesForChallenge(challenge, values)
},
})
}
export async function createTags(application, hierarchy, parent = undefined, resultAccumulator = undefined) {
const result = resultAccumulator || {}
const promises = Object.entries(hierarchy).map(async ([key, value]) => {
let tag = await application.mutator.findOrCreateTag(key)
result[key] = tag
if (parent) {
await application.mutator.setTagParent(parent, tag)
}
if (value === true) {
return
}
await createTags(application, value, tag, result)
})
await Promise.all(promises)
return result
}
export function pinNote(application, note) {
return application.mutator.changeItem(note, (mutator) => {
mutator.pinned = true
})
}
export async function insertItemWithOverride(application, contentType, content, needsSync = false, errorDecrypting) {
const item = await application.itemManager.createItem(contentType, content, needsSync)
if (errorDecrypting) {
const encrypted = new EncryptedPayload({
...item.payload.ejected(),
content: '004:...',
errorDecrypting,
})
await application.itemManager.emitItemFromPayload(encrypted)
} else {
const decrypted = new DecryptedPayload({
...item.payload.ejected(),
})
await application.itemManager.emitItemFromPayload(decrypted)
}
return application.itemManager.findAnyItem(item.uuid)
}
export async function alternateUuidForItem(application, uuid) {
const item = application.itemManager.findItem(uuid)
const payload = new DecryptedPayload(item)
const results = await PayloadsByAlternatingUuid(payload, application.payloadManager.getMasterCollection())
await application.payloadManager.emitPayloads(results, PayloadEmitSource.LocalChanged)
await application.syncService.persistPayloads(results)
return application.itemManager.findItem(results[0].uuid)
}
export async function markDirtyAndSyncItem(application, itemToLookupUuidFor) {
const item = application.itemManager.findItem(itemToLookupUuidFor.uuid)
if (!item) {
throw Error('Attempting to save non-inserted item')
}
if (!item.dirty) {
await application.itemManager.changeItem(item, undefined, MutationType.NoUpdateUserTimestamps)
}
await application.sync.sync()
}
export async function changePayloadTimeStampAndSync(application, payload, timestamp, contentOverride, syncOptions) {
await changePayloadTimeStamp(application, payload, timestamp, contentOverride)
await application.sync.sync(syncOptions)
return application.itemManager.findAnyItem(payload.uuid)
}
export async function changePayloadTimeStamp(application, payload, timestamp, contentOverride) {
payload = application.payloadManager.collection.find(payload.uuid)
const changedPayload = new DecryptedPayload({
...payload,
dirty: true,
dirtyIndex: getIncrementedDirtyIndex(),
content: {
...payload.content,
...contentOverride,
},
updated_at_timestamp: timestamp,
})
await application.itemManager.emitItemFromPayload(changedPayload)
return application.itemManager.findAnyItem(payload.uuid)
}
export async function changePayloadUpdatedAt(application, payload, updatedAt) {
const latestPayload = application.payloadManager.collection.find(payload.uuid)
const changedPayload = new DecryptedPayload({
...latestPayload,
dirty: true,
dirtyIndex: getIncrementedDirtyIndex(),
updated_at: updatedAt,
})
await application.itemManager.emitItemFromPayload(changedPayload)
return application.itemManager.findAnyItem(payload.uuid)
}
export async function changePayloadTimeStampDeleteAndSync(application, payload, timestamp, syncOptions) {
payload = application.payloadManager.collection.find(payload.uuid)
const changedPayload = new DeletedPayload({
...payload,
content: undefined,
dirty: true,
dirtyIndex: getIncrementedDirtyIndex(),
deleted: true,
updated_at_timestamp: timestamp,
})
await application.itemManager.emitItemFromPayload(changedPayload)
await application.sync.sync(syncOptions)
}

View File

@@ -0,0 +1,138 @@
export default class FakeWebCrypto {
constructor() {}
deinit() {}
initialize() {
return
}
randomString(len) {
const charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
let randomString = ''
for (let i = 0; i < len; i++) {
const randomPoz = Math.floor(Math.random() * charSet.length)
randomString += charSet.substring(randomPoz, randomPoz + 1)
}
return randomString
}
generateUUIDSync = () => {
const globalScope = getGlobalScope()
const crypto = globalScope.crypto || globalScope.msCrypto
if (crypto) {
const buf = new Uint32Array(4)
crypto.getRandomValues(buf)
let idx = -1
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
idx++
const r = (buf[idx >> 3] >> ((idx % 8) * 4)) & 15
const v = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
} else {
let d = new Date().getTime()
if (globalScope.performance && typeof globalScope.performance.now === 'function') {
d += performance.now() // use high-precision timer if available
}
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = (d + Math.random() * 16) % 16 | 0
d = Math.floor(d / 16)
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)
})
return uuid
}
}
generateUUID = () => {
return this.generateUUIDSync()
}
timingSafeEqual(a, b) {
return a === b
}
base64Encode(text) {
return btoa(text)
}
base64URLEncode(text) {
return this.base64Encode(text).replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, '')
}
base64Decode(base64String) {
return atob(base64String)
}
async pbkdf2(password, salt, iterations, length) {
return btoa(password + salt + iterations)
}
generateRandomKey(bits) {
const length = bits / 8
return this.randomString(length)
}
async aes256CbcEncrypt(plaintext, iv, key) {
const data = {
plaintext,
iv,
key,
}
return btoa(JSON.stringify(data))
}
async aes256CbcDecrypt(ciphertext, iv, key) {
const data = JSON.parse(atob(ciphertext))
if (data.key !== key || data.iv !== iv) {
return undefined
}
return data.plaintext
}
async hmac256(message, key) {
return btoa(message + key)
}
async sha256(text) {
return new SNWebCrypto().sha256(text)
}
async hmac1(message, key) {
return btoa(message + key)
}
async unsafeSha1(text) {
return btoa(text)
}
argon2(password, salt, iterations, bytes, length) {
return btoa(password)
}
xchacha20Encrypt(plaintext, nonce, key, assocData) {
const data = {
plaintext,
nonce,
key,
assocData,
}
return btoa(JSON.stringify(data))
}
xchacha20Decrypt(ciphertext, nonce, key, assocData) {
const data = JSON.parse(atob(ciphertext))
if (data.key !== key || data.nonce !== nonce || data.assocData !== assocData) {
return undefined
}
return data.plaintext
}
generateOtpSecret() {
return 'WQVV2GFBRQWU3UQZWQFZC37PSNRXKTA6'
}
totpToken(secret, timestamp, tokenLength, step) {
return '123456'
}
}

View File

@@ -0,0 +1,157 @@
/* eslint-disable no-undef */
const KEYCHAIN_STORAGE_KEY = 'keychain'
export default class WebDeviceInterface {
environment = Environment.Web
async getRawStorageValue(key) {
return localStorage.getItem(key)
}
async getJsonParsedRawStorageValue(key) {
const value = await this.getRawStorageValue(key)
if (isNullOrUndefined(value)) {
return undefined
}
try {
return JSON.parse(value)
} catch (e) {
return value
}
}
async getAllRawStorageKeyValues() {
const results = []
for (const key of Object.keys(localStorage)) {
results.push({
key: key,
value: localStorage[key],
})
}
return results
}
async setRawStorageValue(key, value) {
localStorage.setItem(key, value)
}
async removeRawStorageValue(key) {
localStorage.removeItem(key)
}
async removeAllRawStorageValues() {
localStorage.clear()
}
async openDatabase(_identifier) {
return {}
}
_getDatabaseKeyPrefix(identifier) {
if (identifier) {
return `${identifier}-item-`
} else {
return 'item-'
}
}
_keyForPayloadId(id, identifier) {
return `${this._getDatabaseKeyPrefix(identifier)}${id}`
}
async getAllRawDatabasePayloads(identifier) {
const models = []
for (const key in localStorage) {
if (key.startsWith(this._getDatabaseKeyPrefix(identifier))) {
models.push(JSON.parse(localStorage[key]))
}
}
return models
}
async saveRawDatabasePayload(payload, identifier) {
localStorage.setItem(this._keyForPayloadId(payload.uuid, identifier), JSON.stringify(payload))
}
async saveRawDatabasePayloads(payloads, identifier) {
for (const payload of payloads) {
await this.saveRawDatabasePayload(payload, identifier)
}
}
async removeRawDatabasePayloadWithId(id, identifier) {
localStorage.removeItem(this._keyForPayloadId(id, identifier))
}
async removeAllRawDatabasePayloads(identifier) {
for (const key in localStorage) {
if (key.startsWith(this._getDatabaseKeyPrefix(identifier))) {
delete localStorage[key]
}
}
}
/** @keychain */
async getNamespacedKeychainValue(identifier) {
const keychain = await this.getRawKeychainValue(identifier)
if (!keychain) {
return
}
return keychain[identifier]
}
async setNamespacedKeychainValue(value, identifier) {
let keychain = await this.getRawKeychainValue()
if (!keychain) {
keychain = {}
}
localStorage.setItem(
KEYCHAIN_STORAGE_KEY,
JSON.stringify({
...keychain,
[identifier]: value,
}),
)
}
async clearNamespacedKeychainValue(identifier) {
const keychain = await this.getRawKeychainValue()
if (!keychain) {
return
}
delete keychain[identifier]
localStorage.setItem(KEYCHAIN_STORAGE_KEY, JSON.stringify(keychain))
}
/** Allows unit tests to set legacy keychain structure as it was <= 003 */
// eslint-disable-next-line camelcase
async setLegacyRawKeychainValue(value) {
localStorage.setItem(KEYCHAIN_STORAGE_KEY, JSON.stringify(value))
}
async getRawKeychainValue() {
const keychain = localStorage.getItem(KEYCHAIN_STORAGE_KEY)
return JSON.parse(keychain)
}
async clearRawKeychainValue() {
localStorage.removeItem(KEYCHAIN_STORAGE_KEY)
}
performSoftReset() {
}
performHardReset() {
}
isDeviceDestroyed() {
return false
}
deinit() {
}
}