Compare commits
93 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf6b4f5423 | ||
|
|
ca46765760 | ||
|
|
4de3287f55 | ||
|
|
cdc27e061b | ||
|
|
78b3d239eb | ||
|
|
005d3b0eca | ||
|
|
23ba69c6a6 | ||
|
|
d644ee97e1 | ||
|
|
0afd0d63b3 | ||
|
|
f04d9b94d9 | ||
|
|
8cd4ec30af | ||
|
|
4215810cf8 | ||
|
|
1f0c2d2aa6 | ||
|
|
dc7782fb0c | ||
|
|
c7ba0c5f33 | ||
|
|
5d372607a4 | ||
|
|
b203b373b9 | ||
|
|
a4e12a1e0c | ||
|
|
233858ad09 | ||
|
|
863d602a70 | ||
|
|
f97bf015f1 | ||
|
|
9d73a39b9d | ||
|
|
3455e7f7c3 | ||
|
|
e9861e5311 | ||
|
|
f902c8ca4c | ||
|
|
4180621bd9 | ||
|
|
ab2eb77722 | ||
|
|
09e6511d64 | ||
|
|
5c4ec23b1a | ||
|
|
c7a78a70c9 | ||
|
|
a4df578665 | ||
|
|
4cfbe91ad4 | ||
|
|
ae6e910e84 | ||
|
|
533e0c413a | ||
|
|
4ec71f6151 | ||
|
|
adc157fe56 | ||
|
|
3b8219020a | ||
|
|
9b759a02a5 | ||
|
|
bed3bd9612 | ||
|
|
27c8111e63 | ||
|
|
4b66689e07 | ||
|
|
f22b0cde8d | ||
|
|
1705229ad9 | ||
|
|
baa941c9dd | ||
|
|
55e9a7049e | ||
|
|
6b4b69f0bb | ||
|
|
572417d3aa | ||
|
|
90193eaed9 | ||
|
|
d80f783013 | ||
|
|
16c36a088f | ||
|
|
c0444680c6 | ||
|
|
03b1accfda | ||
|
|
8e379a8e43 | ||
|
|
e05bd71fe1 | ||
|
|
5cc4d8cfb3 | ||
|
|
c359775ebc | ||
|
|
80148f95af | ||
|
|
16bb23d977 | ||
|
|
0e59f80999 | ||
|
|
a13d2a065e | ||
|
|
993bf81e43 | ||
|
|
71f3ce4120 | ||
|
|
694662d851 | ||
|
|
492748f854 | ||
|
|
c6811ffadc | ||
|
|
d432f08dc6 | ||
|
|
2930f22208 | ||
|
|
b52de5d9e6 | ||
|
|
fd6b030dff | ||
|
|
ae07e435ae | ||
|
|
abab8df412 | ||
|
|
c0cc5f29e1 | ||
|
|
cd817f6df9 | ||
|
|
2d56c656cc | ||
|
|
b33ec5e347 | ||
|
|
3bdfebfcea | ||
|
|
02d2db92c2 | ||
|
|
3db1c55ce3 | ||
|
|
8ed5bd84d2 | ||
|
|
7855e3925a | ||
|
|
f5125b7981 | ||
|
|
5b11569ea9 | ||
|
|
c2dc0e6318 | ||
|
|
4ab4b51b56 | ||
|
|
25c84d5b56 | ||
|
|
8f7cf104ed | ||
|
|
dfc21f60a8 | ||
|
|
ed3067d9d2 | ||
|
|
459e501700 | ||
|
|
67c5452dee | ||
|
|
b847880d41 | ||
|
|
257e4e1486 | ||
|
|
f8f13f9c8c |
@@ -95,6 +95,12 @@
|
||||
"title": "La Poste",
|
||||
"slug": "laposte"
|
||||
},
|
||||
{
|
||||
"title": "Mastodon",
|
||||
"altNames": ["mstdn", "fediscience", "mathstodon", "fosstodon"],
|
||||
"slug": "mastodon",
|
||||
"hex": "6364FF"
|
||||
},
|
||||
{
|
||||
"title": "Microsoft"
|
||||
},
|
||||
@@ -127,6 +133,10 @@
|
||||
"title": "Peerberry",
|
||||
"hex": "03E5A5"
|
||||
},
|
||||
{
|
||||
"title": "Pingvin Share",
|
||||
"hex": "485099"
|
||||
},
|
||||
{
|
||||
"title": "Plutus",
|
||||
"hex": "DEC685"
|
||||
@@ -153,6 +163,9 @@
|
||||
{
|
||||
"title": "Proton"
|
||||
},
|
||||
{
|
||||
"title": "Proxmox"
|
||||
},
|
||||
{
|
||||
"title": "Revolt",
|
||||
"hex": "858585"
|
||||
@@ -196,10 +209,6 @@
|
||||
"title": "Twingate",
|
||||
"hex": "858585"
|
||||
},
|
||||
{
|
||||
"title": "Twitter",
|
||||
"slug": "x"
|
||||
},
|
||||
{
|
||||
"title": "Ubisoft",
|
||||
"hex": "4285f4"
|
||||
@@ -219,7 +228,9 @@
|
||||
"title": "Wise"
|
||||
},
|
||||
{
|
||||
"title": "X"
|
||||
"title": "X",
|
||||
"altNames": ["twitter"],
|
||||
"slug": "x"
|
||||
},
|
||||
{
|
||||
"title": "NextDNS"
|
||||
@@ -228,8 +239,5 @@
|
||||
"title": "Skiff",
|
||||
"hex": "EF5A3C"
|
||||
}
|
||||
{
|
||||
"title": "zzz_dev_test_icon"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
1
assets/custom-icons/icons/mastodon.svg
Normal file
1
assets/custom-icons/icons/mastodon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Mastodon</title><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
30
assets/custom-icons/icons/pingvinshare.svg
Normal file
30
assets/custom-icons/icons/pingvinshare.svg
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 943.11 911.62"><script xmlns=""/>
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #37474f;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #46509e;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<ellipse class="cls-3" cx="471.56" cy="454.28" rx="471.56" ry="454.28"/>
|
||||
<g>
|
||||
<g>
|
||||
<ellipse class="cls-2" cx="471.56" cy="390.28" rx="233.66" ry="207"/>
|
||||
<path class="cls-2" d="m705.22,848.95c-36.69,21.14-123.09,64.33-240.64,62.57-109.54-1.63-190.04-41.45-226.68-62.57v-454.19h467.33v454.19Z"/>
|
||||
</g>
|
||||
<path class="cls-1" d="m658.81,397.7v475.8c-36.98,15.7-98.93,36.54-177.98,38.04-88.67,1.69-157.75-21.73-196.2-38.04v-475.8c0-95.55,83.77-173.02,187.09-173.02s187.09,77.47,187.09,173.02Z"/>
|
||||
<polygon class="cls-3" points="565.02 431.68 471.56 514.49 378.09 431.68 565.02 431.68"/>
|
||||
<ellipse class="cls-2" cx="378.09" cy="369.58" rx="23.37" ry="20.7"/>
|
||||
<ellipse class="cls-2" cx="565.02" cy="369.58" rx="23.37" ry="20.7"/>
|
||||
<path class="cls-2" d="m658.49,400.63c0-40.04-36.59-72.45-81.78-72.45s-81.78,32.41-81.78,72.45c0,11.14,2.81,21.65,7.9,31.05h-62.54c5.1-9.4,7.9-19.91,7.9-31.05,0-40.04-36.59-72.45-81.78-72.45s-81.78,32.41-81.78,72.45l-46.73-10.35c0-114.32,104.63-207,233.66-207s233.66,92.69,233.66,207l-46.73,10.35Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
6
assets/custom-icons/icons/proxmox.svg
Normal file
6
assets/custom-icons/icons/proxmox.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 388.755L126.571 249.665L0 110.571C13.9095 96.6616 33.3816 87.8523 54.7141 87.8523C77.4297 87.8523 97.3643 97.5937 111.274 112.89L181.281 189.854L235.992 249.665L181.281 309.942L111.274 386.902C97.3643 402.202 77.4297 411.94 54.7141 411.94C33.3816 411.94 13.9095 403.127 0 388.755ZM500 388.757L373.43 249.667L500 110.573C486.091 96.6633 466.619 87.8536 445.286 87.8536C422.571 87.8536 402.636 97.5956 388.726 112.892L318.719 189.856L264.008 249.667L318.719 309.943L388.726 386.904C402.636 402.204 422.571 411.942 445.286 411.942C466.619 411.942 486.091 403.128 500 388.757Z" fill="#E57000"/>
|
||||
<g style="mix-blend-mode:difference">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M249.934 235.708L299.87 180.692L415.394 53.7358C402.7 41.0404 384.926 33 365.456 33C344.722 33 326.528 41.8913 313.83 55.8522L249.934 126.1L185.607 55.8522C172.491 41.4659 155.138 33 134.4 33C114.941 33 97.163 41.0404 84.4681 53.7358L199.996 180.692L249.934 235.708ZM249.937 263.615L299.873 318.631L415.398 445.588C402.703 458.283 384.929 466.323 365.459 466.323C344.725 466.323 326.531 457.432 313.834 443.471L249.937 373.224L185.61 443.471C172.494 457.857 155.141 466.323 134.403 466.323C114.944 466.323 97.1661 458.283 84.4713 445.588L200 318.631L249.937 263.615Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -1,5 +0,0 @@
|
||||
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g style="mix-blend-mode:difference">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M250.207 5C111.849 5 0 117.52 0 256.723C0 367.996 71.6655 462.186 171.084 495.522C183.514 498.028 188.067 490.106 188.067 483.442C188.067 477.606 187.658 457.603 187.658 436.761C118.056 451.767 103.562 406.754 103.562 406.754C92.3769 377.581 75.8036 370.083 75.8036 370.083C53.0231 354.662 77.463 354.662 77.463 354.662C102.733 356.329 115.992 380.501 115.992 380.501C138.358 418.84 174.398 408.007 188.897 401.338C190.966 385.084 197.599 373.832 204.641 367.582C149.128 361.746 90.7226 340.075 90.7226 243.385C90.7226 215.879 100.658 193.374 116.402 175.872C113.918 169.622 105.217 143.779 118.891 109.189C118.891 109.189 140.017 102.519 187.653 135.028C208.047 129.517 229.079 126.714 250.207 126.691C271.333 126.691 292.869 129.611 312.756 135.028C360.396 102.519 381.523 109.189 381.523 109.189C395.197 143.779 386.491 169.622 384.007 175.872C400.165 193.374 409.691 215.879 409.691 243.385C409.691 340.075 351.285 361.326 295.358 367.582C304.474 375.499 312.341 390.5 312.341 414.257C312.341 448.013 311.931 475.105 311.931 483.437C311.931 490.106 316.49 498.028 328.914 495.527C428.333 462.18 499.999 367.996 499.999 256.723C500.409 117.52 388.15 5 250.207 5Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
Submodule assets/simple-icons updated: 7e1ad45175...8e7701d6a4
@@ -41,6 +41,8 @@ class InvalidStateError extends AssertionError {
|
||||
|
||||
class KeyDerivationError extends Error {}
|
||||
|
||||
class LoginKeyDerivationError extends Error {}
|
||||
|
||||
class SrpSetupNotCompleteError extends Error {}
|
||||
|
||||
class AuthenticatorKeyNotFound extends Error {}
|
||||
|
||||
1
lib/l10n/arb/app_ar.arb
Normal file
1
lib/l10n/arb/app_ar.arb
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"account": "Account",
|
||||
"unlock": "Unlock",
|
||||
"recoveryKey": "Recovery key",
|
||||
"counterAppBarTitle": "Counter",
|
||||
"@counterAppBarTitle": {
|
||||
@@ -83,9 +84,12 @@
|
||||
"importFromApp": "Import codes from {appName}",
|
||||
"importGoogleAuthGuide": "Export your accounts from Google Authenticator to a QR code using the \"Transfer Accounts\" option. Then using another device, scan the QR code.\n\nTip: You can use your laptop's webcam to take a picture of the QR code.",
|
||||
"importSelectJsonFile": "Select JSON file",
|
||||
"importSelectAppExport": "Select {appName} export file",
|
||||
"importEnteEncGuide": "Select the encrypted JSON file exported from ente",
|
||||
"importRaivoGuide": "Use the \"Export OTPs to Zip archive\" option in Raivo's Settings.\n\nExtract the zip file and import the JSON file.",
|
||||
"importBitwardenGuide": "Use the \"Export vault\" option within Bitwarden Tools and import the unencrypted JSON file.",
|
||||
"importAegisGuide": "Use the \"Export the vault\" option in Aegis's Settings.\n\nIf your vault is encrypted, you will need to enter vault password to decrypt the vault.",
|
||||
"import2FasGuide": "Use the \"Settings->Backup -Export\" option in 2FAS.\n\nIf your backup is encrypted, you will need to enter the password to decrypt the backup",
|
||||
"exportCodes": "Export codes",
|
||||
"importLabel": "Import",
|
||||
"importInstruction": "Please select a file that contains a list of your codes in the following format",
|
||||
@@ -97,6 +101,8 @@
|
||||
"authToViewYourRecoveryKey": "Please authenticate to view your recovery key",
|
||||
"authToChangeYourEmail": "Please authenticate to change your email",
|
||||
"authToChangeYourPassword": "Please authenticate to change your password",
|
||||
"authToViewSecrets": "Please authenticate to view your secrets",
|
||||
"authToInitiateSignIn": "Please authenticate to initiate sign in for backup.",
|
||||
"ok": "Ok",
|
||||
"cancel": "Cancel",
|
||||
"yes": "Yes",
|
||||
@@ -126,6 +132,7 @@
|
||||
"faq_q_5": "How can I enable FaceID lock in ente Auth",
|
||||
"faq_a_5": "You can enable FaceID lock under Settings → Security → Lockscreen.",
|
||||
"somethingWentWrongMessage": "Something went wrong, please try again",
|
||||
|
||||
"leaveFamily": "Leave family",
|
||||
"leaveFamilyMessage": "Are you sure that you want to leave the family plan?",
|
||||
"inFamilyPlanMessage": "You are on a family plan!",
|
||||
@@ -329,6 +336,7 @@
|
||||
"offlineModeWarning": "You have chosen to proceed without backups. Please take manual backups to make sure your codes are safe.",
|
||||
"showLargeIcons": "Show large icons",
|
||||
"shouldHideCode": "Hide codes",
|
||||
"doubleTapToViewHiddenCode" : "You can double tap on an entry to view code",
|
||||
"focusOnSearchBar": "Focus search on app start",
|
||||
"confirmUpdatingkey": "Are you sure you want to update the secret key?",
|
||||
"minimizeAppOnCopy": "Minimize app on copy",
|
||||
@@ -336,5 +344,59 @@
|
||||
"deleteCodeAuthMessage": "Authenticate to delete code",
|
||||
"showQRAuthMessage": "Authenticate to show QR code",
|
||||
"confirmAccountDeleteTitle": "Confirm account deletion",
|
||||
"confirmAccountDeleteMessage": "This account is linked to other ente apps, if you use any.\n\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted."
|
||||
"confirmAccountDeleteMessage": "This account is linked to other ente apps, if you use any.\n\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
|
||||
"androidBiometricHint": "Verify identity",
|
||||
"@androidBiometricHint": {
|
||||
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidBiometricNotRecognized": "Not recognized. Try again.",
|
||||
"@androidBiometricNotRecognized": {
|
||||
"description": "Message to let the user know that authentication was failed. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidBiometricSuccess": "Success",
|
||||
"@androidBiometricSuccess": {
|
||||
"description": "Message to let the user know that authentication was successful. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidCancelButton": "Cancel",
|
||||
"@androidCancelButton": {
|
||||
"description": "Message showed on a button that the user can click to leave the current dialog. It is used on Android side. Maximum 30 characters."
|
||||
},
|
||||
"androidSignInTitle": "Authentication required",
|
||||
"@androidSignInTitle": {
|
||||
"description": "Message showed as a title in a dialog which indicates the user that they need to scan biometric to continue. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidBiometricRequiredTitle": "Biometric required",
|
||||
"@androidBiometricRequiredTitle": {
|
||||
"description": "Message showed as a title in a dialog which indicates the user has not set up biometric authentication on their device. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidDeviceCredentialsRequiredTitle": "Device credentials required",
|
||||
"@androidDeviceCredentialsRequiredTitle": {
|
||||
"description": "Message showed as a title in a dialog which indicates the user has not set up credentials authentication on their device. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidDeviceCredentialsSetupDescription": "Device credentials required",
|
||||
"@androidDeviceCredentialsSetupDescription": {
|
||||
"description": "Message advising the user to go to the settings and configure device credentials on their device. It shows in a dialog on Android side."
|
||||
},
|
||||
"goToSettings": "Go to settings",
|
||||
"@goToSettings": {
|
||||
"description": "Message showed on a button that the user can click to go to settings pages from the current dialog. It is used on both Android and iOS side. Maximum 30 characters."
|
||||
},
|
||||
"androidGoToSettingsDescription": "Biometric authentication is not set up on your device. Go to 'Settings > Security' to add biometric authentication.",
|
||||
"@androidGoToSettingsDescription": {
|
||||
"description": "Message advising the user to go to the settings and configure biometric on their device. It shows in a dialog on Android side."
|
||||
},
|
||||
"iOSLockOut": "Biometric authentication is disabled. Please lock and unlock your screen to enable it.",
|
||||
"@iOSLockOut": {
|
||||
"description": "Message advising the user to re-enable biometrics on their device. It shows in a dialog on iOS side."
|
||||
},
|
||||
"iOSGoToSettingsDescription": "Biometric authentication is not set up on your device. Please either enable Touch ID or Face ID on your phone.",
|
||||
"@iOSGoToSettingsDescription": {
|
||||
"description": "Message advising the user to go to the settings and configure Biometrics for their device. It shows in a dialog on iOS side."
|
||||
},
|
||||
"iOSOkButton": "OK",
|
||||
"@iOSOkButton": {
|
||||
"description": "Message showed on a button that the user can click to leave the current dialog. It is used on iOS side. Maximum 30 characters."
|
||||
},
|
||||
"noInternetConnection": "No internet connection",
|
||||
"pleaseCheckYourInternetConnectionAndTryAgain": "Please check your internet connection and try again."
|
||||
}
|
||||
|
||||
@@ -334,5 +334,7 @@
|
||||
"minimizeAppOnCopy": "Minimizar aplicación al copiar",
|
||||
"editCodeAuthMessage": "Autenticar para editar código",
|
||||
"deleteCodeAuthMessage": "Autenticar para borrar código",
|
||||
"showQRAuthMessage": "Autenticar para mostrar código QR"
|
||||
"showQRAuthMessage": "Autenticar para mostrar código QR",
|
||||
"confirmAccountDeleteTitle": "Confirmar eliminación de la cuenta",
|
||||
"confirmAccountDeleteMessage": "Esta cuenta está vinculada a otras aplicaciones de ente, si utiliza alguna.\n\nSe programará la eliminación de los datos que cargue en todas las aplicaciones de ente y su cuenta se eliminará permanentemente."
|
||||
}
|
||||
@@ -334,5 +334,7 @@
|
||||
"minimizeAppOnCopy": "Réduire l'application après la copie",
|
||||
"editCodeAuthMessage": "Authentification requise pour modifier le code",
|
||||
"deleteCodeAuthMessage": "Authentification requise pour supprimer le code",
|
||||
"showQRAuthMessage": "Authentification requise pour afficher le code QR"
|
||||
"showQRAuthMessage": "Authentification requise pour afficher le code QR",
|
||||
"confirmAccountDeleteTitle": "Confirmer la suppression du compte",
|
||||
"confirmAccountDeleteMessage": "Ce compte peut être lié à d'autres applications ente.\n\nVos données seront bientôt effacées de toutes les applications et votre compte sera définitivement supprimé."
|
||||
}
|
||||
@@ -1,32 +1,37 @@
|
||||
{
|
||||
"account": "アカウント",
|
||||
"recoveryKey": "回復キー",
|
||||
"counterAppBarTitle": "カウンタ",
|
||||
"counterAppBarTitle": "カウンター",
|
||||
"@counterAppBarTitle": {
|
||||
"description": "Text shown in the AppBar of the Counter Page"
|
||||
},
|
||||
"onBoardingBody": "2要素認証コードの保護",
|
||||
"onBoardingBody": "2要素認証コードを安全にバックアップ",
|
||||
"onBoardingGetStarted": "さあ、はじめよう",
|
||||
"setupFirstAccount": "最初のアカウントを設定しましょう",
|
||||
"importScanQrCode": "QRコードをスキャン",
|
||||
"importEnterSetupKey": "セットアップキーを入力",
|
||||
"importScanQrCode": "QR コードを読み取り",
|
||||
"qrCode": "QR コード",
|
||||
"importEnterSetupKey": "手動で入力",
|
||||
"importAccountPageTitle": "アカウントの詳細を入力",
|
||||
"secretCanNotBeEmpty": "秘密鍵は空欄にできません",
|
||||
"bothIssuerAndAccountCanNotBeEmpty": "発行者とアカウントの両方を空欄にはできません",
|
||||
"incorrectDetails": "不正な詳細",
|
||||
"pleaseVerifyDetails": "詳細を確認し、もう一度お試しください",
|
||||
"codeIssuerHint": "発行者",
|
||||
"codeSecretKeyHint": "シークレットキー",
|
||||
"codeSecretKeyHint": "秘密鍵",
|
||||
"codeAccountHint": "アカウント (you@domain.com)",
|
||||
"accountKeyType": "キーの種類",
|
||||
"sessionExpired": "セッションの有効期間超過",
|
||||
"accountKeyType": "鍵の種類",
|
||||
"sessionExpired": "セッションが失効しました",
|
||||
"@sessionExpired": {
|
||||
"description": "Title of the dialog when the users current session is invalid/expired"
|
||||
},
|
||||
"pleaseLoginAgain": "再度ログインしてください",
|
||||
"loggingOut": "ログアウト中...",
|
||||
"timeBasedKeyType": "時間ベース (TOTP)",
|
||||
"counterBasedKeyType": "カウンタベース (HOTP)",
|
||||
"counterBasedKeyType": "カウンターベース (HOTP)",
|
||||
"saveAction": "保存",
|
||||
"nextTotpTitle": "次へ",
|
||||
"deleteCodeTitle": "コードを削除しますか?",
|
||||
"deleteCodeMessage": "このコードを削除してもよろしいですか? このアクションは元に戻せません。",
|
||||
"deleteCodeTitle": "コードを削除しますか?",
|
||||
"deleteCodeMessage": "本当にこのコードを削除してもよろしいですか?この決定は元に戻せません。",
|
||||
"viewLogsAction": "ログの表示",
|
||||
"sendLogsDescription": "問題のデバッグに役立つログが送信されます。機密情報が記録されないように予防措置を講じていますが、共有する前にこれらのログを確認することをお勧めします。",
|
||||
"preparingLogsTitle": "ログを準備中...",
|
||||
@@ -40,7 +45,7 @@
|
||||
}
|
||||
},
|
||||
"copyEmailAction": "メールをコピー",
|
||||
"exportLogsAction": "ログのエクスポート",
|
||||
"exportLogsAction": "ログをエクスポート",
|
||||
"reportABug": "バグを報告",
|
||||
"crashAndErrorReporting": "クラッシュとエラーの報告",
|
||||
"reportBug": "バグを報告",
|
||||
@@ -53,25 +58,41 @@
|
||||
}
|
||||
},
|
||||
"contactSupport": "サポートに問い合わせ",
|
||||
"rateUsOnStore": "{storeName} で評価",
|
||||
"blog": "ブログ",
|
||||
"merchandise": "グッズ",
|
||||
"verifyPassword": "パスワードを確認",
|
||||
"pleaseWait": "お待ちください...",
|
||||
"generatingEncryptionKeysTitle": "暗号化キーを生成中...",
|
||||
"recreatePassword": "パスワードを再設定",
|
||||
"recreatePasswordMessage": "現在のデバイスは、パスワードを確認するのに十分ではありません。全てのデバイスで利用できるように再生成する必要があります。\n\nリカバリーキーを使用してログインし、パスワードを再生成してください (ご希望の場合は再度同じパスワードを使用できます)",
|
||||
"generatingEncryptionKeysTitle": "暗号化鍵を生成中...",
|
||||
"recreatePassword": "パスワードを再作成",
|
||||
"recreatePasswordMessage": "現在のデバイスは、パスワードを確認するのに十分ではありません。全てのデバイスで利用できるように再生成する必要があります。\n\n回復キーを使用してログインし、パスワードを再生成してください (ご希望の場合は再度同じパスワードを使用できます)",
|
||||
"useRecoveryKey": "回復キーを使用",
|
||||
"incorrectPasswordTitle": "パスワードが正しくありません",
|
||||
"welcomeBack": "おかえりなさい!",
|
||||
"welcomeBack": "おかえりなさい!",
|
||||
"madeWithLoveAtPrefix": "made with ❤️ at ",
|
||||
"supportDevs": "プロジェクト支援のために <bold-green>ente</bold-green> に登録",
|
||||
"supportDiscount": "クーポンコード \"AUTH\" を使用すると、初年度が10%オフになります。",
|
||||
"supportDiscount": "クーポンコード \"AUTH\" の使用で初年度が 10% オフに",
|
||||
"changeEmail": "メールアドレスを変更",
|
||||
"changePassword": "パスワードを変更",
|
||||
"data": "データ",
|
||||
"importCodes": "コードをインポート",
|
||||
"importTypePlainText": "プレーンテキスト",
|
||||
"importTypeEnteEncrypted": "ente 暗号化エクスポート",
|
||||
"passwordForDecryptingExport": "復号化用パスワード",
|
||||
"passwordEmptyError": "パスワードは空欄にできません",
|
||||
"importFromApp": "{appName} からコードをインポート",
|
||||
"importGoogleAuthGuide": "Google Authenticator の \"アカウントを転送\" オプションを使用してあなたのアカウントを QR コードにエクスポートしてください。その後、他のデバイスで QR コードを読み取ってください。\n\nヒント: ノートパソコンのウェブカメラを使用して QR コードを撮影することができます。",
|
||||
"importSelectJsonFile": "JSON ファイルを選択",
|
||||
"importEnteEncGuide": "ente からエクスポートされた暗号化 JSON ファイルを選択",
|
||||
"importRaivoGuide": "Raivo の設定の \"OTP を zip アーカイブにエクスポート\" を使用してください。\n\nzip ファイルを解凍し JSON ファイルをインポートしてください。",
|
||||
"importAegisGuide": "Aegis の設定の \"保管庫をエクスポート\" を使用してください。\n\n保管庫が暗号化されている場合、保管庫を復号するためにパスワードの入力が必要になります。",
|
||||
"exportCodes": "コードをエクスポート",
|
||||
"importInstruction": "以下のフォーマットで、コードのリストを含むファイルを選択してください",
|
||||
"importLabel": "インポート",
|
||||
"importInstruction": "以下の形式のコードのリストを含むファイルを選択してください",
|
||||
"importCodeDelimiterInfo": "コードはカンマまたは改行で区切ることができます",
|
||||
"selectFile": "ファイルを選択",
|
||||
"emailVerificationToggle": "メール認証",
|
||||
"authToChangeEmailVerificationSetting": "メール認証を変更するためには認証が必要です",
|
||||
"authToViewYourRecoveryKey": "回復キーを表示するためには認証が必要です",
|
||||
"authToChangeYourEmail": "メールアドレスを変更するためには認証が必要です",
|
||||
"authToChangeYourPassword": "パスワードを変更するためには認証が必要です",
|
||||
@@ -79,8 +100,9 @@
|
||||
"cancel": "キャンセル",
|
||||
"yes": "はい",
|
||||
"no": "いいえ",
|
||||
"email": "Eメール",
|
||||
"email": "E メール",
|
||||
"support": "サポート",
|
||||
"general": "一般",
|
||||
"settings": "設定",
|
||||
"copied": "コピーしました",
|
||||
"pleaseTryAgain": "再度お試しください",
|
||||
@@ -90,6 +112,12 @@
|
||||
"enterYourPasswordHint": "パスワードを入力してください",
|
||||
"forgotPassword": "パスワードを忘れた場合",
|
||||
"oops": "おっと",
|
||||
"suggestFeatures": "機能を提案",
|
||||
"faq": "FAQ",
|
||||
"faq_q_1": "ente Auth はどのくらい安全ですか?",
|
||||
"faq_a_1": "ente でバックアップされたすべてのコードはエンドツーエンドで暗号化されて保管されます。これはあなただけがコードにアクセスできることを意味します。私たちのアプリはオープンソースであり、私たちの暗号は外部有識者によって検証済みです。",
|
||||
"faq_q_2": "パソコンから私のコードにアクセスできますか?",
|
||||
"faq_a_2": "auth.ente.io で Web からコードにアクセス可能です。",
|
||||
"somethingWentWrongMessage": "問題が発生しました、再試行してください",
|
||||
"leaveFamily": "ファミリーから退会",
|
||||
"leaveFamilyMessage": "本当にファミリープランを退会しますか?",
|
||||
|
||||
92
lib/l10n/arb/app_ka.arb
Normal file
92
lib/l10n/arb/app_ka.arb
Normal file
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"account": "ანგარიში",
|
||||
"unlock": "განბლოკვა",
|
||||
"recoveryKey": "აღდგენის კოდი",
|
||||
"counterAppBarTitle": "მრიცხველზე დაფუძნებული",
|
||||
"@counterAppBarTitle": {
|
||||
"description": "Text shown in the AppBar of the Counter Page"
|
||||
},
|
||||
"onBoardingBody": "შექმენით თქვენი ორმხრივი აუთენთიფიკაციის კოდების სარეზერვო ასლი უსაფრთხოდ",
|
||||
"onBoardingGetStarted": "დაწყება",
|
||||
"setupFirstAccount": "დააყენეთ თქვენი პირველი ანგარიში",
|
||||
"importScanQrCode": "QR კოდის დასკანერება",
|
||||
"qrCode": "QR კოდი",
|
||||
"importEnterSetupKey": "შეიყვანეთ დაყენების კოდი",
|
||||
"importAccountPageTitle": "შეიყვანეთ ანგარიშის დეტალები",
|
||||
"secretCanNotBeEmpty": "გასაღების ველი არ შეიძლება ცარიელი იყოს",
|
||||
"bothIssuerAndAccountCanNotBeEmpty": "ანგარიშის მომწოდებლისა და ანგარიშის ველი არ შეიძლება ცარიელი იყოს",
|
||||
"incorrectDetails": "არასწორი მონაცემები",
|
||||
"pleaseVerifyDetails": "გთხოვთ, გადაამოწმოთ დეტალები და სცადოთ ხელახლა",
|
||||
"codeIssuerHint": "მომწოდებელი",
|
||||
"codeSecretKeyHint": "გასაღები",
|
||||
"codeAccountHint": "ანგარიში (you@domain.com)",
|
||||
"accountKeyType": "გასაღების ტიპი",
|
||||
"sessionExpired": "სესიის დრო ამოიწურა",
|
||||
"@sessionExpired": {
|
||||
"description": "Title of the dialog when the users current session is invalid/expired"
|
||||
},
|
||||
"pleaseLoginAgain": "გთხოვთ, გაიაროთ ავტორიზაცია ხელახლა",
|
||||
"loggingOut": "მიმდინარეობს გამოსვლა...",
|
||||
"timeBasedKeyType": "დროზე დაფუძნებული (TOTP)",
|
||||
"counterBasedKeyType": "მრიცხველზე დაფუძნებული (HOTP)",
|
||||
"saveAction": "შენახვა",
|
||||
"nextTotpTitle": "შემდეგი",
|
||||
"deleteCodeTitle": "გსურთ კოდის წაშლა?",
|
||||
"deleteCodeMessage": "დარწმუნებული ხართ რომ გსურთ ამ კოდის წაშლა? ამ მოქმედების გაუქმება შეუძლებელია.",
|
||||
"viewLogsAction": "აღრიცხვის ფაილების ნახვა",
|
||||
"sendLogsDescription": "თქვენი პრობლემის აღმოსაფხვრელად, ეს ქმედება გააგზავნის აღრიცხვის ფაილებს. მიუხედავად იმისა, რომ ჩვენ ვიღებთ უსაფრთხოების ზომებს, რათა სენსიტიური ინფორმაცია არ მოხვდეს აღრიცხვის ფაილებში, გაგზავნამდე, გირჩევთ, გადახედოთ აღრიცხვის ფაილებს.",
|
||||
"preparingLogsTitle": "მიმდინარეობს აღრიცხვის ფაილების მზადება...",
|
||||
"emailLogsTitle": "აღრიცხვის ფაილების ელექტრონული ფოსტით გაგზავნა",
|
||||
"emailLogsMessage": "გთხოვთ, გამოაგზავნოთ აღრიცხვის ფაილები {email}-ზე",
|
||||
"@emailLogsMessage": {
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"copyEmailAction": "ელექტრონული ფოსტის დაკოპირება",
|
||||
"exportLogsAction": "აღრიცხვის ფაილების ექსპორტირება",
|
||||
"reportABug": "პრობლემის შესახებ შეტყობინება",
|
||||
"crashAndErrorReporting": "აპლიკაციის ხარვეზის & პრობლემის შეტყობინება",
|
||||
"reportBug": "პრობლემის შეტყობინება",
|
||||
"emailUsMessage": "გთხოვთ, მოგვწეროთ ელექტრონულ ფოსტაზე {email}",
|
||||
"@emailUsMessage": {
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contactSupport": "მხარდაჭერის გუნდთან დაკავშირება",
|
||||
"rateUsOnStore": "შეგვაფასეთ {storeName}-ზე",
|
||||
"blog": "ბლოგი",
|
||||
"merchandise": "მერჩანტი",
|
||||
"verifyPassword": "პაროლის დასტური",
|
||||
"pleaseWait": "გთხოვთ, დაელოდოთ...",
|
||||
"generatingEncryptionKeysTitle": "მიმდინარეობს დაშიფრვის გასაღებების გენერირება...",
|
||||
"recreatePassword": "პაროლის ხელახლა შექმნა",
|
||||
"incorrectPasswordTitle": "არასწორი პაროლი",
|
||||
"welcomeBack": "კეთილი იყოს თქვენი დაბრუნება!",
|
||||
"madeWithLoveAtPrefix": "შეიქმნა ❤️ ",
|
||||
"changeEmail": "ელექტრონული ფოსტის შეცვლა",
|
||||
"changePassword": "პაროლის შეცვლა",
|
||||
"data": "მონაცემები",
|
||||
"importCodes": "კოდების იმპორტირება",
|
||||
"importTypePlainText": "სტანდარტული ტექსტი",
|
||||
"importTypeEnteEncrypted": "ente დაშიფრული ექსპორტი",
|
||||
"passwordForDecryptingExport": "ექსპორტის გაშიფრვის პაროლი",
|
||||
"passwordEmptyError": "პაროლის ველი არ შეიძლება იყოს ცარიელი",
|
||||
"emailVerificationToggle": "ელექტრონული ფოსტის ვერიფიკაცია",
|
||||
"cancel": "გაუქმება",
|
||||
"yes": "დიახ",
|
||||
"no": "არა",
|
||||
"email": "ელექტრონული ფოსტა",
|
||||
"support": "მხარდაჭერა",
|
||||
"general": "ზოგადი",
|
||||
"settings": "პარამეტრები",
|
||||
"copied": "დაკოპირებულია",
|
||||
"pleaseTryAgain": "გთხოვთ, სცადოთ ხელახლა",
|
||||
"existingUser": "არსებული მომხმარებელი",
|
||||
"delete": "წაშლა"
|
||||
}
|
||||
@@ -92,6 +92,7 @@
|
||||
"importCodeDelimiterInfo": "De codes mogen gescheiden worden door een komma of een nieuwe regel",
|
||||
"selectFile": "Bestand selecteren",
|
||||
"emailVerificationToggle": "E-mailverificatie",
|
||||
"emailVerificationEnableWarning": "Als u de 2FA van uw e-mail bij ons opslaat, kan de verificatie van die e-mail resulteren in een riskante impasse. Als u bent uitgesloten van een van beide diensten, kunt u zich mogelijk niet meer aanmelden bij de andere.",
|
||||
"authToChangeEmailVerificationSetting": "Gelieve te verifiëren om de e-mailverificatie te wijzigen",
|
||||
"authToViewYourRecoveryKey": "Graag verifiëren om uw herstelsleutel te bekijken",
|
||||
"authToChangeYourEmail": "Graag verifiëren om je e-mailadres te wijzigen",
|
||||
@@ -327,8 +328,13 @@
|
||||
"sigInBackupReminder": "Exporteer de codes zodat je een back-up hebt waarvandaan je kan herstellen.",
|
||||
"offlineModeWarning": "Je hebt ervoor gekozen om verder te gaan zonder back-ups. Neem handmatige back-ups om ervoor te zorgen dat jouw codes veilig zijn.",
|
||||
"showLargeIcons": "Grote iconen",
|
||||
"shouldHideCode": "Verberg codes",
|
||||
"focusOnSearchBar": "Focus zoekveld na starten app",
|
||||
"confirmUpdatingkey": "Weet u zeker dat u de geheime sleutel wilt bijwerken?",
|
||||
"minimizeAppOnCopy": "Na kopiëren app minimaliseren",
|
||||
"editCodeAuthMessage": "Authenticeren om code te bewerken",
|
||||
"deleteCodeAuthMessage": "Authenticeren om code te verwijderen",
|
||||
"showQRAuthMessage": "Authenticeren om QR-code te tonen"
|
||||
"showQRAuthMessage": "Authenticeren om QR-code te tonen",
|
||||
"confirmAccountDeleteTitle": "Account verwijderen bevestigen",
|
||||
"confirmAccountDeleteMessage": "Dit account is gekoppeld aan andere ente apps, als je er gebruik van maakt.\n\nJe geüploade gegevens worden in alle ente apps gepland voor verwijdering, en je account wordt permanent verwijderd voor alle ente diensten."
|
||||
}
|
||||
@@ -334,5 +334,7 @@
|
||||
"minimizeAppOnCopy": "Minimalizuj aplikację przy kopiowaniu",
|
||||
"editCodeAuthMessage": "Uwierzytelnij, aby edytować kod",
|
||||
"deleteCodeAuthMessage": "Uwierzytelnij, aby usunąć kod",
|
||||
"showQRAuthMessage": "Uwierzytelnij, aby pokazać kod QR"
|
||||
"showQRAuthMessage": "Uwierzytelnij, aby pokazać kod QR",
|
||||
"confirmAccountDeleteTitle": "Potwierdź usunięcie konta",
|
||||
"confirmAccountDeleteMessage": "To konto jest połączone z innymi aplikacjami ente, jeśli ich używasz.\n\nTwoje przesłane dane, we wszystkich aplikacjach ente, zostaną zaplanowane do usunięcia, a Twoje konto zostanie trwale usunięte."
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"account": "Аккаунт",
|
||||
"unlock": "Разблокировать",
|
||||
"recoveryKey": "Ключ восстановления",
|
||||
"counterAppBarTitle": "Счетчик",
|
||||
"@counterAppBarTitle": {
|
||||
@@ -85,6 +86,7 @@
|
||||
"importSelectJsonFile": "Выбрать JSON-файл",
|
||||
"importEnteEncGuide": "Выберите зашифрованный JSON-файл, экспортированный из ente",
|
||||
"importRaivoGuide": "Используйте опцию «Export OTPs to Zip archive» в настройках Raivo.\n\nРаспакуйте zip-архив и импортируйте JSON-файл.",
|
||||
"importBitwardenGuide": "Используйте опцию \"Экспортировать хранилище\" в Bitwarden Tools и импортируйте незашифрованный JSON файл.",
|
||||
"importAegisGuide": "Используйте опцию «Экспортировать хранилище» в настройках Aegis.\n\nЕсли ваше хранилище зашифровано, то для его расшифровки потребуется ввести пароль хранилища.",
|
||||
"exportCodes": "Экспортировать коды",
|
||||
"importLabel": "Импорт",
|
||||
@@ -92,16 +94,19 @@
|
||||
"importCodeDelimiterInfo": "Коды могут быть разделены запятой или новой строкой",
|
||||
"selectFile": "Выбрать файл",
|
||||
"emailVerificationToggle": "Подтверждение электронной почты",
|
||||
"emailVerificationEnableWarning": "Если вы храните у нас двухфакторную аутентификацию в своей электронной почте, включение проверки электронной почты может привести к тупиковой ситуации. Если у вас заблокирован доступ к одной службе, возможно, вы не сможете войти в другую.",
|
||||
"authToChangeEmailVerificationSetting": "Авторизуйтесь, чтобы изменить подтверждение электронной почты",
|
||||
"authToViewYourRecoveryKey": "Пожалуйста, авторизуйтесь для просмотра вашего ключа восстановления",
|
||||
"authToChangeYourEmail": "Пожалуйста, авторизуйтесь, чтобы изменить адрес электронной почты",
|
||||
"authToChangeYourPassword": "Пожалуйста, авторизуйтесь, чтобы изменить пароль",
|
||||
"authToViewSecrets": "Пожалуйста, авторизуйтесь для просмотра ваших секретов",
|
||||
"ok": "Ок",
|
||||
"cancel": "Отменить",
|
||||
"yes": "Да",
|
||||
"no": "Нет",
|
||||
"email": "Электронная почта",
|
||||
"support": "Поддержка",
|
||||
"general": "Общие",
|
||||
"settings": "Настройки",
|
||||
"copied": "Скопировано",
|
||||
"pleaseTryAgain": "Пожалуйста, попробуйте ещё раз",
|
||||
@@ -181,6 +186,7 @@
|
||||
"enterDetailsManually": "Ввести детали вручную",
|
||||
"edit": "Редактировать",
|
||||
"copiedToClipboard": "Скопировано",
|
||||
"copiedNextToClipboard": "Следующий код скопирован в буфер обмена",
|
||||
"error": "Ошибка",
|
||||
"recoveryKeyCopiedToClipboard": "Ключ восстановления скопирован в буфер обмена",
|
||||
"recoveryKeyOnForgotPassword": "Если вы забыли свой пароль, то восстановить данные можно только с помощью этого ключа.",
|
||||
@@ -251,6 +257,10 @@
|
||||
"privacy": "Конфиденциальность",
|
||||
"terms": "Условия использования",
|
||||
"checkForUpdates": "Проверить наличие обновлений",
|
||||
"downloadUpdate": "Скачать",
|
||||
"criticalUpdateAvailable": "Доступно критическое обновление",
|
||||
"updateAvailable": "Доступно обновление",
|
||||
"update": "Обновить",
|
||||
"checking": "Проверка...",
|
||||
"youAreOnTheLatestVersion": "Вы используете последнюю версию",
|
||||
"warning": "Предупреждение",
|
||||
@@ -315,7 +325,73 @@
|
||||
"plainText": "Обычный текст",
|
||||
"passwordToEncryptExport": "Пароль для шифрования экспорта",
|
||||
"export": "Экспорт",
|
||||
"useOffline": "Использовать без резервных копий",
|
||||
"signInToBackup": "Войдите в систему, чтобы создать резервную копию своих кодов",
|
||||
"singIn": "Войти",
|
||||
"sigInBackupReminder": "Экспортируйте свои коды, чтобы убедиться, что у вас есть резервная копия, из которой можно восстановить.",
|
||||
"offlineModeWarning": "Вы решили продолжить без резервных копий. Пожалуйста, создайте резервные копии вручную, чтобы убедиться, что ваши коды в безопасности.",
|
||||
"showLargeIcons": "Показать большие значки",
|
||||
"shouldHideCode": "Скрыть коды",
|
||||
"focusOnSearchBar": "Фокусировать поиск при запуске приложения",
|
||||
"confirmUpdatingkey": "Вы уверены, что хотите обновить секретный ключ?",
|
||||
"minimizeAppOnCopy": "Свернуть приложение при копировании",
|
||||
"editCodeAuthMessage": "Аутентификация для редактирования кода",
|
||||
"deleteCodeAuthMessage": "Аутентификация для удаления кода",
|
||||
"showQRAuthMessage": "Аутентификация для отображения QR-кода"
|
||||
"showQRAuthMessage": "Аутентификация для отображения QR-кода",
|
||||
"confirmAccountDeleteTitle": "Подтвердить удаление аккаунта",
|
||||
"confirmAccountDeleteMessage": "Эта учетная запись связана с другими приложениями ente, если вы ими пользуетесь.\n\nЗагруженные вами данные во всех приложениях ente будут запланированы к удалению, а ваша учетная запись будет удалена без возможности восстановления.",
|
||||
"androidBiometricHint": "Подтвердите личность",
|
||||
"@androidBiometricHint": {
|
||||
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidBiometricNotRecognized": "Не распознано. Попробуйте еще раз.",
|
||||
"@androidBiometricNotRecognized": {
|
||||
"description": "Message to let the user know that authentication was failed. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidBiometricSuccess": "Успех",
|
||||
"@androidBiometricSuccess": {
|
||||
"description": "Message to let the user know that authentication was successful. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidCancelButton": "Отменить",
|
||||
"@androidCancelButton": {
|
||||
"description": "Message showed on a button that the user can click to leave the current dialog. It is used on Android side. Maximum 30 characters."
|
||||
},
|
||||
"androidSignInTitle": "Требуется аутентификация",
|
||||
"@androidSignInTitle": {
|
||||
"description": "Message showed as a title in a dialog which indicates the user that they need to scan biometric to continue. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidBiometricRequiredTitle": "Требуется биометрия",
|
||||
"@androidBiometricRequiredTitle": {
|
||||
"description": "Message showed as a title in a dialog which indicates the user has not set up biometric authentication on their device. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidDeviceCredentialsRequiredTitle": "Требуются учетные данные устройства",
|
||||
"@androidDeviceCredentialsRequiredTitle": {
|
||||
"description": "Message showed as a title in a dialog which indicates the user has not set up credentials authentication on their device. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidDeviceCredentialsSetupDescription": "Требуются учетные данные устройства",
|
||||
"@androidDeviceCredentialsSetupDescription": {
|
||||
"description": "Message advising the user to go to the settings and configure device credentials on their device. It shows in a dialog on Android side."
|
||||
},
|
||||
"goToSettings": "Перейдите к настройкам",
|
||||
"@goToSettings": {
|
||||
"description": "Message showed on a button that the user can click to go to settings pages from the current dialog. It is used on both Android and iOS side. Maximum 30 characters."
|
||||
},
|
||||
"androidGoToSettingsDescription": "Биометрическая аутентификация не настроена на вашем устройстве. Перейдите в \"Настройки > Безопасность\", чтобы добавить биометрическую аутентификацию.",
|
||||
"@androidGoToSettingsDescription": {
|
||||
"description": "Message advising the user to go to the settings and configure biometric on their device. It shows in a dialog on Android side."
|
||||
},
|
||||
"iOSLockOut": "Биометрическая аутентификация отключена. Пожалуйста, заблокируйте и разблокируйте экран, чтобы включить ее.",
|
||||
"@iOSLockOut": {
|
||||
"description": "Message advising the user to re-enable biometrics on their device. It shows in a dialog on iOS side."
|
||||
},
|
||||
"iOSGoToSettingsDescription": "Биометрическая аутентификация не настроена на вашем устройстве. Пожалуйста, включите Touch ID или Face ID на вашем телефоне.",
|
||||
"@iOSGoToSettingsDescription": {
|
||||
"description": "Message advising the user to go to the settings and configure Biometrics for their device. It shows in a dialog on iOS side."
|
||||
},
|
||||
"iOSOkButton": "ОК",
|
||||
"@iOSOkButton": {
|
||||
"description": "Message showed on a button that the user can click to leave the current dialog. It is used on iOS side. Maximum 30 characters."
|
||||
},
|
||||
"noInternetConnection": "Нет подключения к Интернету",
|
||||
"pleaseCheckYourInternetConnectionAndTryAgain": "Проверьте подключение к Интернету и повторите попытку."
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"account": "Hesabım",
|
||||
"unlock": "Kilidi aç",
|
||||
"recoveryKey": "Kurtarma Anahtarı",
|
||||
"counterAppBarTitle": "Sayaç",
|
||||
"@counterAppBarTitle": {
|
||||
@@ -83,6 +84,7 @@
|
||||
"importSelectJsonFile": "JSON dosyasını seçin",
|
||||
"importEnteEncGuide": "Ente'den dışa aktarılan şifrelenmiş JSON dosyasını seçin",
|
||||
"importRaivoGuide": "Raivo'nun ayarlarında \"OTP'leri Zip arşivine aktar\" seçeneğini kullanın.\n\nZip dosyasını çıkarın ve JSON dosyasını içe aktarın.",
|
||||
"importBitwardenGuide": "Bitwarden Tools içindeki \"Kasayı dışa aktar\" seçeneğini kullanın ve şifrelenmemiş JSON dosyasını içe aktarın.",
|
||||
"importAegisGuide": "Aegis'in Ayarlarında \"Kasayı dışa aktar\" seçeneğini kullanın.\n\nKasanız şifrelenmişse, kasanın şifresini çözmek için kasa parolasını girmeniz gerekecektir.",
|
||||
"exportCodes": "Kodu dışa aktar",
|
||||
"importLabel": "İçe aktar",
|
||||
@@ -90,16 +92,19 @@
|
||||
"importCodeDelimiterInfo": "Kodlar, virgülle ya da yeni bir satırla ayrılabilir",
|
||||
"selectFile": "Dosya seç",
|
||||
"emailVerificationToggle": "E-posta doğrulama",
|
||||
"emailVerificationEnableWarning": "E-postanız için 2FA'yı bizde saklıyorsanız, e-posta doğrulamasını açmak bir kilitlenmeye neden olabilir. Bir hizmetin dışında kalırsanız, diğerine giriş yapamayabilirsiniz.",
|
||||
"authToChangeEmailVerificationSetting": "E-posta doğrulamasını değiştirmek için lütfen kimlik doğrulaması yapın",
|
||||
"authToViewYourRecoveryKey": "Kurtarma anahtarınızı görmek için lütfen kimliğinizi doğrulayın",
|
||||
"authToChangeYourEmail": "Epostanızı değiştirmek için lütfen kimliğinizi doğrulayın",
|
||||
"authToChangeYourPassword": "Şifrenizi değiştirmek için lütfen kimliğinizi doğrulayın",
|
||||
"authToInitiateSignIn": "Yedekleme için giriş yapmayı başlatmak üzere lütfen kimlik doğrulaması yapın.",
|
||||
"ok": "Tamam",
|
||||
"cancel": "İptal Et",
|
||||
"yes": "Evet",
|
||||
"no": "Hayır",
|
||||
"email": "E-Posta",
|
||||
"support": "Destek",
|
||||
"general": "Genel",
|
||||
"settings": "Ayarlar",
|
||||
"copied": "Kopyalandı",
|
||||
"pleaseTryAgain": "Lütfen tekrar deneyin",
|
||||
@@ -179,6 +184,7 @@
|
||||
"enterDetailsManually": "Bilgileri elle girin",
|
||||
"edit": "Düzenle",
|
||||
"copiedToClipboard": "Panoya kopyalandı",
|
||||
"copiedNextToClipboard": "Sonraki kod panoya kopyalandı",
|
||||
"error": "Hata",
|
||||
"recoveryKeyCopiedToClipboard": "Kurtarma anahtarı panoya kopyalandı",
|
||||
"recoveryKeyOnForgotPassword": "Eğer şifrenizi unutursanız, verilerinizi kurtarabileceğiniz tek yol bu anahtardır.",
|
||||
@@ -249,6 +255,10 @@
|
||||
"privacy": "Gizlilik",
|
||||
"terms": "Şartlar",
|
||||
"checkForUpdates": "Güncellemeleri denetleyin",
|
||||
"downloadUpdate": "İndir",
|
||||
"criticalUpdateAvailable": "Kritik güncelleme mevcut",
|
||||
"updateAvailable": "Güncelleme mevcut",
|
||||
"update": "Güncelle",
|
||||
"checking": "Denetleniyor...",
|
||||
"youAreOnTheLatestVersion": "En son sürümdesiniz",
|
||||
"warning": "Uyarı",
|
||||
@@ -313,7 +323,69 @@
|
||||
"plainText": "Düz metin",
|
||||
"passwordToEncryptExport": "Dışa aktarımı şifrelemek için parola",
|
||||
"export": "Dışa aktar",
|
||||
"useOffline": "Yedekleme olmadan kullan",
|
||||
"signInToBackup": "Kodlarınızı yedeklemek için giriş yapın",
|
||||
"singIn": "Giriş yap",
|
||||
"sigInBackupReminder": "Geri yükleyebileceğiniz bir yedeğiniz olduğundan emin olmak için lütfen kodlarınızı dışa aktarın.",
|
||||
"offlineModeWarning": "Yedekleme yapmadan devam etmeyi seçtiniz. Kodlarınızın güvende olduğundan emin olmak için lütfen manuel yedekleme yapın.",
|
||||
"showLargeIcons": "Büyük simgeler göster",
|
||||
"shouldHideCode": "Kodları gizle",
|
||||
"doubleTapToViewHiddenCode": "Kodu görüntülemek için bir girdiye çift dokunabilirsiniz",
|
||||
"focusOnSearchBar": "Uygulama başladığında arama bölümüne odaklan",
|
||||
"minimizeAppOnCopy": "Kopyalarken uygulamayı küçült",
|
||||
"editCodeAuthMessage": "Kodu düzenlemek için doğrulama yapın",
|
||||
"deleteCodeAuthMessage": "Kodu silmek için doğrulama yapın",
|
||||
"showQRAuthMessage": "QR kodunu göstermek için doğrulama yapın"
|
||||
"showQRAuthMessage": "QR kodunu göstermek için doğrulama yapın",
|
||||
"confirmAccountDeleteTitle": "Hesap silme işlemini onayla",
|
||||
"confirmAccountDeleteMessage": "Bu hesap, eğer kullanıyorsanız, diğer ente uygulamalarıyla bağlantılıdır.\n\nTüm ente uygulamalarında yüklediğiniz veriler silinmek üzere programlanacak ve hesabınız kalıcı olarak silinecektir.",
|
||||
"androidBiometricHint": "Kimliği doğrula",
|
||||
"@androidBiometricHint": {
|
||||
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidBiometricNotRecognized": "Tanınmadı. Tekrar deneyin.",
|
||||
"@androidBiometricNotRecognized": {
|
||||
"description": "Message to let the user know that authentication was failed. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidBiometricSuccess": "Başarılı",
|
||||
"@androidBiometricSuccess": {
|
||||
"description": "Message to let the user know that authentication was successful. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidCancelButton": "İptal et",
|
||||
"@androidCancelButton": {
|
||||
"description": "Message showed on a button that the user can click to leave the current dialog. It is used on Android side. Maximum 30 characters."
|
||||
},
|
||||
"androidSignInTitle": "Kimlik doğrulaması gerekli",
|
||||
"@androidSignInTitle": {
|
||||
"description": "Message showed as a title in a dialog which indicates the user that they need to scan biometric to continue. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidBiometricRequiredTitle": "Biyometrik gerekli",
|
||||
"@androidBiometricRequiredTitle": {
|
||||
"description": "Message showed as a title in a dialog which indicates the user has not set up biometric authentication on their device. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidDeviceCredentialsRequiredTitle": "Cihaz kimlik bilgileri gerekli",
|
||||
"@androidDeviceCredentialsRequiredTitle": {
|
||||
"description": "Message showed as a title in a dialog which indicates the user has not set up credentials authentication on their device. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"goToSettings": "Ayarlara git",
|
||||
"@goToSettings": {
|
||||
"description": "Message showed on a button that the user can click to go to settings pages from the current dialog. It is used on both Android and iOS side. Maximum 30 characters."
|
||||
},
|
||||
"androidGoToSettingsDescription": "Biyometrik kimlik doğrulama cihazınızda ayarlanmamış. Biyometrik kimlik doğrulama eklemek için 'Ayarlar > Güvenlik' bölümüne gidin.",
|
||||
"@androidGoToSettingsDescription": {
|
||||
"description": "Message advising the user to go to the settings and configure biometric on their device. It shows in a dialog on Android side."
|
||||
},
|
||||
"iOSLockOut": "Biyometrik kimlik doğrulama devre dışı. Etkinleştirmek için lütfen ekranınızı kilitleyin ve kilidini açın.",
|
||||
"@iOSLockOut": {
|
||||
"description": "Message advising the user to re-enable biometrics on their device. It shows in a dialog on iOS side."
|
||||
},
|
||||
"iOSGoToSettingsDescription": "Cihazınızda biyometrik kimlik doğrulama ayarlanmamış. Lütfen telefonunuzda Touch ID veya Face ID'yi etkinleştirin.",
|
||||
"@iOSGoToSettingsDescription": {
|
||||
"description": "Message advising the user to go to the settings and configure Biometrics for their device. It shows in a dialog on iOS side."
|
||||
},
|
||||
"iOSOkButton": "Tamam",
|
||||
"@iOSOkButton": {
|
||||
"description": "Message showed on a button that the user can click to leave the current dialog. It is used on iOS side. Maximum 30 characters."
|
||||
},
|
||||
"noInternetConnection": "İnternet bağlantısı yok",
|
||||
"pleaseCheckYourInternetConnectionAndTryAgain": "Lütfen internet bağlantınızı kontrol edin ve yeniden deneyin."
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"account": "账户",
|
||||
"unlock": "解锁",
|
||||
"recoveryKey": "恢复密钥",
|
||||
"counterAppBarTitle": "计数器",
|
||||
"@counterAppBarTitle": {
|
||||
@@ -85,6 +86,7 @@
|
||||
"importSelectJsonFile": "选择 JSON 文件",
|
||||
"importEnteEncGuide": "选择从ente导出的加密JSON文件",
|
||||
"importRaivoGuide": "使用 Raivo 设置中的“将 OTP 导出到 Zip 存档”选项。\n\n解压 zip 文件并导入 JSON 文件。",
|
||||
"importBitwardenGuide": "使用 Bitwarden 工具中的“导出保管库”选项并导入未加密的 JSON 文件。",
|
||||
"importAegisGuide": "在Aegis的设置中使用\"导出密码库\"选项。\n\n如果您的密码库已加密,您需要输入密码才能解密密码库。",
|
||||
"exportCodes": "导出代码",
|
||||
"importLabel": "导入",
|
||||
@@ -97,6 +99,8 @@
|
||||
"authToViewYourRecoveryKey": "请验证以查看您的恢复密钥",
|
||||
"authToChangeYourEmail": "请验证以更改您的电子邮件",
|
||||
"authToChangeYourPassword": "请验证以更改密码",
|
||||
"authToViewSecrets": "请进行身份验证以查看您的秘密",
|
||||
"authToInitiateSignIn": "请进行身份验证以启动登录进行备份。",
|
||||
"ok": "好的",
|
||||
"cancel": "取消",
|
||||
"yes": "是",
|
||||
@@ -329,6 +333,7 @@
|
||||
"offlineModeWarning": "您已选择在不进行备份的情况下继续操作。请手动备份以确保您的代码安全。",
|
||||
"showLargeIcons": "显示大图标",
|
||||
"shouldHideCode": "隐藏代码",
|
||||
"doubleTapToViewHiddenCode": "您可以双击条目来查看代码",
|
||||
"focusOnSearchBar": "应用启动后聚焦搜索",
|
||||
"confirmUpdatingkey": "您确定要更新此密钥吗?",
|
||||
"minimizeAppOnCopy": "复制时最小化应用",
|
||||
@@ -336,5 +341,59 @@
|
||||
"deleteCodeAuthMessage": "删除代码需要身份验证",
|
||||
"showQRAuthMessage": "显示QR码需要身份验证",
|
||||
"confirmAccountDeleteTitle": "确认删除账户",
|
||||
"confirmAccountDeleteMessage": "该账户已链接到其他ente旗下的应用程序(如果您使用任何相关的应用程序)。\n\n您在所有ente旗下应用程序中上传的数据都将被安排删除,并且您的账户将被永久删除。"
|
||||
"confirmAccountDeleteMessage": "该账户已链接到其他ente旗下的应用程序(如果您使用任何相关的应用程序)。\n\n您在所有ente旗下应用程序中上传的数据都将被安排删除,并且您的账户将被永久删除。",
|
||||
"androidBiometricHint": "验证身份",
|
||||
"@androidBiometricHint": {
|
||||
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidBiometricNotRecognized": "未能识别。再试一次。",
|
||||
"@androidBiometricNotRecognized": {
|
||||
"description": "Message to let the user know that authentication was failed. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidBiometricSuccess": "成功",
|
||||
"@androidBiometricSuccess": {
|
||||
"description": "Message to let the user know that authentication was successful. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidCancelButton": "取消",
|
||||
"@androidCancelButton": {
|
||||
"description": "Message showed on a button that the user can click to leave the current dialog. It is used on Android side. Maximum 30 characters."
|
||||
},
|
||||
"androidSignInTitle": "需要进行身份验证",
|
||||
"@androidSignInTitle": {
|
||||
"description": "Message showed as a title in a dialog which indicates the user that they need to scan biometric to continue. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidBiometricRequiredTitle": "需要进行生物识别认证",
|
||||
"@androidBiometricRequiredTitle": {
|
||||
"description": "Message showed as a title in a dialog which indicates the user has not set up biometric authentication on their device. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidDeviceCredentialsRequiredTitle": "需要设备凭据",
|
||||
"@androidDeviceCredentialsRequiredTitle": {
|
||||
"description": "Message showed as a title in a dialog which indicates the user has not set up credentials authentication on their device. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidDeviceCredentialsSetupDescription": "需要设备凭据",
|
||||
"@androidDeviceCredentialsSetupDescription": {
|
||||
"description": "Message advising the user to go to the settings and configure device credentials on their device. It shows in a dialog on Android side."
|
||||
},
|
||||
"goToSettings": "前往设置",
|
||||
"@goToSettings": {
|
||||
"description": "Message showed on a button that the user can click to go to settings pages from the current dialog. It is used on both Android and iOS side. Maximum 30 characters."
|
||||
},
|
||||
"androidGoToSettingsDescription": "您的设备上未设置生物识别身份验证。转到“设置 > 安全”以添加生物识别身份验证。",
|
||||
"@androidGoToSettingsDescription": {
|
||||
"description": "Message advising the user to go to the settings and configure biometric on their device. It shows in a dialog on Android side."
|
||||
},
|
||||
"iOSLockOut": "生物识别身份验证已禁用。请锁定再解锁您的屏幕以启用它。",
|
||||
"@iOSLockOut": {
|
||||
"description": "Message advising the user to re-enable biometrics on their device. It shows in a dialog on iOS side."
|
||||
},
|
||||
"iOSGoToSettingsDescription": "您的设备上未设置生物识别身份验证。请在您的手机上启用 触控 ID 或 面容 ID。",
|
||||
"@iOSGoToSettingsDescription": {
|
||||
"description": "Message advising the user to go to the settings and configure Biometrics for their device. It shows in a dialog on iOS side."
|
||||
},
|
||||
"iOSOkButton": "好的",
|
||||
"@iOSOkButton": {
|
||||
"description": "Message showed on a button that the user can click to leave the current dialog. It is used on iOS side. Maximum 30 characters."
|
||||
},
|
||||
"noInternetConnection": "无互联网连接",
|
||||
"pleaseCheckYourInternetConnectionAndTryAgain": "请检查您的互联网连接,然后重试。"
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/ui/tools/app_lock.dart';
|
||||
import 'package:ente_auth/utils/auth_util.dart';
|
||||
@@ -19,7 +17,7 @@ class LocalAuthenticationService {
|
||||
) async {
|
||||
if (await _isLocalAuthSupportedOnDevice()) {
|
||||
AppLock.of(context)!.setEnabled(false);
|
||||
final result = await requestAuthentication(infoMessage);
|
||||
final result = await requestAuthentication(context, infoMessage);
|
||||
AppLock.of(context)!.setEnabled(
|
||||
Configuration.instance.shouldShowLockScreen(),
|
||||
);
|
||||
@@ -43,6 +41,7 @@ class LocalAuthenticationService {
|
||||
if (await LocalAuthentication().isDeviceSupported()) {
|
||||
AppLock.of(context)!.disable();
|
||||
final result = await requestAuthentication(
|
||||
context,
|
||||
infoMessage,
|
||||
);
|
||||
if (result) {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/core/constants.dart';
|
||||
@@ -94,7 +92,7 @@ class UpdateService {
|
||||
// Note: in auth, currently we don't have a way to identify if the
|
||||
// app was installed from play store, f-droid or github based on pkg name
|
||||
if (Platform.isAndroid) {
|
||||
if(flavor == "playstore") {
|
||||
if (flavor == "playstore") {
|
||||
return const Tuple2(
|
||||
"Play Store",
|
||||
"market://details??id=io.ente.auth",
|
||||
@@ -120,8 +118,7 @@ class UpdateService {
|
||||
// Fall back if we fail to open play-store market app on android
|
||||
if (Platform.isAndroid && url.startsWith("market://")) {
|
||||
launchUrlString(
|
||||
"https://play.google.com/store/apps/details?id=io"
|
||||
".ente.auth",
|
||||
"https://play.google.com/store/apps/details?id=io.ente.auth",
|
||||
mode: LaunchMode.externalApplication,
|
||||
).ignore();
|
||||
}
|
||||
@@ -129,7 +126,8 @@ class UpdateService {
|
||||
}
|
||||
|
||||
bool isIndependent() {
|
||||
return flavor == "independent" || _packageInfo.packageName.endsWith("independent");
|
||||
return flavor == "independent" ||
|
||||
_packageInfo.packageName.endsWith("independent");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,13 +24,12 @@ import 'package:ente_auth/ui/account/ott_verification_page.dart';
|
||||
import 'package:ente_auth/ui/account/password_entry_page.dart';
|
||||
import 'package:ente_auth/ui/account/password_reentry_page.dart';
|
||||
import 'package:ente_auth/ui/account/recovery_page.dart';
|
||||
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
|
||||
import 'package:ente_auth/ui/common/progress_dialog.dart';
|
||||
import 'package:ente_auth/ui/home_page.dart';
|
||||
import 'package:ente_auth/ui/two_factor_authentication_page.dart';
|
||||
import 'package:ente_auth/ui/two_factor_recovery_page.dart';
|
||||
import 'package:ente_auth/utils/crypto_util.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/email_util.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import "package:flutter/foundation.dart";
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -48,7 +47,7 @@ class UserService {
|
||||
static const keyUserDetails = "user_details";
|
||||
static const kCanDisableEmailMFA = "can_disable_email_mfa";
|
||||
static const kIsEmailMFAEnabled = "is_email_mfa_enabled";
|
||||
final SRP6GroupParameters kDefaultSrpGroup = SRP6StandardGroups.rfc5054_4096;
|
||||
final SRP6GroupParameters kDefaultSrpGroup = SRP6StandardGroups.rfc5054_4096;
|
||||
final _dio = Network.instance.getDio();
|
||||
final _enteDio = Network.instance.enteDio;
|
||||
final _logger = Logger((UserService).toString());
|
||||
@@ -68,12 +67,12 @@ class UserService {
|
||||
}
|
||||
|
||||
Future<void> sendOtt(
|
||||
BuildContext context,
|
||||
String email, {
|
||||
bool isChangeEmail = false,
|
||||
bool isCreateAccountScreen = false,
|
||||
bool isResetPasswordScreen = false,
|
||||
}) async {
|
||||
BuildContext context,
|
||||
String email, {
|
||||
bool isChangeEmail = false,
|
||||
bool isCreateAccountScreen = false,
|
||||
bool isResetPasswordScreen = false,
|
||||
}) async {
|
||||
final dialog = createProgressDialog(context, context.l10n.pleaseWait);
|
||||
await dialog.show();
|
||||
try {
|
||||
@@ -122,17 +121,16 @@ class UserService {
|
||||
}
|
||||
|
||||
Future<void> sendFeedback(
|
||||
BuildContext context,
|
||||
String feedback, {
|
||||
String type = "SubCancellation",
|
||||
}) async {
|
||||
BuildContext context,
|
||||
String feedback, {
|
||||
String type = "SubCancellation",
|
||||
}) async {
|
||||
await _dio.post(
|
||||
_config.getHttpEndpoint() + "/anonymous/feedback",
|
||||
data: {"feedback": feedback, "type": "type"},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Future<UserDetails> getUserDetailsV2({
|
||||
bool memoryCount = false,
|
||||
bool shouldCache = true,
|
||||
@@ -146,9 +144,15 @@ class UserService {
|
||||
);
|
||||
final userDetails = UserDetails.fromMap(response.data);
|
||||
if (shouldCache) {
|
||||
if(userDetails.profileData != null) {
|
||||
_preferences.setBool(kIsEmailMFAEnabled, userDetails.profileData!.isEmailMFAEnabled);
|
||||
_preferences.setBool(kCanDisableEmailMFA, userDetails.profileData!.canDisableEmailMFA);
|
||||
if (userDetails.profileData != null) {
|
||||
_preferences.setBool(
|
||||
kIsEmailMFAEnabled,
|
||||
userDetails.profileData!.isEmailMFAEnabled,
|
||||
);
|
||||
_preferences.setBool(
|
||||
kCanDisableEmailMFA,
|
||||
userDetails.profileData!.canDisableEmailMFA,
|
||||
);
|
||||
}
|
||||
// handle email change from different client
|
||||
if (userDetails.email != _config.getEmail()) {
|
||||
@@ -156,7 +160,7 @@ class UserService {
|
||||
}
|
||||
}
|
||||
return userDetails;
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
_logger.warning("Failed to fetch", e);
|
||||
rethrow;
|
||||
}
|
||||
@@ -210,15 +214,15 @@ class UserService {
|
||||
//to close and only then to show the error dialog.
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 150),
|
||||
() => showGenericErrorDialog(context: context),
|
||||
() => showGenericErrorDialog(context: context),
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<DeleteChallengeResponse?> getDeleteChallenge(
|
||||
BuildContext context,
|
||||
) async {
|
||||
BuildContext context,
|
||||
) async {
|
||||
try {
|
||||
final response = await _enteDio.get("/users/delete-challenge");
|
||||
if (response.statusCode == 200) {
|
||||
@@ -237,8 +241,9 @@ class UserService {
|
||||
}
|
||||
|
||||
Future<void> deleteAccount(
|
||||
BuildContext context,
|
||||
String challengeResponse,) async {
|
||||
BuildContext context,
|
||||
String challengeResponse,
|
||||
) async {
|
||||
try {
|
||||
final response = await _enteDio.delete(
|
||||
"/users/delete",
|
||||
@@ -258,9 +263,11 @@ class UserService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> verifyEmail(BuildContext context, String ott, {bool
|
||||
isResettingPasswordScreen = false,})
|
||||
async {
|
||||
Future<void> verifyEmail(
|
||||
BuildContext context,
|
||||
String ott, {
|
||||
bool isResettingPasswordScreen = false,
|
||||
}) async {
|
||||
final dialog = createProgressDialog(context, context.l10n.pleaseWait);
|
||||
await dialog.show();
|
||||
try {
|
||||
@@ -280,14 +287,15 @@ class UserService {
|
||||
} else {
|
||||
await _saveConfiguration(response);
|
||||
if (Configuration.instance.getEncryptedToken() != null) {
|
||||
if(isResettingPasswordScreen) {
|
||||
if (isResettingPasswordScreen) {
|
||||
page = const RecoveryPage();
|
||||
} else {
|
||||
page = const PasswordReentryPage();
|
||||
}
|
||||
|
||||
} else {
|
||||
page = const PasswordEntryPage(mode: PasswordEntryMode.set,);
|
||||
page = const PasswordEntryPage(
|
||||
mode: PasswordEntryMode.set,
|
||||
);
|
||||
}
|
||||
}
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
@@ -296,7 +304,7 @@ class UserService {
|
||||
return page;
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
} else {
|
||||
// should never reach here
|
||||
@@ -336,10 +344,10 @@ class UserService {
|
||||
}
|
||||
|
||||
Future<void> changeEmail(
|
||||
BuildContext context,
|
||||
String email,
|
||||
String ott,
|
||||
) async {
|
||||
BuildContext context,
|
||||
String email,
|
||||
String ott,
|
||||
) async {
|
||||
final dialog = createProgressDialog(context, context.l10n.pleaseWait);
|
||||
await dialog.show();
|
||||
try {
|
||||
@@ -431,9 +439,9 @@ class UserService {
|
||||
}
|
||||
|
||||
Future<void> registerOrUpdateSrp(
|
||||
Uint8List loginKey, {
|
||||
SetKeysRequest? setKeysRequest,
|
||||
}) async {
|
||||
Uint8List loginKey, {
|
||||
SetKeysRequest? setKeysRequest,
|
||||
}) async {
|
||||
try {
|
||||
final String username = const Uuid().v4().toString();
|
||||
final SecureRandom random = _getSecureRandom();
|
||||
@@ -466,14 +474,14 @@ class UserService {
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final SetupSRPResponse setupSRPResponse =
|
||||
SetupSRPResponse.fromJson(response.data);
|
||||
SetupSRPResponse.fromJson(response.data);
|
||||
final serverB =
|
||||
SRP6Util.decodeBigInt(base64Decode(setupSRPResponse.srpB));
|
||||
SRP6Util.decodeBigInt(base64Decode(setupSRPResponse.srpB));
|
||||
// ignore: need to calculate secret to get M1, unused_local_variable
|
||||
final clientS = client.calculateSecret(serverB);
|
||||
final clientM = client.calculateClientEvidenceMessage();
|
||||
late Response srpCompleteResponse;
|
||||
if(setKeysRequest == null) {
|
||||
if (setKeysRequest == null) {
|
||||
srpCompleteResponse = await _enteDio.post(
|
||||
"/users/srp/complete",
|
||||
data: {
|
||||
@@ -494,8 +502,8 @@ class UserService {
|
||||
} else {
|
||||
throw Exception("register-srp action failed");
|
||||
}
|
||||
} catch (e,s) {
|
||||
_logger.severe("failed to register srp" ,e,s);
|
||||
} catch (e, s) {
|
||||
_logger.severe("failed to register srp", e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -512,133 +520,96 @@ class UserService {
|
||||
}
|
||||
|
||||
Future<void> verifyEmailViaPassword(
|
||||
BuildContext context,
|
||||
SrpAttributes srpAttributes,
|
||||
String userPassword,
|
||||
) async {
|
||||
final dialog = createProgressDialog(
|
||||
context,
|
||||
context.l10n.pleaseWait,
|
||||
isDismissible: true,
|
||||
);
|
||||
await dialog.show();
|
||||
BuildContext context,
|
||||
SrpAttributes srpAttributes,
|
||||
String userPassword,
|
||||
ProgressDialog dialog,
|
||||
) async {
|
||||
late Uint8List keyEncryptionKey;
|
||||
try {
|
||||
keyEncryptionKey = await CryptoUtil.deriveKey(
|
||||
utf8.encode(userPassword) as Uint8List,
|
||||
CryptoUtil.base642bin(srpAttributes.kekSalt),
|
||||
srpAttributes.memLimit,
|
||||
srpAttributes.opsLimit,
|
||||
);
|
||||
final loginKey = await CryptoUtil.deriveLoginKey(keyEncryptionKey);
|
||||
final Uint8List identity = Uint8List.fromList(
|
||||
utf8.encode(srpAttributes.srpUserID),
|
||||
);
|
||||
final Uint8List salt = base64Decode(srpAttributes.srpSalt);
|
||||
final Uint8List password = loginKey;
|
||||
final SecureRandom random = _getSecureRandom();
|
||||
_logger.finest('Start deriving key');
|
||||
keyEncryptionKey = await CryptoUtil.deriveKey(
|
||||
utf8.encode(userPassword) as Uint8List,
|
||||
CryptoUtil.base642bin(srpAttributes.kekSalt),
|
||||
srpAttributes.memLimit,
|
||||
srpAttributes.opsLimit,
|
||||
);
|
||||
_logger.finest('keyDerivation done, derive LoginKey');
|
||||
final loginKey = await CryptoUtil.deriveLoginKey(keyEncryptionKey);
|
||||
final Uint8List identity = Uint8List.fromList(
|
||||
utf8.encode(srpAttributes.srpUserID),
|
||||
);
|
||||
_logger.finest('longinKey derivation done');
|
||||
final Uint8List salt = base64Decode(srpAttributes.srpSalt);
|
||||
final Uint8List password = loginKey;
|
||||
final SecureRandom random = _getSecureRandom();
|
||||
|
||||
final client = SRP6Client(
|
||||
group: kDefaultSrpGroup,
|
||||
digest: Digest('SHA-256'),
|
||||
random: random,
|
||||
);
|
||||
final client = SRP6Client(
|
||||
group: kDefaultSrpGroup,
|
||||
digest: Digest('SHA-256'),
|
||||
random: random,
|
||||
);
|
||||
|
||||
final A = client.generateClientCredentials(salt, identity, password);
|
||||
final createSessionResponse = await _dio.post(
|
||||
_config.getHttpEndpoint() + "/users/srp/create-session",
|
||||
data: {
|
||||
"srpUserID": srpAttributes.srpUserID,
|
||||
"srpA": base64Encode(SRP6Util.encodeBigInt(A!)),
|
||||
},
|
||||
);
|
||||
final String sessionID = createSessionResponse.data["sessionID"];
|
||||
final String srpB = createSessionResponse.data["srpB"];
|
||||
final A = client.generateClientCredentials(salt, identity, password);
|
||||
final createSessionResponse = await _dio.post(
|
||||
_config.getHttpEndpoint() + "/users/srp/create-session",
|
||||
data: {
|
||||
"srpUserID": srpAttributes.srpUserID,
|
||||
"srpA": base64Encode(SRP6Util.encodeBigInt(A!)),
|
||||
},
|
||||
);
|
||||
final String sessionID = createSessionResponse.data["sessionID"];
|
||||
final String srpB = createSessionResponse.data["srpB"];
|
||||
|
||||
final serverB = SRP6Util.decodeBigInt(base64Decode(srpB));
|
||||
// ignore: need to calculate secret to get M1, unused_local_variable
|
||||
final clientS = client.calculateSecret(serverB);
|
||||
final clientM = client.calculateClientEvidenceMessage();
|
||||
final response = await _dio.post(
|
||||
_config.getHttpEndpoint() + "/users/srp/verify-session",
|
||||
data: {
|
||||
"sessionID": sessionID,
|
||||
"srpUserID": srpAttributes.srpUserID,
|
||||
"srpM1": base64Encode(SRP6Util.encodeBigInt(clientM!)),
|
||||
},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
Widget page;
|
||||
final String twoFASessionID = response.data["twoFactorSessionID"];
|
||||
Configuration.instance.setVolatilePassword(userPassword);
|
||||
if (twoFASessionID.isNotEmpty) {
|
||||
page = TwoFactorAuthenticationPage(twoFASessionID);
|
||||
} else {
|
||||
await _saveConfiguration(response);
|
||||
if (Configuration.instance.getEncryptedToken() != null) {
|
||||
await Configuration.instance.decryptSecretsAndGetKeyEncKey(
|
||||
userPassword,
|
||||
Configuration.instance.getKeyAttributes()!,
|
||||
keyEncryptionKey: keyEncryptionKey,
|
||||
);
|
||||
page = const HomePage();
|
||||
} else {
|
||||
throw Exception("unexpected response during email verification");
|
||||
}
|
||||
}
|
||||
await dialog.hide();
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return page;
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
final serverB = SRP6Util.decodeBigInt(base64Decode(srpB));
|
||||
// ignore: need to calculate secret to get M1, unused_local_variable
|
||||
final clientS = client.calculateSecret(serverB);
|
||||
final clientM = client.calculateClientEvidenceMessage();
|
||||
final response = await _dio.post(
|
||||
_config.getHttpEndpoint() + "/users/srp/verify-session",
|
||||
data: {
|
||||
"sessionID": sessionID,
|
||||
"srpUserID": srpAttributes.srpUserID,
|
||||
"srpM1": base64Encode(SRP6Util.encodeBigInt(clientM!)),
|
||||
},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
Widget page;
|
||||
final String twoFASessionID = response.data["twoFactorSessionID"];
|
||||
Configuration.instance.setVolatilePassword(userPassword);
|
||||
if (twoFASessionID.isNotEmpty) {
|
||||
page = TwoFactorAuthenticationPage(twoFASessionID);
|
||||
} else {
|
||||
// should never reach here
|
||||
throw Exception("unexpected response during email verification");
|
||||
}
|
||||
} on DioError catch (e, s) {
|
||||
await dialog.hide();
|
||||
if (e.response != null && e.response!.statusCode == 401) {
|
||||
final dialogChoice = await showChoiceDialog(
|
||||
context,
|
||||
title: context.l10n.incorrectPasswordTitle,
|
||||
body: context.l10n.pleaseTryAgain,
|
||||
firstButtonLabel: context.l10n.contactSupport,
|
||||
secondButtonLabel: context.l10n.ok,
|
||||
);
|
||||
if (dialogChoice!.action == ButtonAction.first) {
|
||||
await sendLogs(
|
||||
context,
|
||||
context.l10n.contactSupport,
|
||||
"support@ente.io",
|
||||
postShare: () {},
|
||||
await _saveConfiguration(response);
|
||||
if (Configuration.instance.getEncryptedToken() != null) {
|
||||
await Configuration.instance.decryptSecretsAndGetKeyEncKey(
|
||||
userPassword,
|
||||
Configuration.instance.getKeyAttributes()!,
|
||||
keyEncryptionKey: keyEncryptionKey,
|
||||
);
|
||||
page = const HomePage();
|
||||
} else {
|
||||
throw Exception("unexpected response during email verification");
|
||||
}
|
||||
} else {
|
||||
_logger.fine('failed to verify password', e, s);
|
||||
await showErrorDialog(
|
||||
context,
|
||||
context.l10n.oops,
|
||||
context.l10n.verificationFailedPleaseTryAgain,
|
||||
);
|
||||
}
|
||||
} catch (e, s) {
|
||||
_logger.fine('failed to verify password', e, s);
|
||||
await dialog.hide();
|
||||
await showErrorDialog(
|
||||
context,
|
||||
context.l10n.oops,
|
||||
context.l10n.verificationFailedPleaseTryAgain,
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return page;
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
} else {
|
||||
// should never reach here
|
||||
throw Exception("unexpected response during email verification");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateKeyAttributes(KeyAttributes keyAttributes, Uint8List
|
||||
loginKey,)
|
||||
async {
|
||||
Future<void> updateKeyAttributes(
|
||||
KeyAttributes keyAttributes,
|
||||
Uint8List loginKey,
|
||||
) async {
|
||||
try {
|
||||
final setKeyRequest = SetKeysRequest(
|
||||
kekSalt: keyAttributes.kekSalt,
|
||||
@@ -679,10 +650,10 @@ class UserService {
|
||||
}
|
||||
|
||||
Future<void> verifyTwoFactor(
|
||||
BuildContext context,
|
||||
String sessionID,
|
||||
String code,
|
||||
) async {
|
||||
BuildContext context,
|
||||
String sessionID,
|
||||
String code,
|
||||
) async {
|
||||
final dialog = createProgressDialog(context, context.l10n.pleaseWait);
|
||||
await dialog.show();
|
||||
try {
|
||||
@@ -703,7 +674,7 @@ class UserService {
|
||||
return const PasswordReentryPage();
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
}
|
||||
} on DioError catch (e) {
|
||||
@@ -717,7 +688,7 @@ class UserService {
|
||||
return const LoginPage();
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
} else {
|
||||
showErrorDialog(
|
||||
@@ -758,7 +729,7 @@ class UserService {
|
||||
);
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
}
|
||||
} on DioError catch (e) {
|
||||
@@ -771,7 +742,7 @@ class UserService {
|
||||
return const LoginPage();
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
} else {
|
||||
showErrorDialog(
|
||||
@@ -793,12 +764,12 @@ class UserService {
|
||||
}
|
||||
|
||||
Future<void> removeTwoFactor(
|
||||
BuildContext context,
|
||||
String sessionID,
|
||||
String recoveryKey,
|
||||
String encryptedSecret,
|
||||
String secretDecryptionNonce,
|
||||
) async {
|
||||
BuildContext context,
|
||||
String sessionID,
|
||||
String recoveryKey,
|
||||
String encryptedSecret,
|
||||
String secretDecryptionNonce,
|
||||
) async {
|
||||
final dialog = createProgressDialog(context, context.l10n.pleaseWait);
|
||||
await dialog.show();
|
||||
String secret;
|
||||
@@ -847,7 +818,7 @@ class UserService {
|
||||
return const PasswordReentryPage();
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
}
|
||||
} on DioError catch (e) {
|
||||
@@ -860,7 +831,7 @@ class UserService {
|
||||
return const LoginPage();
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
} else {
|
||||
showErrorDialog(
|
||||
@@ -881,13 +852,6 @@ class UserService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Future<void> _saveConfiguration(Response response) async {
|
||||
await Configuration.instance.setUserID(response.data["id"]);
|
||||
if (response.data["encryptedToken"] != null) {
|
||||
@@ -904,6 +868,7 @@ class UserService {
|
||||
bool? canDisableEmailMFA() {
|
||||
return _preferences.getBool(kCanDisableEmailMFA);
|
||||
}
|
||||
|
||||
bool hasEmailMFAEnabled() {
|
||||
return _preferences.getBool(kIsEmailMFAEnabled) ?? true;
|
||||
}
|
||||
@@ -918,9 +883,8 @@ class UserService {
|
||||
);
|
||||
_preferences.setBool(kIsEmailMFAEnabled, isEnabled);
|
||||
} catch (e) {
|
||||
_logger.severe("Failed to update email mfa",e);
|
||||
_logger.severe("Failed to update email mfa", e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
|
||||
import "package:dio/dio.dart";
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import "package:ente_auth/core/errors.dart";
|
||||
import "package:ente_auth/l10n/l10n.dart";
|
||||
import "package:ente_auth/models/api/user/srp.dart";
|
||||
import "package:ente_auth/services/user_service.dart";
|
||||
import "package:ente_auth/theme/ente_theme.dart";
|
||||
import 'package:ente_auth/ui/common/dynamic_fab.dart';
|
||||
import "package:ente_auth/ui/components/buttons/button_widget.dart";
|
||||
import "package:ente_auth/utils/dialog_util.dart";
|
||||
import "package:ente_auth/utils/email_util.dart";
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:logging/logging.dart";
|
||||
|
||||
@@ -16,14 +19,16 @@ import "package:logging/logging.dart";
|
||||
// volatile password.
|
||||
class LoginPasswordVerificationPage extends StatefulWidget {
|
||||
final SrpAttributes srpAttributes;
|
||||
const LoginPasswordVerificationPage({Key? key, required this.srpAttributes}) : super(key: key);
|
||||
const LoginPasswordVerificationPage({Key? key, required this.srpAttributes})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<LoginPasswordVerificationPage> createState() => _LoginPasswordVerificationPageState();
|
||||
State<LoginPasswordVerificationPage> createState() =>
|
||||
_LoginPasswordVerificationPageState();
|
||||
}
|
||||
|
||||
class _LoginPasswordVerificationPageState extends
|
||||
State<LoginPasswordVerificationPage> {
|
||||
class _LoginPasswordVerificationPageState
|
||||
extends State<LoginPasswordVerificationPage> {
|
||||
final _logger = Logger((_LoginPasswordVerificationPageState).toString());
|
||||
final _passwordController = TextEditingController();
|
||||
final FocusNode _passwordFocusNode = FocusNode();
|
||||
@@ -74,9 +79,7 @@ State<LoginPasswordVerificationPage> {
|
||||
buttonText: context.l10n.logInLabel,
|
||||
onPressedFunction: () async {
|
||||
FocusScope.of(context).unfocus();
|
||||
await UserService.instance.verifyEmailViaPassword(context, widget
|
||||
.srpAttributes,
|
||||
_passwordController.text,);
|
||||
await verifyPassword(context, _passwordController.text);
|
||||
},
|
||||
),
|
||||
floatingActionButtonLocation: fabLocation(),
|
||||
@@ -84,6 +87,106 @@ State<LoginPasswordVerificationPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> verifyPassword(BuildContext context, String password) async {
|
||||
final dialog = createProgressDialog(
|
||||
context,
|
||||
context.l10n.pleaseWait,
|
||||
isDismissible: true,
|
||||
);
|
||||
await dialog.show();
|
||||
try {
|
||||
await UserService.instance.verifyEmailViaPassword(
|
||||
context,
|
||||
widget.srpAttributes,
|
||||
password,
|
||||
dialog,
|
||||
);
|
||||
} on DioError catch (e, s) {
|
||||
await dialog.hide();
|
||||
if (e.response != null && e.response!.statusCode == 401) {
|
||||
_logger.severe('server reject, failed verify SRP login', e, s);
|
||||
await _showContactSupportDialog(
|
||||
context,
|
||||
context.l10n.incorrectPasswordTitle,
|
||||
context.l10n.pleaseTryAgain,
|
||||
);
|
||||
} else {
|
||||
_logger.severe('API failure during SRP login', e, s);
|
||||
if (e.type == DioErrorType.other) {
|
||||
await _showContactSupportDialog(
|
||||
context,
|
||||
context.l10n.noInternetConnection,
|
||||
context.l10n.pleaseCheckYourInternetConnectionAndTryAgain,
|
||||
);
|
||||
} else {
|
||||
await _showContactSupportDialog(
|
||||
context,
|
||||
context.l10n.oops,
|
||||
context.l10n.verificationFailedPleaseTryAgain,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e, s) {
|
||||
_logger.info('error during loginViaPassword', e);
|
||||
await dialog.hide();
|
||||
if (e is LoginKeyDerivationError) {
|
||||
_logger.severe('loginKey derivation error', e, s);
|
||||
// LoginKey err, perform regular login via ott verification
|
||||
await UserService.instance.sendOtt(
|
||||
context,
|
||||
email!,
|
||||
isCreateAccountScreen: true,
|
||||
);
|
||||
return;
|
||||
} else if (e is KeyDerivationError) {
|
||||
// device is not powerful enough to perform derive key
|
||||
final dialogChoice = await showChoiceDialog(
|
||||
context,
|
||||
title: context.l10n.recreatePasswordTitle,
|
||||
body: context.l10n.recreatePasswordBody,
|
||||
firstButtonLabel: context.l10n.useRecoveryKey,
|
||||
);
|
||||
if (dialogChoice!.action == ButtonAction.first) {
|
||||
await UserService.instance.sendOtt(
|
||||
context,
|
||||
email!,
|
||||
isResetPasswordScreen: true,
|
||||
);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
_logger.severe('unexpected error while verifying password', e, s);
|
||||
await _showContactSupportDialog(
|
||||
context,
|
||||
context.l10n.oops,
|
||||
context.l10n.verificationFailedPleaseTryAgain,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showContactSupportDialog(
|
||||
BuildContext context,
|
||||
String title,
|
||||
String message,
|
||||
) async {
|
||||
final dialogChoice = await showChoiceDialog(
|
||||
context,
|
||||
title: title,
|
||||
body: message,
|
||||
firstButtonLabel: context.l10n.contactSupport,
|
||||
secondButtonLabel: context.l10n.ok,
|
||||
);
|
||||
if (dialogChoice!.action == ButtonAction.first) {
|
||||
await sendLogs(
|
||||
context,
|
||||
context.l10n.contactSupport,
|
||||
"auth@ente.io",
|
||||
postShare: () {},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _getBody() {
|
||||
return Column(
|
||||
children: [
|
||||
@@ -92,17 +195,22 @@ State<LoginPasswordVerificationPage> {
|
||||
child: ListView(
|
||||
children: [
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(top: 30, left: 20, right: 20),
|
||||
padding: const EdgeInsets.only(top: 30, left: 20, right: 20),
|
||||
child: Text(
|
||||
context.l10n.enterPassword,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 30, left: 22, right:
|
||||
20,),
|
||||
child: Text(email ?? '', style: getEnteTextTheme(context).smallMuted,),
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 30,
|
||||
left: 22,
|
||||
right: 20,
|
||||
),
|
||||
child: Text(
|
||||
email ?? '',
|
||||
style: getEnteTextTheme(context).smallMuted,
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
// hidden textForm for suggesting auto-fill service for saving
|
||||
@@ -133,19 +241,19 @@ State<LoginPasswordVerificationPage> {
|
||||
),
|
||||
suffixIcon: _passwordInFocus
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
_passwordVisible
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_passwordVisible = !_passwordVisible;
|
||||
});
|
||||
},
|
||||
)
|
||||
icon: Icon(
|
||||
_passwordVisible
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_passwordVisible = !_passwordVisible;
|
||||
});
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
style: const TextStyle(
|
||||
@@ -176,9 +284,11 @@ State<LoginPasswordVerificationPage> {
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () async {
|
||||
await UserService.instance
|
||||
.sendOtt(context, email!,
|
||||
isResetPasswordScreen: true,);
|
||||
await UserService.instance.sendOtt(
|
||||
context,
|
||||
email!,
|
||||
isResetPasswordScreen: true,
|
||||
);
|
||||
},
|
||||
child: Center(
|
||||
child: Text(
|
||||
@@ -187,9 +297,9 @@ State<LoginPasswordVerificationPage> {
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -213,9 +323,9 @@ State<LoginPasswordVerificationPage> {
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -229,4 +339,4 @@ State<LoginPasswordVerificationPage> {
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
209
lib/ui/settings/data/import/2fas_import.dart
Normal file
209
lib/ui/settings/data/import/2fas_import.dart
Normal file
@@ -0,0 +1,209 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/services/authenticator_service.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/ui/common/progress_dialog.dart';
|
||||
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
|
||||
import 'package:ente_auth/ui/components/dialog_widget.dart';
|
||||
import 'package:ente_auth/ui/components/models/button_type.dart';
|
||||
import 'package:ente_auth/ui/settings/data/import/import_success.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:pointycastle/export.dart';
|
||||
|
||||
Future<void> show2FasImportInstruction(BuildContext context) async {
|
||||
final l10n = context.l10n;
|
||||
final result = await showDialogWidget(
|
||||
context: context,
|
||||
title: l10n.importFromApp("2FAS Authenticator"),
|
||||
body: l10n.import2FasGuide,
|
||||
buttons: [
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.primary,
|
||||
labelText: l10n.importSelectAppExport("2FAS Authenticator"),
|
||||
isInAlert: true,
|
||||
buttonSize: ButtonSize.large,
|
||||
buttonAction: ButtonAction.first,
|
||||
),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.secondary,
|
||||
labelText: context.l10n.cancel,
|
||||
buttonSize: ButtonSize.large,
|
||||
isInAlert: true,
|
||||
buttonAction: ButtonAction.second,
|
||||
),
|
||||
],
|
||||
);
|
||||
if (result?.action != null && result!.action != ButtonAction.cancel) {
|
||||
if (result.action == ButtonAction.first) {
|
||||
await _pick2FasFile(context);
|
||||
} else {}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pick2FasFile(BuildContext context) async {
|
||||
final l10n = context.l10n;
|
||||
FilePickerResult? result = await FilePicker.platform
|
||||
.pickFiles(dialogTitle: l10n.importSelectJsonFile);
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
final ProgressDialog progressDialog =
|
||||
createProgressDialog(context, l10n.pleaseWait);
|
||||
await progressDialog.show();
|
||||
try {
|
||||
String path = result.files.single.path!;
|
||||
int? count = await _process2FasExportFile(context, path, progressDialog);
|
||||
await progressDialog.hide();
|
||||
if (count != null) {
|
||||
await importSuccessDialog(context, count);
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logger('2FASImport').severe('exception while processing import', e, s);
|
||||
await progressDialog.hide();
|
||||
await showErrorDialog(
|
||||
context,
|
||||
context.l10n.sorry,
|
||||
context.l10n.importFailureDesc,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<int?> _process2FasExportFile(
|
||||
BuildContext context,
|
||||
String path,
|
||||
final ProgressDialog dialog,
|
||||
) async {
|
||||
File file = File(path);
|
||||
|
||||
final jsonString = await file.readAsString();
|
||||
final decodedJson = jsonDecode(jsonString);
|
||||
int version = (decodedJson['schemaVersion'] ?? 0) as int;
|
||||
if (version != 3 && version != 4) {
|
||||
await dialog.hide();
|
||||
// todo: extract strings for l10n. Use same naming format as in aegis
|
||||
// to avoid duplicate translation efforts.
|
||||
await showErrorDialog(
|
||||
context,
|
||||
'Unsupported format: $version',
|
||||
version == 0
|
||||
? "The selected file is not a valid 2FAS Authenticator export."
|
||||
: "Sorry, the app doesn't support this version of 2FAS Authenticator export",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
var decodedServices = decodedJson['services'];
|
||||
// https://github.com/twofas/2fas-android/blob/e97f1a1040eafaed6d5284d54d33403dff215886/data/services/src/main/java/com/twofasapp/data/services/domain/BackupContent.kt#L39
|
||||
final isEncrypted = decodedJson['reference'] != null;
|
||||
if (isEncrypted) {
|
||||
String? password;
|
||||
try {
|
||||
await showTextInputDialog(
|
||||
context,
|
||||
title: "Enter password to decrypt 2FAS backup",
|
||||
submitButtonLabel: "Submit",
|
||||
isPasswordInput: true,
|
||||
onSubmit: (value) async {
|
||||
password = value;
|
||||
},
|
||||
);
|
||||
if (password == null) {
|
||||
await dialog.hide();
|
||||
return null;
|
||||
}
|
||||
final content = decrypt2FasVault(decodedJson, password: password!);
|
||||
decodedServices = jsonDecode(content);
|
||||
} catch (e, s) {
|
||||
Logger("2FASImport").warning("exception while decrypting backup", e, s);
|
||||
await dialog.hide();
|
||||
if (password != null) {
|
||||
await showErrorDialog(
|
||||
context,
|
||||
"Failed to decrypt 2Fas export",
|
||||
"Please check your password and try again.",
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
final parsedCodes = [];
|
||||
for (var item in decodedServices) {
|
||||
var kind = item['otp']['tokenType'];
|
||||
var account = item['otp']['account'] ?? '';
|
||||
var issuer = item['otp']['issuer'] ?? item['name'] ?? '';
|
||||
var algorithm = item['otp']['algorithm'];
|
||||
var secret = item['secret'];
|
||||
var timer = item['otp']['period'];
|
||||
var digits = item['otp']['digits'];
|
||||
var counter = item['otp']['counter'];
|
||||
|
||||
// Build the OTP URL
|
||||
String otpUrl;
|
||||
|
||||
if (kind.toLowerCase() == 'totp') {
|
||||
otpUrl =
|
||||
'otpauth://$kind/$issuer:$account?secret=$secret&issuer=$issuer&algorithm=$algorithm&digits=$digits&period=$timer';
|
||||
} else if (kind.toLowerCase() == 'hotp') {
|
||||
otpUrl =
|
||||
'otpauth://$kind/$issuer:$account?secret=$secret&issuer=$issuer&algorithm=$algorithm&digits=$digits&counter=$counter';
|
||||
} else {
|
||||
throw Exception('Invalid OTP type');
|
||||
}
|
||||
parsedCodes.add(Code.fromRawData(otpUrl));
|
||||
}
|
||||
|
||||
for (final code in parsedCodes) {
|
||||
await CodeStore.instance.addCode(code, shouldSync: false);
|
||||
}
|
||||
unawaited(AuthenticatorService.instance.onlineSync());
|
||||
int count = parsedCodes.length;
|
||||
return count;
|
||||
}
|
||||
|
||||
String decrypt2FasVault(dynamic data, {required String password}) {
|
||||
int ITERATION_COUNT = 10000;
|
||||
int KEY_SIZE = 256;
|
||||
final String encryptedServices = data["servicesEncrypted"];
|
||||
var split = encryptedServices.split(":");
|
||||
final encryptedData = base64.decode(split[0]);
|
||||
final salt = base64.decode(split[1]);
|
||||
final iv = base64.decode(split[2]);
|
||||
// derive 256 key using PBKDF2WithHmacSHA256 and 10000 iterations and above salt
|
||||
final pbkdf2 = PBKDF2KeyDerivator(HMac(SHA256Digest(), 64));
|
||||
final params = Pbkdf2Parameters(
|
||||
salt,
|
||||
ITERATION_COUNT,
|
||||
KEY_SIZE ~/ 8,
|
||||
);
|
||||
pbkdf2.init(params);
|
||||
Uint8List key = Uint8List(KEY_SIZE ~/ 8);
|
||||
pbkdf2.deriveKey(Uint8List.fromList(utf8.encode(password)), 0, key, 0);
|
||||
final decrypted = decrypt(key, iv, encryptedData);
|
||||
final utf8Decode = utf8.decode(decrypted);
|
||||
return utf8Decode;
|
||||
}
|
||||
|
||||
Uint8List decrypt(Uint8List key, Uint8List iv, Uint8List data) {
|
||||
final cipher = GCMBlockCipher(AESEngine())
|
||||
..init(
|
||||
false,
|
||||
AEADParameters(
|
||||
KeyParameter(key),
|
||||
128,
|
||||
iv,
|
||||
Uint8List.fromList(<int>[]),
|
||||
),
|
||||
);
|
||||
|
||||
final dbBytes = cipher.process(data);
|
||||
return dbBytes;
|
||||
}
|
||||
103
lib/ui/settings/data/import/bitwarden_import.dart
Normal file
103
lib/ui/settings/data/import/bitwarden_import.dart
Normal file
@@ -0,0 +1,103 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/services/authenticator_service.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
|
||||
import 'package:ente_auth/ui/components/dialog_widget.dart';
|
||||
import 'package:ente_auth/ui/components/models/button_type.dart';
|
||||
import 'package:ente_auth/ui/settings/data/import/import_success.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Future<void> showBitwardenImportInstruction(BuildContext context) async {
|
||||
final l10n = context.l10n;
|
||||
final result = await showDialogWidget(
|
||||
context: context,
|
||||
title: l10n.importFromApp("Bitwarden"),
|
||||
body: l10n.importBitwardenGuide,
|
||||
buttons: [
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.primary,
|
||||
labelText: l10n.importSelectJsonFile,
|
||||
isInAlert: true,
|
||||
buttonSize: ButtonSize.large,
|
||||
buttonAction: ButtonAction.first,
|
||||
),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.secondary,
|
||||
labelText: context.l10n.cancel,
|
||||
buttonSize: ButtonSize.large,
|
||||
isInAlert: true,
|
||||
buttonAction: ButtonAction.second,
|
||||
),
|
||||
],
|
||||
);
|
||||
if (result?.action != null && result!.action != ButtonAction.cancel) {
|
||||
if (result.action == ButtonAction.first) {
|
||||
await _pickBitwardenJsonFile(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pickBitwardenJsonFile(BuildContext context) async {
|
||||
final l10n = context.l10n;
|
||||
FilePickerResult? result = await FilePicker.platform.pickFiles();
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
final progressDialog = createProgressDialog(context, l10n.pleaseWait);
|
||||
await progressDialog.show();
|
||||
try {
|
||||
String path = result.files.single.path!;
|
||||
int? count = await _processBitwardenExportFile(context, path);
|
||||
await progressDialog.hide();
|
||||
if (count != null) {
|
||||
await importSuccessDialog(context, count);
|
||||
}
|
||||
} catch (e) {
|
||||
await progressDialog.hide();
|
||||
await showErrorDialog(
|
||||
context,
|
||||
context.l10n.sorry,
|
||||
context.l10n.importFailureDesc,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<int?> _processBitwardenExportFile(
|
||||
BuildContext context,
|
||||
String path,
|
||||
) async {
|
||||
File file = File(path);
|
||||
final jsonString = await file.readAsString();
|
||||
final data = jsonDecode(jsonString);
|
||||
List<dynamic> jsonArray = data['items'];
|
||||
final parsedCodes = [];
|
||||
for (var item in jsonArray) {
|
||||
if (item['login']['totp'] != null) {
|
||||
var issuer = item['name'];
|
||||
var account = item['login']['username'];
|
||||
var secret = item['login']['totp'];
|
||||
|
||||
parsedCodes.add(
|
||||
Code.fromAccountAndSecret(
|
||||
account,
|
||||
issuer,
|
||||
secret,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (final code in parsedCodes) {
|
||||
await CodeStore.instance.addCode(code, shouldSync: false);
|
||||
}
|
||||
unawaited(AuthenticatorService.instance.onlineSync());
|
||||
return parsedCodes.length;
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'package:ente_auth/ui/settings/data/import/2fas_import.dart';
|
||||
import 'package:ente_auth/ui/settings/data/import/aegis_import.dart';
|
||||
import 'package:ente_auth/ui/settings/data/import/bitwarden_import.dart';
|
||||
import 'package:ente_auth/ui/settings/data/import/encrypted_ente_import.dart';
|
||||
import 'package:ente_auth/ui/settings/data/import/google_auth_import.dart';
|
||||
import 'package:ente_auth/ui/settings/data/import/plain_text_import.dart';
|
||||
@@ -31,6 +33,12 @@ class ImportService {
|
||||
case ImportType.aegis:
|
||||
showAegisImportInstruction(context);
|
||||
break;
|
||||
case ImportType.twoFas:
|
||||
show2FasImportInstruction(context);
|
||||
break;
|
||||
case ImportType.bitwarden:
|
||||
showBitwardenImportInstruction(context);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,15 +15,19 @@ enum ImportType {
|
||||
ravio,
|
||||
googleAuthenticator,
|
||||
aegis,
|
||||
twoFas,
|
||||
bitwarden,
|
||||
}
|
||||
|
||||
class ImportCodePage extends StatelessWidget {
|
||||
late List<ImportType> importOptions = [
|
||||
ImportType.plainText,
|
||||
ImportType.encrypted,
|
||||
ImportType.ravio,
|
||||
ImportType.twoFas,
|
||||
ImportType.aegis,
|
||||
ImportType.bitwarden,
|
||||
ImportType.googleAuthenticator,
|
||||
ImportType.ravio,
|
||||
];
|
||||
|
||||
ImportCodePage({super.key});
|
||||
@@ -40,6 +44,10 @@ class ImportCodePage extends StatelessWidget {
|
||||
return 'Google Authenticator';
|
||||
case ImportType.aegis:
|
||||
return 'Aegis Authenticator';
|
||||
case ImportType.twoFas:
|
||||
return '2FAS Authenticator';
|
||||
case ImportType.bitwarden:
|
||||
return 'Bitwarden';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +70,7 @@ class ImportCodePage extends StatelessWidget {
|
||||
iconButtonType: IconButtonType.secondary,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
if(Navigator.canPop(context)) {
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:ente_auth/ui/components/toggle_switch_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/common_settings.dart';
|
||||
import 'package:ente_auth/ui/settings/language_picker.dart';
|
||||
import 'package:ente_auth/utils/navigation_util.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AdvancedSectionWidget extends StatefulWidget {
|
||||
@@ -86,6 +87,9 @@ class _AdvancedSectionWidgetState extends State<AdvancedSectionWidget> {
|
||||
await PreferenceService.instance.setHideCodes(
|
||||
!PreferenceService.instance.shouldHideCodes(),
|
||||
);
|
||||
if(PreferenceService.instance.shouldHideCodes()) {
|
||||
showToast(context, context.l10n.doubleTapToViewHiddenCode);
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
|
||||
@@ -3,7 +3,9 @@ import 'dart:io';
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/onboarding/view/onboarding_page.dart';
|
||||
import 'package:ente_auth/services/local_authentication_service.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/theme/colors.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
|
||||
@@ -99,6 +101,19 @@ class SettingsPage extends StatelessWidget {
|
||||
await handleExportClick(context);
|
||||
} else {
|
||||
if (result.action == ButtonAction.second) {
|
||||
bool hasCodes =
|
||||
(await CodeStore.instance.getAllCodes()).isNotEmpty;
|
||||
if (hasCodes) {
|
||||
final hasAuthenticated = await LocalAuthenticationService
|
||||
.instance
|
||||
.requestLocalAuthentication(
|
||||
context,
|
||||
context.l10n.authToInitiateSignIn,
|
||||
);
|
||||
if (!hasAuthenticated) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await routeToPage(
|
||||
context,
|
||||
const OnboardingPage(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/ui/common/gradient_button.dart';
|
||||
import 'package:ente_auth/ui/tools/app_lock.dart';
|
||||
import 'package:ente_auth/utils/auth_util.dart';
|
||||
@@ -13,13 +14,25 @@ class LockScreen extends StatefulWidget {
|
||||
State<LockScreen> createState() => _LockScreenState();
|
||||
}
|
||||
|
||||
class _LockScreenState extends State<LockScreen> {
|
||||
class _LockScreenState extends State<LockScreen> with WidgetsBindingObserver {
|
||||
final _logger = Logger("LockScreen");
|
||||
bool _isShowingLockScreen = false;
|
||||
bool _hasPlacedAppInBackground = false;
|
||||
bool _hasAuthenticationFailed = false;
|
||||
int? lastAuthenticatingTime;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_showLockScreen();
|
||||
_logger.info("initiatingState");
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
if (isNonMobileIOSDevice()) {
|
||||
_logger.info('ignore init for non mobile iOS device');
|
||||
return;
|
||||
}
|
||||
_showLockScreen(source: "postFrameInit");
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -34,16 +47,16 @@ class _LockScreenState extends State<LockScreen> {
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Opacity(
|
||||
opacity: 0.3,
|
||||
opacity: 0.2,
|
||||
child: Image.asset('assets/loading_photos_background.png'),
|
||||
),
|
||||
SizedBox(
|
||||
width: 142,
|
||||
width: 180,
|
||||
child: GradientButton(
|
||||
text: "Unlock",
|
||||
text: context.l10n.unlock,
|
||||
iconData: Icons.lock_open_outlined,
|
||||
onTap: () async {
|
||||
_showLockScreen();
|
||||
_showLockScreen(source: "tapUnlock");
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -55,16 +68,76 @@ class _LockScreenState extends State<LockScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showLockScreen() async {
|
||||
_logger.info("Showing lockscreen");
|
||||
bool isNonMobileIOSDevice() {
|
||||
if (Platform.isAndroid) {
|
||||
return false;
|
||||
}
|
||||
var shortestSide = MediaQuery.of(context).size.shortestSide;
|
||||
return shortestSide > 600 ? true : false;
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
_logger.info(state.toString());
|
||||
if (state == AppLifecycleState.resumed && !_isShowingLockScreen) {
|
||||
// This is triggered either when the lock screen is dismissed or when
|
||||
// the app is brought to foreground
|
||||
_hasPlacedAppInBackground = false;
|
||||
final bool didAuthInLast5Seconds = lastAuthenticatingTime != null &&
|
||||
DateTime.now().millisecondsSinceEpoch - lastAuthenticatingTime! <
|
||||
5000;
|
||||
if (!_hasAuthenticationFailed && !didAuthInLast5Seconds) {
|
||||
// Show the lock screen again only if the app is resuming from the
|
||||
// background, and not when the lock screen was explicitly dismissed
|
||||
Future.delayed(
|
||||
Duration.zero,
|
||||
() => _showLockScreen(source: "lifeCycle"),
|
||||
);
|
||||
} else {
|
||||
_hasAuthenticationFailed = false; // Reset failure state
|
||||
}
|
||||
} else if (state == AppLifecycleState.paused ||
|
||||
state == AppLifecycleState.inactive) {
|
||||
// This is triggered either when the lock screen pops up or when
|
||||
// the app is pushed to background
|
||||
if (!_isShowingLockScreen) {
|
||||
_hasPlacedAppInBackground = true;
|
||||
_hasAuthenticationFailed = false; // reset failure state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_logger.info('disposing');
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _showLockScreen({String source = ''}) async {
|
||||
final int id = DateTime.now().millisecondsSinceEpoch;
|
||||
_logger.info("Showing lock screen $source $id");
|
||||
try {
|
||||
_isShowingLockScreen = true;
|
||||
final result = await requestAuthentication(
|
||||
"Please authenticate to view your secrets",
|
||||
context,
|
||||
context.l10n.authToViewSecrets,
|
||||
);
|
||||
_logger.finest("LockScreen Result $result $id");
|
||||
_isShowingLockScreen = false;
|
||||
if (result) {
|
||||
lastAuthenticatingTime = DateTime.now().millisecondsSinceEpoch;
|
||||
AppLock.of(context)!.didUnlock();
|
||||
} else {
|
||||
if (!_hasPlacedAppInBackground) {
|
||||
// Treat this as a failure only if user did not explicitly
|
||||
// put the app in background
|
||||
_hasAuthenticationFailed = true;
|
||||
_logger.info("Authentication failed");
|
||||
}
|
||||
}
|
||||
} catch (e, s) {
|
||||
_isShowingLockScreen = false;
|
||||
_logger.severe(e, s);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class IconUtils {
|
||||
IconUtils._privateConstructor();
|
||||
@@ -77,20 +78,32 @@ class IconUtils {
|
||||
}
|
||||
|
||||
Future<void> _loadJson() async {
|
||||
final simpleIconData = await rootBundle
|
||||
.loadString('assets/simple-icons/_data/simple-icons.json');
|
||||
final simpleIcons = json.decode(simpleIconData);
|
||||
for (final icon in simpleIcons["icons"]) {
|
||||
_simpleIcons[icon["title"].toString().toLowerCase()] = icon["hex"];
|
||||
}
|
||||
final customIconData = await rootBundle
|
||||
.loadString('assets/custom-icons/_data/custom-icons.json');
|
||||
final customIcons = json.decode(customIconData);
|
||||
for (final icon in customIcons["icons"]) {
|
||||
_customIcons[icon["title"].toString().toLowerCase()] = CustomIconData(
|
||||
icon["slug"],
|
||||
icon["hex"],
|
||||
);
|
||||
try {
|
||||
final simpleIconData = await rootBundle
|
||||
.loadString('assets/simple-icons/_data/simple-icons.json');
|
||||
final simpleIcons = json.decode(simpleIconData);
|
||||
for (final icon in simpleIcons["icons"]) {
|
||||
_simpleIcons[icon["title"].toString().toLowerCase()] = icon["hex"];
|
||||
}
|
||||
final customIconData = await rootBundle
|
||||
.loadString('assets/custom-icons/_data/custom-icons.json');
|
||||
final customIcons = json.decode(customIconData);
|
||||
for (final icon in customIcons["icons"]) {
|
||||
_customIcons[icon["title"].toString().toLowerCase()] = CustomIconData(
|
||||
icon["slug"],
|
||||
icon["hex"],
|
||||
);
|
||||
if (icon["altNames"] != null) {
|
||||
for (final name in icon["altNames"]) {
|
||||
_customIcons[name] = CustomIconData(
|
||||
icon["slug"],
|
||||
icon["hex"],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Logger("IconUtils").severe("Error loading icons", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,37 @@
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:local_auth_android/local_auth_android.dart';
|
||||
import 'package:local_auth_ios/types/auth_messages_ios.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
Future<bool> requestAuthentication(String reason) async {
|
||||
Future<bool> requestAuthentication(BuildContext context, String reason) async {
|
||||
Logger("AuthUtil").info("Requesting authentication");
|
||||
await LocalAuthentication().stopAuthentication();
|
||||
final l10n = context.l10n;
|
||||
return await LocalAuthentication().authenticate(
|
||||
localizedReason: reason,
|
||||
authMessages: [
|
||||
const AndroidAuthMessages(
|
||||
biometricHint: "Verify identity",
|
||||
biometricNotRecognized: "Not recognized, try again",
|
||||
biometricRequiredTitle: "Biometric required",
|
||||
biometricSuccess: "Successfully verified",
|
||||
cancelButton: "Cancel",
|
||||
deviceCredentialsRequiredTitle: "Device credentials required",
|
||||
deviceCredentialsSetupDescription: "Device credentials required",
|
||||
goToSettingsButton: "Go to settings",
|
||||
goToSettingsDescription:
|
||||
"Authentication is not setup on your device, go to Settings > Security to set it up",
|
||||
signInTitle: "Authentication required",
|
||||
AndroidAuthMessages(
|
||||
biometricHint: l10n.androidBiometricHint,
|
||||
biometricNotRecognized: l10n.androidBiometricNotRecognized,
|
||||
biometricRequiredTitle: l10n.androidBiometricRequiredTitle,
|
||||
biometricSuccess: l10n.androidBiometricSuccess,
|
||||
cancelButton: l10n.androidCancelButton,
|
||||
deviceCredentialsRequiredTitle:
|
||||
l10n.androidDeviceCredentialsRequiredTitle,
|
||||
deviceCredentialsSetupDescription:
|
||||
l10n.androidDeviceCredentialsSetupDescription,
|
||||
goToSettingsButton: l10n.goToSettings,
|
||||
goToSettingsDescription: l10n.androidGoToSettingsDescription,
|
||||
signInTitle: l10n.androidSignInTitle,
|
||||
),
|
||||
IOSAuthMessages(
|
||||
goToSettingsButton: l10n.goToSettings,
|
||||
goToSettingsDescription: l10n.goToSettings,
|
||||
lockOut: l10n.iOSLockOut,
|
||||
// cancelButton default value is "Ok"
|
||||
cancelButton: l10n.iOSOkButton,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -42,8 +42,8 @@ Uint8List cryptoPwHash(Map<String, dynamic> args) {
|
||||
}
|
||||
|
||||
Uint8List cryptoKdfDeriveFromKey(
|
||||
Map<String, dynamic> args,
|
||||
) {
|
||||
Map<String, dynamic> args,
|
||||
) {
|
||||
return Sodium.cryptoKdfDeriveFromKey(
|
||||
args["subkeyLen"],
|
||||
args["subkeyId"],
|
||||
@@ -58,7 +58,7 @@ Future<Uint8List> cryptoGenericHash(Map<String, dynamic> args) async {
|
||||
final sourceFileLength = await sourceFile.length();
|
||||
final inputFile = sourceFile.openSync(mode: io.FileMode.read);
|
||||
final state =
|
||||
Sodium.cryptoGenerichashInit(null, Sodium.cryptoGenerichashBytesMax);
|
||||
Sodium.cryptoGenerichashInit(null, Sodium.cryptoGenerichashBytesMax);
|
||||
var bytesRead = 0;
|
||||
bool isDone = false;
|
||||
while (!isDone) {
|
||||
@@ -77,7 +77,7 @@ Future<Uint8List> cryptoGenericHash(Map<String, dynamic> args) async {
|
||||
|
||||
EncryptionResult chachaEncryptData(Map<String, dynamic> args) {
|
||||
final initPushResult =
|
||||
Sodium.cryptoSecretstreamXchacha20poly1305InitPush(args["key"]);
|
||||
Sodium.cryptoSecretstreamXchacha20poly1305InitPush(args["key"]);
|
||||
final encryptedData = Sodium.cryptoSecretstreamXchacha20poly1305Push(
|
||||
initPushResult.state,
|
||||
args["source"],
|
||||
@@ -102,7 +102,7 @@ Future<EncryptionResult> chachaEncryptFile(Map<String, dynamic> args) async {
|
||||
final inputFile = sourceFile.openSync(mode: io.FileMode.read);
|
||||
final key = args["key"] ?? Sodium.cryptoSecretstreamXchacha20poly1305Keygen();
|
||||
final initPushResult =
|
||||
Sodium.cryptoSecretstreamXchacha20poly1305InitPush(key);
|
||||
Sodium.cryptoSecretstreamXchacha20poly1305InitPush(key);
|
||||
var bytesRead = 0;
|
||||
var tag = Sodium.cryptoSecretstreamXchacha20poly1305TagMessage;
|
||||
while (tag != Sodium.cryptoSecretstreamXchacha20poly1305TagFinal) {
|
||||
@@ -156,7 +156,7 @@ Future<void> chachaDecryptFile(Map<String, dynamic> args) async {
|
||||
final buffer = await inputFile.read(chunkSize);
|
||||
bytesRead += chunkSize;
|
||||
final pullResult =
|
||||
Sodium.cryptoSecretstreamXchacha20poly1305Pull(pullState, buffer, null);
|
||||
Sodium.cryptoSecretstreamXchacha20poly1305Pull(pullState, buffer, null);
|
||||
await destinationFile.writeAsBytes(pullResult.m, mode: io.FileMode.append);
|
||||
tag = pullResult.tag;
|
||||
}
|
||||
@@ -190,20 +190,22 @@ class CryptoUtil {
|
||||
Sodium.init();
|
||||
}
|
||||
|
||||
static Uint8List base642bin(String b64, {
|
||||
static Uint8List base642bin(
|
||||
String b64, {
|
||||
String? ignore,
|
||||
int variant = Sodium.base64VariantOriginal,
|
||||
}) {
|
||||
return Sodium.base642bin(b64, ignore: ignore, variant: variant);
|
||||
}
|
||||
|
||||
static String bin2base64(Uint8List bin, {
|
||||
static String bin2base64(
|
||||
Uint8List bin, {
|
||||
bool urlSafe = false,
|
||||
}) {
|
||||
return Sodium.bin2base64(
|
||||
bin,
|
||||
variant:
|
||||
urlSafe ? Sodium.base64VariantUrlsafe : Sodium.base64VariantOriginal,
|
||||
urlSafe ? Sodium.base64VariantUrlsafe : Sodium.base64VariantOriginal,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -237,9 +239,11 @@ class CryptoUtil {
|
||||
|
||||
// Decrypts the given cipher, with the given key and nonce using XSalsa20
|
||||
// (w Poly1305 MAC).
|
||||
static Future<Uint8List> decrypt(Uint8List cipher,
|
||||
Uint8List key,
|
||||
Uint8List nonce,) async {
|
||||
static Future<Uint8List> decrypt(
|
||||
Uint8List cipher,
|
||||
Uint8List key,
|
||||
Uint8List nonce,
|
||||
) async {
|
||||
final args = <String, dynamic>{};
|
||||
args["cipher"] = cipher;
|
||||
args["nonce"] = nonce;
|
||||
@@ -256,9 +260,11 @@ class CryptoUtil {
|
||||
// This function runs on the same thread as the caller, so should be used only
|
||||
// for small amounts of data where thread switching can result in a degraded
|
||||
// user experience
|
||||
static Uint8List decryptSync(Uint8List cipher,
|
||||
Uint8List key,
|
||||
Uint8List nonce,) {
|
||||
static Uint8List decryptSync(
|
||||
Uint8List cipher,
|
||||
Uint8List key,
|
||||
Uint8List nonce,
|
||||
) {
|
||||
final args = <String, dynamic>{};
|
||||
args["cipher"] = cipher;
|
||||
args["nonce"] = nonce;
|
||||
@@ -270,8 +276,10 @@ class CryptoUtil {
|
||||
// nonce, using XChaCha20 (w Poly1305 MAC).
|
||||
// This function runs on the isolate pool held by `_computer`.
|
||||
// TODO: Remove "ChaCha", an implementation detail from the function name
|
||||
static Future<EncryptionResult> encryptChaCha(Uint8List source,
|
||||
Uint8List key,) async {
|
||||
static Future<EncryptionResult> encryptChaCha(
|
||||
Uint8List source,
|
||||
Uint8List key,
|
||||
) async {
|
||||
final args = <String, dynamic>{};
|
||||
args["source"] = source;
|
||||
args["key"] = key;
|
||||
@@ -285,9 +293,11 @@ class CryptoUtil {
|
||||
// Decrypts the given source, with the given key and header using XChaCha20
|
||||
// (w Poly1305 MAC).
|
||||
// TODO: Remove "ChaCha", an implementation detail from the function name
|
||||
static Future<Uint8List> decryptChaCha(Uint8List source,
|
||||
Uint8List key,
|
||||
Uint8List header,) async {
|
||||
static Future<Uint8List> decryptChaCha(
|
||||
Uint8List source,
|
||||
Uint8List key,
|
||||
Uint8List header,
|
||||
) async {
|
||||
final args = <String, dynamic>{};
|
||||
args["source"] = source;
|
||||
args["key"] = key;
|
||||
@@ -304,10 +314,10 @@ class CryptoUtil {
|
||||
// to the destinationFilePath.
|
||||
// If a key is not provided, one is generated and returned.
|
||||
static Future<EncryptionResult> encryptFile(
|
||||
String sourceFilePath,
|
||||
String destinationFilePath, {
|
||||
Uint8List? key,
|
||||
}) {
|
||||
String sourceFilePath,
|
||||
String destinationFilePath, {
|
||||
Uint8List? key,
|
||||
}) {
|
||||
final args = <String, dynamic>{};
|
||||
args["sourceFilePath"] = sourceFilePath;
|
||||
args["destinationFilePath"] = destinationFilePath;
|
||||
@@ -322,10 +332,11 @@ class CryptoUtil {
|
||||
// Decrypts the file at sourceFilePath, with the given key and header using
|
||||
// XChaCha20 (w Poly1305 MAC), and writes it to the destinationFilePath.
|
||||
static Future<void> decryptFile(
|
||||
String sourceFilePath,
|
||||
String destinationFilePath,
|
||||
Uint8List header,
|
||||
Uint8List key,) {
|
||||
String sourceFilePath,
|
||||
String destinationFilePath,
|
||||
Uint8List header,
|
||||
Uint8List key,
|
||||
) {
|
||||
final args = <String, dynamic>{};
|
||||
args["sourceFilePath"] = sourceFilePath;
|
||||
args["destinationFilePath"] = destinationFilePath;
|
||||
@@ -356,10 +367,10 @@ class CryptoUtil {
|
||||
|
||||
// Decrypts the input using the given publicKey-secretKey pair
|
||||
static Uint8List openSealSync(
|
||||
Uint8List input,
|
||||
Uint8List publicKey,
|
||||
Uint8List secretKey,
|
||||
) {
|
||||
Uint8List input,
|
||||
Uint8List publicKey,
|
||||
Uint8List secretKey,
|
||||
) {
|
||||
return Sodium.cryptoBoxSealOpen(input, publicKey, secretKey);
|
||||
}
|
||||
|
||||
@@ -377,9 +388,9 @@ class CryptoUtil {
|
||||
// At all points, we ensure that the product of these two variables (the area
|
||||
// under the graph that determines the amount of work required) is a constant.
|
||||
static Future<DerivedKeyResult> deriveSensitiveKey(
|
||||
Uint8List password,
|
||||
Uint8List salt,
|
||||
) async {
|
||||
Uint8List password,
|
||||
Uint8List salt,
|
||||
) async {
|
||||
final logger = Logger("pwhash");
|
||||
int memLimit = Sodium.cryptoPwhashMemlimitSensitive;
|
||||
int opsLimit = Sodium.cryptoPwhashOpslimitSensitive;
|
||||
@@ -407,7 +418,10 @@ class CryptoUtil {
|
||||
return DerivedKeyResult(key, memLimit, opsLimit);
|
||||
} catch (e, s) {
|
||||
logger.warning(
|
||||
"failed to deriveKey mem: $memLimit, ops: $opsLimit", e, s,);
|
||||
"failed to deriveKey mem: $memLimit, ops: $opsLimit",
|
||||
e,
|
||||
s,
|
||||
);
|
||||
}
|
||||
memLimit = (memLimit / 2).round();
|
||||
opsLimit = opsLimit * 2;
|
||||
@@ -421,9 +435,9 @@ class CryptoUtil {
|
||||
// extra layer of authentication (atop the access token and collection key).
|
||||
// More details @ https://ente.io/blog/building-shareable-links/
|
||||
static Future<DerivedKeyResult> deriveInteractiveKey(
|
||||
Uint8List password,
|
||||
Uint8List salt,
|
||||
) async {
|
||||
Uint8List password,
|
||||
Uint8List salt,
|
||||
) async {
|
||||
final int memLimit = Sodium.cryptoPwhashMemlimitInteractive;
|
||||
final int opsLimit = Sodium.cryptoPwhashOpslimitInteractive;
|
||||
final key = await deriveKey(password, salt, memLimit, opsLimit);
|
||||
@@ -433,13 +447,13 @@ class CryptoUtil {
|
||||
// Derives a key for a given password, salt, memLimit and opsLimit using
|
||||
// Argon2id, v1.3.
|
||||
static Future<Uint8List> deriveKey(
|
||||
Uint8List password,
|
||||
Uint8List salt,
|
||||
int memLimit,
|
||||
int opsLimit,
|
||||
) {
|
||||
Uint8List password,
|
||||
Uint8List salt,
|
||||
int memLimit,
|
||||
int opsLimit,
|
||||
) async {
|
||||
try {
|
||||
return _computer.compute(
|
||||
return await _computer.compute(
|
||||
cryptoPwHash,
|
||||
param: {
|
||||
"password": password,
|
||||
@@ -449,7 +463,7 @@ class CryptoUtil {
|
||||
},
|
||||
taskName: "deriveKey",
|
||||
);
|
||||
} catch(e,s) {
|
||||
} catch (e, s) {
|
||||
final String errMessage = 'failed to deriveKey memLimit: $memLimit and '
|
||||
'opsLimit: $opsLimit';
|
||||
Logger("CryptoUtilDeriveKey").warning(errMessage, e, s);
|
||||
@@ -461,20 +475,25 @@ class CryptoUtil {
|
||||
// (Key Derivation Function) with the `loginSubKeyId` and
|
||||
// `loginSubKeyLen` and `loginSubKeyContext` as context
|
||||
static Future<Uint8List> deriveLoginKey(
|
||||
Uint8List key,
|
||||
) async {
|
||||
final Uint8List derivedKey = await _computer.compute(
|
||||
cryptoKdfDeriveFromKey,
|
||||
param: {
|
||||
"key": key,
|
||||
"subkeyId": loginSubKeyId,
|
||||
"subkeyLen": loginSubKeyLen,
|
||||
"context": utf8.encode(loginSubKeyContext),
|
||||
},
|
||||
taskName: "deriveLoginKey",
|
||||
);
|
||||
// return the first 16 bytes of the derived key
|
||||
return derivedKey.sublist(0, 16);
|
||||
Uint8List key,
|
||||
) async {
|
||||
try {
|
||||
final Uint8List derivedKey = await _computer.compute(
|
||||
cryptoKdfDeriveFromKey,
|
||||
param: {
|
||||
"key": key,
|
||||
"subkeyId": loginSubKeyId,
|
||||
"subkeyLen": loginSubKeyLen,
|
||||
"context": utf8.encode(loginSubKeyContext),
|
||||
},
|
||||
taskName: "deriveLoginKey",
|
||||
);
|
||||
// return the first 16 bytes of the derived key
|
||||
return derivedKey.sublist(0, 16);
|
||||
} catch (e, s) {
|
||||
Logger("deriveLoginKey").severe("loginKeyDerivation failed", e, s);
|
||||
throw LoginKeyDerivationError();
|
||||
}
|
||||
}
|
||||
|
||||
// Computes and returns the hash of the source file
|
||||
|
||||
@@ -768,7 +768,7 @@ packages:
|
||||
source: hosted
|
||||
version: "2.1.7"
|
||||
local_auth_android:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: local_auth_android
|
||||
sha256: "523dd636ce061ddb296cbc3db410cb8f21efb7d8798f7b9532c8038ce2f8bad5"
|
||||
@@ -776,7 +776,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.0.31"
|
||||
local_auth_ios:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: local_auth_ios
|
||||
sha256: edc2977c5145492f3451db9507a2f2f284ee4f408950b3e16670838726761940
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: ente_auth
|
||||
description: ente two-factor authenticator
|
||||
version: 2.0.14+214
|
||||
version: 2.0.25+225
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
@@ -54,6 +54,9 @@ dependencies:
|
||||
intl: ^0.18.0
|
||||
json_annotation: ^4.5.0
|
||||
local_auth: ^2.1.7
|
||||
|
||||
local_auth_android: ^1.0.31
|
||||
local_auth_ios: ^1.1.3
|
||||
logging: ^1.0.1
|
||||
modal_bottom_sheet: ^3.0.0-pre
|
||||
move_to_background: ^1.0.2
|
||||
|
||||
Reference in New Issue
Block a user