feat: add @standardnotes/encryption package (#1199)
* feat: add @standardnotes/encryption package * fix: mobile dependency on encryption package * fix: order of build & lint in pr workflows * fix: web dependency on encryption package * fix: remove encryption package composite configuration * fix: import order
This commit is contained in:
46
packages/encryption/src/Domain/Keys/ItemsKey/ItemsKey.ts
Normal file
46
packages/encryption/src/Domain/Keys/ItemsKey/ItemsKey.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
ConflictStrategy,
|
||||
ItemsKeyContent,
|
||||
DecryptedItem,
|
||||
DecryptedPayloadInterface,
|
||||
DecryptedItemInterface,
|
||||
HistoryEntryInterface,
|
||||
ItemsKeyInterface,
|
||||
RootKeyInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { ContentType, ProtocolVersion } from '@standardnotes/common'
|
||||
|
||||
export function isItemsKey(x: ItemsKeyInterface | RootKeyInterface): x is ItemsKeyInterface {
|
||||
return x.content_type === ContentType.ItemsKey
|
||||
}
|
||||
|
||||
/**
|
||||
* A key used to encrypt other items. Items keys are synced and persisted.
|
||||
*/
|
||||
export class SNItemsKey extends DecryptedItem<ItemsKeyContent> implements ItemsKeyInterface {
|
||||
keyVersion: ProtocolVersion
|
||||
isDefault: boolean | undefined
|
||||
itemsKey: string
|
||||
|
||||
constructor(payload: DecryptedPayloadInterface<ItemsKeyContent>) {
|
||||
super(payload)
|
||||
this.keyVersion = payload.content.version
|
||||
this.isDefault = payload.content.isDefault
|
||||
this.itemsKey = this.payload.content.itemsKey
|
||||
}
|
||||
|
||||
/** Do not duplicate items keys. Always keep original */
|
||||
override strategyWhenConflictingWithItem(
|
||||
_item: DecryptedItemInterface,
|
||||
_previousRevision?: HistoryEntryInterface,
|
||||
): ConflictStrategy {
|
||||
return ConflictStrategy.KeepBase
|
||||
}
|
||||
|
||||
get dataAuthenticationKey(): string | undefined {
|
||||
if (this.keyVersion === ProtocolVersion.V004) {
|
||||
throw 'Attempting to access legacy data authentication key.'
|
||||
}
|
||||
return this.payload.content.dataAuthenticationKey
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DecryptedItemMutator, ItemsKeyMutatorInterface, ItemsKeyContent } from '@standardnotes/models'
|
||||
|
||||
export class ItemsKeyMutator extends DecryptedItemMutator<ItemsKeyContent> implements ItemsKeyMutatorInterface {
|
||||
set isDefault(isDefault: boolean) {
|
||||
this.mutableContent.isDefault = isDefault
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { RegisterItemClass, DecryptedItemMutator, ItemsKeyContent } from '@standardnotes/models'
|
||||
import { SNItemsKey } from './ItemsKey'
|
||||
import { ItemsKeyMutator } from './ItemsKeyMutator'
|
||||
|
||||
RegisterItemClass(ContentType.ItemsKey, SNItemsKey, ItemsKeyMutator as unknown as DecryptedItemMutator<ItemsKeyContent>)
|
||||
3
packages/encryption/src/Domain/Keys/ItemsKey/index.ts
Normal file
3
packages/encryption/src/Domain/Keys/ItemsKey/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './ItemsKey'
|
||||
export * from './ItemsKeyMutator'
|
||||
export * from './Registration'
|
||||
51
packages/encryption/src/Domain/Keys/RootKey/Functions.ts
Normal file
51
packages/encryption/src/Domain/Keys/RootKey/Functions.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { SNRootKey } from './RootKey'
|
||||
import {
|
||||
DecryptedPayload,
|
||||
FillItemContentSpecialized,
|
||||
PayloadTimestampDefaults,
|
||||
RootKeyContent,
|
||||
RootKeyContentSpecialized,
|
||||
} from '@standardnotes/models'
|
||||
import { UuidGenerator } from '@standardnotes/utils'
|
||||
import { ContentType, ProtocolVersion } from '@standardnotes/common'
|
||||
|
||||
export function CreateNewRootKey(content: RootKeyContentSpecialized): SNRootKey {
|
||||
const uuid = UuidGenerator.GenerateUuid()
|
||||
|
||||
const payload = new DecryptedPayload<RootKeyContent>({
|
||||
uuid: uuid,
|
||||
content_type: ContentType.RootKey,
|
||||
content: FillRootKeyContent(content),
|
||||
...PayloadTimestampDefaults(),
|
||||
})
|
||||
|
||||
return new SNRootKey(payload)
|
||||
}
|
||||
|
||||
export function FillRootKeyContent(content: Partial<RootKeyContentSpecialized>): RootKeyContent {
|
||||
if (!content.version) {
|
||||
if (content.dataAuthenticationKey) {
|
||||
/**
|
||||
* If there's no version stored, it must be either 001 or 002.
|
||||
* If there's a dataAuthenticationKey, it has to be 002. Otherwise it's 001.
|
||||
*/
|
||||
content.version = ProtocolVersion.V002
|
||||
} else {
|
||||
content.version = ProtocolVersion.V001
|
||||
}
|
||||
}
|
||||
|
||||
return FillItemContentSpecialized(content)
|
||||
}
|
||||
|
||||
export function ContentTypeUsesRootKeyEncryption(contentType: ContentType): boolean {
|
||||
return (
|
||||
contentType === ContentType.RootKey ||
|
||||
contentType === ContentType.ItemsKey ||
|
||||
contentType === ContentType.EncryptedStorage
|
||||
)
|
||||
}
|
||||
|
||||
export function ItemContentTypeUsesRootKeyEncryption(contentType: ContentType): boolean {
|
||||
return contentType === ContentType.ItemsKey
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { KeyParamsResponse } from '@standardnotes/responses'
|
||||
import {
|
||||
KeyParamsContent001,
|
||||
KeyParamsContent002,
|
||||
KeyParamsContent003,
|
||||
KeyParamsContent004,
|
||||
AnyKeyParamsContent,
|
||||
} from '@standardnotes/common'
|
||||
import { SNRootKeyParams } from './RootKeyParams'
|
||||
import { ProtocolVersionForKeyParams } from './ProtocolVersionForKeyParams'
|
||||
|
||||
/**
|
||||
* 001, 002:
|
||||
* - Nonce is not uploaded to server, instead used to compute salt locally and send to server
|
||||
* - Salt is returned from server
|
||||
* - Cost/iteration count is returned from the server
|
||||
* - Account identifier is returned as 'email'
|
||||
* 003, 004:
|
||||
* - Salt is computed locally via the seed (pw_nonce) returned from the server
|
||||
* - Cost/iteration count is determined locally by the protocol version
|
||||
* - Account identifier is returned as 'identifier'
|
||||
*/
|
||||
|
||||
export type AllKeyParamsContents = KeyParamsContent001 & KeyParamsContent002 & KeyParamsContent003 & KeyParamsContent004
|
||||
|
||||
export function Create001KeyParams(keyParams: KeyParamsContent001) {
|
||||
return CreateAnyKeyParams(keyParams)
|
||||
}
|
||||
|
||||
export function Create002KeyParams(keyParams: KeyParamsContent002) {
|
||||
return CreateAnyKeyParams(keyParams)
|
||||
}
|
||||
|
||||
export function Create003KeyParams(keyParams: KeyParamsContent003) {
|
||||
return CreateAnyKeyParams(keyParams)
|
||||
}
|
||||
|
||||
export function Create004KeyParams(keyParams: KeyParamsContent004) {
|
||||
return CreateAnyKeyParams(keyParams)
|
||||
}
|
||||
|
||||
export function CreateAnyKeyParams(keyParams: AnyKeyParamsContent) {
|
||||
if ('content' in keyParams) {
|
||||
throw Error('Raw key params shouldnt have content; perhaps you passed in a SNRootKeyParams object.')
|
||||
}
|
||||
return new SNRootKeyParams(keyParams)
|
||||
}
|
||||
|
||||
export function KeyParamsFromApiResponse(response: KeyParamsResponse, identifier?: string) {
|
||||
const rawKeyParams: AnyKeyParamsContent = {
|
||||
identifier: identifier || response.data.identifier!,
|
||||
pw_cost: response.data.pw_cost!,
|
||||
pw_nonce: response.data.pw_nonce!,
|
||||
pw_salt: response.data.pw_salt!,
|
||||
version: ProtocolVersionForKeyParams(response.data),
|
||||
origination: response.data.origination,
|
||||
created: response.data.created,
|
||||
}
|
||||
return CreateAnyKeyParams(rawKeyParams)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { V001Algorithm, V002Algorithm } from '../../Algorithm'
|
||||
import { KeyParamsData } from '@standardnotes/responses'
|
||||
import { AnyKeyParamsContent, ProtocolVersion } from '@standardnotes/common'
|
||||
|
||||
export function ProtocolVersionForKeyParams(response: KeyParamsData | AnyKeyParamsContent): ProtocolVersion {
|
||||
if (response.version) {
|
||||
return response.version
|
||||
}
|
||||
/**
|
||||
* 001 and 002 key params (as stored locally) may not report a version number.
|
||||
* In some cases it may be impossible to differentiate between 001 and 002 params,
|
||||
* but there are a few rules we can use to find a best fit.
|
||||
*/
|
||||
/**
|
||||
* First try to determine by cost. If the cost appears in V002 costs but not V001 costs,
|
||||
* we know it's 002.
|
||||
*/
|
||||
const cost = response.pw_cost!
|
||||
const appearsInV001 = V001Algorithm.PbkdfCostsUsed.includes(cost)
|
||||
const appearsInV002 = V002Algorithm.PbkdfCostsUsed.includes(cost)
|
||||
|
||||
if (appearsInV001 && !appearsInV002) {
|
||||
return ProtocolVersion.V001
|
||||
} else if (appearsInV002 && !appearsInV001) {
|
||||
return ProtocolVersion.V002
|
||||
} else if (appearsInV002 && appearsInV001) {
|
||||
/**
|
||||
* If the cost appears in both versions, we can be certain it's 002 if it's missing
|
||||
* the pw_nonce property. (However late versions of 002 also used a pw_nonce, so its
|
||||
* presence doesn't automatically indicate 001.)
|
||||
*/
|
||||
if (!response.pw_nonce) {
|
||||
return ProtocolVersion.V002
|
||||
} else {
|
||||
/**
|
||||
* We're now at the point that the cost has appeared in both versions, and a pw_nonce
|
||||
* is present. We'll have to go with what is more statistically likely.
|
||||
*/
|
||||
if (V002Algorithm.ImprobablePbkdfCostsUsed.includes(cost)) {
|
||||
return ProtocolVersion.V001
|
||||
} else {
|
||||
return ProtocolVersion.V002
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/** Doesn't appear in either V001 or V002; unlikely possibility. */
|
||||
return ProtocolVersion.V002
|
||||
}
|
||||
}
|
||||
95
packages/encryption/src/Domain/Keys/RootKey/RootKey.ts
Normal file
95
packages/encryption/src/Domain/Keys/RootKey/RootKey.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { SNRootKeyParams } from './RootKeyParams'
|
||||
import {
|
||||
RootKeyInterface,
|
||||
RootKeyContent,
|
||||
DecryptedItem,
|
||||
DecryptedPayloadInterface,
|
||||
RootKeyContentInStorage,
|
||||
NamespacedRootKeyInKeychain,
|
||||
} from '@standardnotes/models'
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
import { timingSafeEqual } from '@standardnotes/sncrypto-common'
|
||||
|
||||
/**
|
||||
* A root key is a local only construct that houses the key used for the encryption
|
||||
* and decryption of items keys. A root key extends SNItem for local convenience, but is
|
||||
* not part of the syncing or storage ecosystem—root keys are managed independently.
|
||||
*/
|
||||
export class SNRootKey extends DecryptedItem<RootKeyContent> implements RootKeyInterface {
|
||||
public readonly keyParams: SNRootKeyParams
|
||||
|
||||
constructor(payload: DecryptedPayloadInterface<RootKeyContent>) {
|
||||
super(payload)
|
||||
|
||||
this.keyParams = new SNRootKeyParams(payload.content.keyParams)
|
||||
}
|
||||
|
||||
public get keyVersion(): ProtocolVersion {
|
||||
return this.content.version
|
||||
}
|
||||
|
||||
/**
|
||||
* When the root key is used to encrypt items, we use the masterKey directly.
|
||||
*/
|
||||
public get itemsKey(): string {
|
||||
return this.masterKey
|
||||
}
|
||||
|
||||
public get masterKey(): string {
|
||||
return this.content.masterKey
|
||||
}
|
||||
|
||||
/**
|
||||
* serverPassword is not persisted as part of keychainValue, so if loaded from disk,
|
||||
* this value may be undefined.
|
||||
*/
|
||||
public get serverPassword(): string | undefined {
|
||||
return this.content.serverPassword
|
||||
}
|
||||
|
||||
/** 003 and below only. */
|
||||
public get dataAuthenticationKey(): string | undefined {
|
||||
return this.content.dataAuthenticationKey
|
||||
}
|
||||
|
||||
public compare(otherKey: SNRootKey): boolean {
|
||||
if (this.keyVersion !== otherKey.keyVersion) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.serverPassword && otherKey.serverPassword) {
|
||||
return (
|
||||
timingSafeEqual(this.masterKey, otherKey.masterKey) &&
|
||||
timingSafeEqual(this.serverPassword, otherKey.serverPassword)
|
||||
)
|
||||
} else {
|
||||
return timingSafeEqual(this.masterKey, otherKey.masterKey)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Object suitable for persist in storage when wrapped
|
||||
*/
|
||||
public persistableValueWhenWrapping(): RootKeyContentInStorage {
|
||||
return {
|
||||
...this.getKeychainValue(),
|
||||
keyParams: this.keyParams.getPortableValue(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Object that is suitable for persisting in a keychain
|
||||
*/
|
||||
public getKeychainValue(): NamespacedRootKeyInKeychain {
|
||||
const values: NamespacedRootKeyInKeychain = {
|
||||
version: this.keyVersion,
|
||||
masterKey: this.masterKey,
|
||||
}
|
||||
|
||||
if (this.dataAuthenticationKey) {
|
||||
values.dataAuthenticationKey = this.dataAuthenticationKey
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
}
|
||||
82
packages/encryption/src/Domain/Keys/RootKey/RootKeyParams.ts
Normal file
82
packages/encryption/src/Domain/Keys/RootKey/RootKeyParams.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
KeyParamsContent001,
|
||||
KeyParamsContent002,
|
||||
KeyParamsContent003,
|
||||
KeyParamsContent004,
|
||||
AnyKeyParamsContent,
|
||||
ProtocolVersion,
|
||||
KeyParamsOrigination,
|
||||
} from '@standardnotes/common'
|
||||
import { RootKeyParamsInterface } from '@standardnotes/models'
|
||||
import { pickByCopy } from '@standardnotes/utils'
|
||||
import { ProtocolVersionForKeyParams } from './ProtocolVersionForKeyParams'
|
||||
import { ValidKeyParamsKeys } from './ValidKeyParamsKeys'
|
||||
|
||||
export class SNRootKeyParams implements RootKeyParamsInterface {
|
||||
public readonly content: AnyKeyParamsContent
|
||||
|
||||
constructor(content: AnyKeyParamsContent) {
|
||||
this.content = {
|
||||
...content,
|
||||
origination: content.origination || KeyParamsOrigination.Registration,
|
||||
version: content.version || ProtocolVersionForKeyParams(content),
|
||||
}
|
||||
}
|
||||
|
||||
get isKeyParamsObject(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
get identifier(): string {
|
||||
return this.content004.identifier || this.content002.email
|
||||
}
|
||||
|
||||
get version(): ProtocolVersion {
|
||||
return this.content.version
|
||||
}
|
||||
|
||||
get origination(): KeyParamsOrigination | undefined {
|
||||
return this.content.origination
|
||||
}
|
||||
|
||||
get content001(): KeyParamsContent001 {
|
||||
return this.content as KeyParamsContent001
|
||||
}
|
||||
|
||||
get content002(): KeyParamsContent002 {
|
||||
return this.content as KeyParamsContent002
|
||||
}
|
||||
|
||||
get content003(): KeyParamsContent003 {
|
||||
return this.content as KeyParamsContent003
|
||||
}
|
||||
|
||||
get content004(): KeyParamsContent004 {
|
||||
return this.content as KeyParamsContent004
|
||||
}
|
||||
|
||||
get createdDate(): Date | undefined {
|
||||
if (!this.content004.created) {
|
||||
return undefined
|
||||
}
|
||||
return new Date(Number(this.content004.created))
|
||||
}
|
||||
|
||||
compare(other: SNRootKeyParams): boolean {
|
||||
if (this.version !== other.version) {
|
||||
return false
|
||||
}
|
||||
|
||||
if ([ProtocolVersion.V004, ProtocolVersion.V003].includes(this.version)) {
|
||||
return this.identifier === other.identifier && this.content004.pw_nonce === other.content003.pw_nonce
|
||||
} else if ([ProtocolVersion.V002, ProtocolVersion.V001].includes(this.version)) {
|
||||
return this.identifier === other.identifier && this.content002.pw_salt === other.content001.pw_salt
|
||||
} else {
|
||||
throw Error('Unhandled version in KeyParams.compare')
|
||||
}
|
||||
}
|
||||
|
||||
getPortableValue(): AnyKeyParamsContent {
|
||||
return pickByCopy(this.content, ValidKeyParamsKeys as (keyof AnyKeyParamsContent)[])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { AllKeyParamsContents } from './KeyParamsFunctions'
|
||||
|
||||
export const ValidKeyParamsKeys: (keyof AllKeyParamsContents)[] = [
|
||||
'identifier',
|
||||
'pw_cost',
|
||||
'pw_nonce',
|
||||
'pw_salt',
|
||||
'version',
|
||||
'origination',
|
||||
'created',
|
||||
]
|
||||
95
packages/encryption/src/Domain/Keys/Utils/DecryptItemsKey.ts
Normal file
95
packages/encryption/src/Domain/Keys/Utils/DecryptItemsKey.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
DecryptedPayloadInterface,
|
||||
EncryptedPayloadInterface,
|
||||
isDecryptedPayload,
|
||||
ItemsKeyContent,
|
||||
RootKeyInterface,
|
||||
} from '@standardnotes/models'
|
||||
import {
|
||||
ChallengePrompt,
|
||||
ChallengeReason,
|
||||
ChallengeServiceInterface,
|
||||
ChallengeValidation,
|
||||
} from '@standardnotes/services'
|
||||
import { EncryptionProvider } from '../../Service/Encryption/EncryptionProvider'
|
||||
import { SNRootKeyParams } from '../RootKey/RootKeyParams'
|
||||
import { KeyRecoveryStrings } from './KeyRecoveryStrings'
|
||||
|
||||
export async function DecryptItemsKeyWithUserFallback(
|
||||
itemsKey: EncryptedPayloadInterface,
|
||||
encryptor: EncryptionProvider,
|
||||
challengor: ChallengeServiceInterface,
|
||||
): Promise<DecryptedPayloadInterface<ItemsKeyContent> | 'failed' | 'aborted'> {
|
||||
const decryptionResult = await encryptor.decryptSplitSingle<ItemsKeyContent>({
|
||||
usesRootKeyWithKeyLookup: {
|
||||
items: [itemsKey],
|
||||
},
|
||||
})
|
||||
|
||||
if (isDecryptedPayload(decryptionResult)) {
|
||||
return decryptionResult
|
||||
}
|
||||
|
||||
const secondDecryptionResult = await DecryptItemsKeyByPromptingUser(itemsKey, encryptor, challengor)
|
||||
|
||||
if (secondDecryptionResult === 'aborted' || secondDecryptionResult === 'failed') {
|
||||
return secondDecryptionResult
|
||||
}
|
||||
|
||||
return secondDecryptionResult.decryptedKey
|
||||
}
|
||||
|
||||
export async function DecryptItemsKeyByPromptingUser(
|
||||
itemsKey: EncryptedPayloadInterface,
|
||||
encryptor: EncryptionProvider,
|
||||
challengor: ChallengeServiceInterface,
|
||||
keyParams?: SNRootKeyParams,
|
||||
): Promise<
|
||||
| {
|
||||
decryptedKey: DecryptedPayloadInterface<ItemsKeyContent>
|
||||
rootKey: RootKeyInterface
|
||||
}
|
||||
| 'failed'
|
||||
| 'aborted'
|
||||
> {
|
||||
if (!keyParams) {
|
||||
keyParams = encryptor.getKeyEmbeddedKeyParams(itemsKey)
|
||||
}
|
||||
|
||||
if (!keyParams) {
|
||||
return 'failed'
|
||||
}
|
||||
|
||||
const challenge = challengor.createChallenge(
|
||||
[new ChallengePrompt(ChallengeValidation.None, undefined, undefined, true)],
|
||||
ChallengeReason.Custom,
|
||||
true,
|
||||
KeyRecoveryStrings.KeyRecoveryLoginFlowPrompt(keyParams),
|
||||
KeyRecoveryStrings.KeyRecoveryPasswordRequired,
|
||||
)
|
||||
|
||||
const response = await challengor.promptForChallengeResponse(challenge)
|
||||
|
||||
if (!response) {
|
||||
return 'aborted'
|
||||
}
|
||||
|
||||
const password = response.values[0].value as string
|
||||
|
||||
const rootKey = await encryptor.computeRootKey(password, keyParams)
|
||||
|
||||
const secondDecryptionResult = await encryptor.decryptSplitSingle<ItemsKeyContent>({
|
||||
usesRootKey: {
|
||||
items: [itemsKey],
|
||||
key: rootKey,
|
||||
},
|
||||
})
|
||||
|
||||
challengor.completeChallenge(challenge)
|
||||
|
||||
if (isDecryptedPayload(secondDecryptionResult)) {
|
||||
return { decryptedKey: secondDecryptionResult, rootKey }
|
||||
}
|
||||
|
||||
return 'failed'
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { KeyParamsOrigination } from '@standardnotes/common'
|
||||
import { SNRootKeyParams } from '../RootKey/RootKeyParams'
|
||||
|
||||
export const KeyRecoveryStrings = {
|
||||
KeyRecoveryLoginFlowPrompt: (keyParams: SNRootKeyParams) => {
|
||||
const dateString = keyParams.createdDate?.toLocaleString()
|
||||
switch (keyParams.origination) {
|
||||
case KeyParamsOrigination.EmailChange:
|
||||
return `Enter your account password as it was when you changed your email on ${dateString}.`
|
||||
case KeyParamsOrigination.PasswordChange:
|
||||
return `Enter your account password after it was changed on ${dateString}.`
|
||||
case KeyParamsOrigination.Registration:
|
||||
return `Enter your account password as it was when you registered ${dateString}.`
|
||||
case KeyParamsOrigination.ProtocolUpgrade:
|
||||
return `Enter your account password as it was when you upgraded your encryption version on ${dateString}.`
|
||||
case KeyParamsOrigination.PasscodeChange:
|
||||
return `Enter your application passcode after it was changed on ${dateString}.`
|
||||
case KeyParamsOrigination.PasscodeCreate:
|
||||
return `Enter your application passcode as it was when you created it on ${dateString}.`
|
||||
default:
|
||||
throw Error('Unhandled KeyParamsOrigination case for KeyRecoveryLoginFlowPrompt')
|
||||
}
|
||||
},
|
||||
KeyRecoveryLoginFlowReason: 'Your account password is required to revalidate your session.',
|
||||
KeyRecoveryLoginFlowInvalidPassword: 'Incorrect credentials entered. Please try again.',
|
||||
KeyRecoveryRootKeyReplaced: 'Your credentials have successfully been updated.',
|
||||
KeyRecoveryPasscodeRequiredTitle: 'Passcode Required',
|
||||
KeyRecoveryPasscodeRequiredText: 'You must enter your passcode in order to save your new credentials.',
|
||||
KeyRecoveryPasswordRequired: 'Your account password is required to recover an encryption key.',
|
||||
KeyRecoveryKeyRecovered: 'Your key has successfully been recovered.',
|
||||
KeyRecoveryUnableToRecover: 'Unable to recover your key with the attempted password. Please try again.',
|
||||
}
|
||||
Reference in New Issue
Block a user