Compare commits
84 Commits
v1.0.7
...
old-deskto
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b759be9609 | ||
|
|
ed3aafa7fc | ||
|
|
ff78ffb2dd | ||
|
|
9aa8f4931a | ||
|
|
6505ed37fc | ||
|
|
9417508d05 | ||
|
|
5d08264dd0 | ||
|
|
bd439cc19e | ||
|
|
eacb043fd0 | ||
|
|
5c54b14768 | ||
|
|
6dd3903c78 | ||
|
|
6996650ad5 | ||
|
|
c9e9b7a38d | ||
|
|
26930f2760 | ||
|
|
17fc973bfb | ||
|
|
0aae349509 | ||
|
|
bacca876ae | ||
|
|
d7e1998fbd | ||
|
|
f53b44ec97 | ||
|
|
338efb1eba | ||
|
|
a5cac1579b | ||
|
|
7a90a09d0c | ||
|
|
9508b30353 | ||
|
|
7207921838 | ||
|
|
56a741b2ac | ||
|
|
e52aa78fae | ||
|
|
9bcd107de8 | ||
|
|
8e5488eaf9 | ||
|
|
b20139f3b8 | ||
|
|
21b139687d | ||
|
|
385731196f | ||
|
|
71c2958078 | ||
|
|
7a25da3927 | ||
|
|
34c9ee76b3 | ||
|
|
3fbad241b1 | ||
|
|
9143634000 | ||
|
|
a573c012e0 | ||
|
|
94debda2c7 | ||
|
|
c9f37b4f5a | ||
|
|
05652656ae | ||
|
|
8e715b4cf9 | ||
|
|
e264d3456b | ||
|
|
56a73ab7ee | ||
|
|
963d18b501 | ||
|
|
b35d3bda16 | ||
|
|
7ad76adcaa | ||
|
|
691eaabf50 | ||
|
|
7ab16df330 | ||
|
|
3c16cfd829 | ||
|
|
8c9c9c53d1 | ||
|
|
aeaaddbe40 | ||
|
|
25782870c7 | ||
|
|
39e28dd63b | ||
|
|
517ce33fd9 | ||
|
|
02d2cb5733 | ||
|
|
2b5f349b2f | ||
|
|
08ad496975 | ||
|
|
f52ee5683b | ||
|
|
332faa9166 | ||
|
|
3d95c789f5 | ||
|
|
244d4d969a | ||
|
|
32605dc708 | ||
|
|
ef0c302c94 | ||
|
|
fba694de68 | ||
|
|
3d4cd106cb | ||
|
|
61e6e0ffaa | ||
|
|
e0b952e516 | ||
|
|
546a9234a4 | ||
|
|
da2083ef08 | ||
|
|
8a1177e7db | ||
|
|
8acd1faf03 | ||
|
|
0c28b83b46 | ||
|
|
fa0ac608f4 | ||
|
|
0965b367cc | ||
|
|
37db940720 | ||
|
|
334800472e | ||
|
|
0da4497857 | ||
|
|
0b05a21dc7 | ||
|
|
2558473399 | ||
|
|
4dd465fdfa | ||
|
|
1f20ba17f4 | ||
|
|
9ce8059212 | ||
|
|
e69a3adf14 | ||
|
|
8baa350056 |
9
.vscode/launch.json
vendored
@@ -29,11 +29,18 @@
|
||||
"args": ["--dart-define", "endpoint=http://192.168.1.30:8080"]
|
||||
},
|
||||
{
|
||||
"name": "Prod",
|
||||
"name": "iOS Prod",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"program": "lib/main.dart",
|
||||
"args": ["--target", "lib/main.dart"]
|
||||
},
|
||||
{
|
||||
"name": "Android Prod",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"program": "lib/main.dart",
|
||||
"args": ["--target", "lib/main.dart", "--flavor", "independent"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
687
LICENSE
@@ -1,21 +1,674 @@
|
||||
MIT License
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (c) 2021 Very Good Ventures
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
Preamble
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
|
||||
131
README.md
@@ -1,3 +1,130 @@
|
||||
# ente Auth
|
||||
# 🔥 Warning 🔥
|
||||
|
||||
Open source authenticator app for your 2FA secrets, with end-to-end encrypted backups.
|
||||
This branch contains untested code that can potentially corrupt your data.
|
||||
|
||||
**You <u>should not</u> run this against your main acccount.**
|
||||
|
||||
If you are interested in the official release of our desktop app, please ⭐ this repo.
|
||||
|
||||
---
|
||||
|
||||
<br/>
|
||||
|
||||
# ente Authenticator
|
||||
|
||||
ente's Authenticator app helps you generate and store 2 step verification (2FA)
|
||||
tokens on your mobile devices.
|
||||
|
||||

|
||||
|
||||
## ✨ Features
|
||||
|
||||
### Secure Backups
|
||||
|
||||
ente provides end-to-end encrypted cloud backups so that you don't have to worry
|
||||
about losing your tokens. We use the same protocols [ente
|
||||
Photos](https://ente.io) uses to encrypt and preserve your data.
|
||||
|
||||
|
||||
### Multi Device Synchronization
|
||||
|
||||
ente will automatically sync the 2FA tokens you add to your account, across all
|
||||
your devices. Every new device you sign into will have access to these tokens.
|
||||
|
||||
|
||||
### Offline Mode
|
||||
|
||||
ente generates 2FA tokens offline, so your network connectivity will not get in
|
||||
the way of your workflow.
|
||||
|
||||
### Import and Export Tokens
|
||||
|
||||
You can add tokens to ente by one of the following methods:
|
||||
1. Scanning a QR code
|
||||
2. Manually entering (copy-pasting) a 2FA secret
|
||||
3. Bulk importing from a file that contains a list of codes in the following
|
||||
format:
|
||||
```
|
||||
otpauth://totp/ACCOUNT?secret=SUPERSECRET&issuer=SERVICE
|
||||
```
|
||||
The codes maybe separated by new lines or commas.
|
||||
|
||||
You can also export the codes you have added to ente, to an **unencrypted** text
|
||||
file, that adheres to the above format.
|
||||
|
||||
|
||||
## 📲 Download
|
||||
|
||||
### Android
|
||||
|
||||
This repository's [GitHub releases](https://github.com/ente-io/auth/releases/latest/download/ente-auth.apks) contains APKs, built straight from source. These builds keep themselves updated, without relying on third party stores.
|
||||
|
||||
You can alternatively install the build from PlayStore.
|
||||
|
||||
<a href="https://play.google.com/store/apps/details?id=io.ente.auth">
|
||||
<img width="197" alt="Get it on Google Play" src="https://ente.io/static/ed265c3abdcd3efa5e29f64b927bcb44/e230a/play-store-badge.webp">
|
||||
</a>
|
||||
|
||||
### iOS
|
||||
|
||||
<a href="https://apps.apple.com/us/app/ente-authenticator/id6444121398">
|
||||
<img width="197" alt="Download on AppStore" src="https://user-images.githubusercontent.com/1161789/154795157-c4468ff9-97fd-46f3-87fe-dca789d8733a.png">
|
||||
</a>
|
||||
|
||||
### Desktop
|
||||
|
||||
Support for desktop platforms (Linux, Mac and Windows) is a [work in progress](https://github.com/ente-io/auth/tree/desktop).
|
||||
|
||||
Please ⭐ this repo to be notified of updates.
|
||||
|
||||
|
||||
## 🔩 Architecture
|
||||
|
||||
The architecture that powers end-to-end encrypted storage and sync of your
|
||||
tokens has been documented [here](architecture/index.md).
|
||||
|
||||
|
||||
## 🧑💻 Building from source
|
||||
|
||||
1. [Install Flutter](https://flutter.dev/docs/get-started/install)
|
||||
2. Clone this repository with `git clone git@github.com:ente-io/auth.git`
|
||||
3. Pull in all submodules with `git submodule update --init --recursive`
|
||||
|
||||
### Android
|
||||
```
|
||||
flutter build apk --release --flavor independent
|
||||
```
|
||||
|
||||
### iOS
|
||||
```
|
||||
flutter build ios
|
||||
```
|
||||
|
||||
### Linux
|
||||
```
|
||||
flutter build linux
|
||||
```
|
||||
|
||||
### MacOS
|
||||
```
|
||||
fluter build macos
|
||||
```
|
||||
|
||||
### Windows
|
||||
```
|
||||
flutter build windows
|
||||
```
|
||||
|
||||
|
||||
## 🙋♂️ Support
|
||||
|
||||
If you need help, please reach out to support@ente.io, and a human will get in
|
||||
touch with you.
|
||||
|
||||
On the other hand, if you wish to support us, please
|
||||
[star](https://github.com/ente-io/auth/stargazers) this project.
|
||||
|
||||
|
||||
## 💜 Community
|
||||
- Follow us on [Twitter](https://twitter.com/enteio)
|
||||
- Join us on [Discord](https://ente.io/discord)
|
||||
|
||||
50
SECURITY.md
Normal file
@@ -0,0 +1,50 @@
|
||||
ente believes that working with security researchers across the globe is crucial
|
||||
to keeping our users safe. If you believe you've found a security issue in our
|
||||
product or service, we encourage you to notify us (security@ente.io). We welcome
|
||||
working with you to resolve the issue promptly. Thanks in advance!
|
||||
|
||||
# Disclosure Policy
|
||||
|
||||
- Let us know as soon as possible upon discovery of a potential security issue,
|
||||
and we'll make every effort to quickly resolve the issue.
|
||||
- Provide us a reasonable amount of time to resolve the issue before any
|
||||
disclosure to the public or a third-party. We may publicly disclose the issue
|
||||
before resolving it, if appropriate.
|
||||
- Make a good faith effort to avoid privacy violations, destruction of data, and
|
||||
interruption or degradation of our service. Only interact with accounts you
|
||||
own or with explicit permission of the account holder.
|
||||
- If you would like to encrypt your report, please use the PGP key with long ID
|
||||
`E273695C0403F34F74171932DF6DDDE98EBD2394` (available in the public keyserver
|
||||
pool).
|
||||
|
||||
# In-scope
|
||||
|
||||
- Security issues in any current release of ente. This includes the web app,
|
||||
desktop app, and mobile apps (iOS and Android). Product downloads are
|
||||
available at https://ente.io. Source code is available at
|
||||
https://github.com/ente-io.
|
||||
|
||||
# Exclusions
|
||||
|
||||
The following bug classes are out-of scope:
|
||||
|
||||
- Bugs that are already reported on any of ente's issue trackers
|
||||
(https://github.com/ente-io), or that we already know of. Note that some of
|
||||
our issue tracking is private.
|
||||
- Issues in an upstream software dependency (ex: Flutter, Next.js etc) which are
|
||||
already reported to the upstream maintainer.
|
||||
- Attacks requiring physical access to a user's device.
|
||||
- Self-XSS
|
||||
- Issues related to software or protocols not under ente's control
|
||||
- Vulnerabilities in outdated versions of ente
|
||||
- Missing security best practices that do not directly lead to a vulnerability
|
||||
- Issues that do not have any impact on the general public
|
||||
|
||||
While researching, we'd like to ask you to refrain from:
|
||||
|
||||
- Denial of service
|
||||
- Spamming
|
||||
- Social engineering (including phishing) of ente staff or contractors
|
||||
- Any physical attempts against ente property or data centers
|
||||
|
||||
Thank you for helping keep ente and our users safe!
|
||||
@@ -35,7 +35,7 @@
|
||||
<meta-data android:name="flutterEmbedding" android:value="2"/>
|
||||
|
||||
<meta-data android:name="io.sentry.dsn"
|
||||
android:value="https://8aeb7f013be74f829f8b73b46b3d7a80@sentry.ente.io/8"/>
|
||||
android:value="https://ed4ddd6309b847ba8849935e26e9b648@sentry.ente.io/9"/>
|
||||
</application>
|
||||
|
||||
<queries>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.ente.auth
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
class MainActivity: FlutterFragmentActivity() {
|
||||
}
|
||||
|
||||
35
architecture/authentication.svg
Normal file
|
After Width: | Height: | Size: 119 KiB |
11
architecture/e2ee.svg
Normal file
|
After Width: | Height: | Size: 22 KiB |
26
architecture/file-encryption.svg
Normal file
|
After Width: | Height: | Size: 105 KiB |
275
architecture/index.md
Normal file
@@ -0,0 +1,275 @@
|
||||
# Architecture
|
||||
|
||||
This is an overview of ente's end-to-end encrypted architecture and
|
||||
specifications of the underlying cryptography.
|
||||
|
||||
## Introduction
|
||||
|
||||
Your data is end-to-end encrypted with **ente**. Meaning, they are encrypted
|
||||
with your `keys` before they leave your device.
|
||||
|
||||
<img src="e2ee.svg" class="architecture-svg" style="max-width: 600px"
|
||||
title="End-to-end encryption in ente" />
|
||||
|
||||
<br/>
|
||||
|
||||
These `keys` are available only to you. Meaning only you can access your data
|
||||
else where.
|
||||
|
||||
What follows is an explanation of how we do what we do.
|
||||
|
||||
## Key Encryption
|
||||
|
||||
### Fundamentals
|
||||
|
||||
#### Master Key
|
||||
|
||||
When you sign up for **ente**, your client generates a `masterKey` for you. This
|
||||
never leaves your device unencrypted.
|
||||
|
||||
#### Key Encryption Key
|
||||
|
||||
Once you choose a password, a `keyEncryptionKey` is derived from it. This never
|
||||
leaves your device.
|
||||
|
||||
### Flows
|
||||
|
||||
#### Primary Device
|
||||
|
||||
During registration, your `masterKey` is encrypted with your`keyEncryptionKey`,
|
||||
and the resultant `encryptedMasterKey` is then sent to our servers for storage.
|
||||
|
||||
<img src="key-derivation.svg" class="architecture-svg" title="Key derivation" />
|
||||
|
||||
#### <a id="key-encryption-flows-secondary-device"></a> Secondary Device
|
||||
|
||||
When you sign in on a secondary device, after you successfully verify your
|
||||
email, our servers give you back your `encryptedMasterKey` that was sent to us
|
||||
by your primary device.
|
||||
|
||||
You are then prompted to enter your password. Once entered, your
|
||||
`keyEncryptionKey` is derived, and the client decrypts your `encryptedMasterKey`
|
||||
with this, to yield your original `masterKey`.
|
||||
|
||||
If the decryption fails, the client will know that the derived
|
||||
`keyEncryptionKey` was wrong, indicating an incorrect password, and this
|
||||
information will be surfaced to you.
|
||||
|
||||
### Privacy
|
||||
|
||||
- Since only you know your password, only you can derive your
|
||||
`keyEncryptionKey`.
|
||||
- Since only you can derive your `keyEncryptionKey`, only you have access to
|
||||
your `masterKey`.
|
||||
|
||||
> Keep reading to learn about how this `masterKey` is used to encrypt your data.
|
||||
|
||||
---
|
||||
|
||||
## Token Encryption
|
||||
|
||||
### Fundamentals
|
||||
|
||||
#### Token Key
|
||||
|
||||
Each of your tokens in **ente** are encrypted with a `tokenKey`. These never
|
||||
leave your device unencrypted.
|
||||
|
||||
#### Authenticator Key
|
||||
|
||||
Each of your `tokenKey`s are in turn encrypted with an `authKey`. This never
|
||||
leave your device unencrypted.
|
||||
|
||||
### Flows
|
||||
|
||||
#### Upload
|
||||
|
||||
- Each token and associated metadata is encrypted with randomly generated
|
||||
`tokenKey`s.
|
||||
- Each `tokenKey` is encrypted with your `authKey`. In case your account does
|
||||
not have an `authKey` yet, one is randomly generated and encrypted with your
|
||||
`masterKey`.
|
||||
- All of the above mentioned encrypted data is then pushed to the server for
|
||||
storage.
|
||||
|
||||
<img src="token-encryption.svg" class="architecture-svg" title="Token
|
||||
encryption" />
|
||||
|
||||
#### Download
|
||||
|
||||
- All of the above mentioned encrypted data is pulled from the server.
|
||||
- You first decrypt your `authKey` with your `masterKey`.
|
||||
- You then decrypt each token's `tokenKey` with your `authKey`.
|
||||
- Finally, you decrypt each token and associated metadata with the respective
|
||||
`tokenKey`s.
|
||||
|
||||
### Privacy
|
||||
|
||||
- As explained in the previous section, only you have access to your
|
||||
`masterKey`.
|
||||
- Since only you have access to your `masterKey`, only you can decrypt your
|
||||
`authKey`.
|
||||
- Since only you have access to your `authKey`, only you can decrypt the
|
||||
`tokenKey`s.
|
||||
- Since only you have access to the `tokenKey`s, only you can decrypt the tokens
|
||||
and their associated metadata.
|
||||
|
||||
---
|
||||
|
||||
## Key Recovery
|
||||
|
||||
### Fundamentals
|
||||
|
||||
#### Recovery Key
|
||||
|
||||
When you sign up for **ente**, your app generates a `recoveryKey` for you. This
|
||||
never leaves your device unencrypted.
|
||||
|
||||
### Flow
|
||||
|
||||
#### Storage
|
||||
|
||||
Your `recoveryKey` and `masterKey` are encrypted with each other and stored on
|
||||
the server.
|
||||
|
||||
#### Access
|
||||
|
||||
This encrypted `recoveryKey` is downloaded when you sign in on a new device.
|
||||
This is decrypted with your `masterKey` and surfaced to you whenever you request
|
||||
for it.
|
||||
|
||||
#### Recovery
|
||||
|
||||
Post email verification, if you're unable to unlock your account because you
|
||||
have forgotten your password, the client will prompt you to enter your
|
||||
`recoveryKey`.
|
||||
|
||||
The client then pulls the `masterKey` that was earlier encrypted and pushed to
|
||||
the server (as discussed in [Key Encryption](#key-encryption), and decrypts it
|
||||
with the entered `recoveryKey`. If the decryption succeeds, the client will know
|
||||
that you have entered the correct `recoveryKey`.
|
||||
|
||||
<img src="recovery.svg" class="architecture-svg" title="Recovery" />
|
||||
|
||||
Now that you have your `masterKey`, the client will prompt you to set a new
|
||||
password, using which it will derive a new `keyEncryptionKey`. This is then used
|
||||
to encrypt your `masterKey` and this new `encryptedMasterKey` is uploaded to our
|
||||
servers, similar to what was earlier discussed in [Key
|
||||
Encryption](#key-encryption).
|
||||
|
||||
### Privacy
|
||||
|
||||
- Since only you have access to your `masterKey`, only you can access your
|
||||
`recoveryKey`.
|
||||
- Since only you can access your `recoveryKey`, only you can reset your
|
||||
password.
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
### Fundamentals
|
||||
|
||||
#### One Time Token
|
||||
|
||||
When you attempt to verify ownership of an email address, our server generates a
|
||||
`oneTimeToken`, that if presented confirms your access to the said email
|
||||
address. This token is valid for a short time and can only be used once.
|
||||
|
||||
#### Authentication Token
|
||||
|
||||
When you successfully authenticate yourself against our server by proving
|
||||
ownership of your email (and in future any other configured vectors), the server
|
||||
generates an `authToken`, that can from there on be used to authenticate against
|
||||
our private APIs.
|
||||
|
||||
#### Encrypted Authentication Token
|
||||
|
||||
A generated `authToken` is returned to your client after being encrypted with
|
||||
your `publicKey`. This `encryptedAuthToken` can only be decrypted with your
|
||||
`privateKey`.
|
||||
|
||||
### Flow
|
||||
|
||||
- You are asked for an email address, to which a `oneTimeToken` is sent.
|
||||
- Once you present this information correctly to our server, an `authToken` is
|
||||
generated and an `encryptedAuthToken` is returned to you, along with your
|
||||
other encrypted keys.
|
||||
- You are then prompted to enter your password, using which your `masterKey` is
|
||||
derived (as discussed [here](#key-encryption-flows-secondary-device)).
|
||||
- Using this `masterKey`, the rest of your keys, including your `privateKey` is
|
||||
decrypted (as discussed [here](#private-key)).
|
||||
- Using your `privateKey`, the client will then decrypt the `encryptedAuthToken`
|
||||
that was earlier encrypted by our server with your `publicKey`.
|
||||
- This decrypted `authToken` can then from there on be used to authenticate all
|
||||
API calls against our servers.
|
||||
|
||||
<img src="authentication.svg" class="architecture-svg" title="Authentication" />
|
||||
|
||||
### Security
|
||||
|
||||
Only by verifying access to your email and knowing your password can you obtain
|
||||
an`authToken` that can be used to authenticate yourself against our servers.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Details
|
||||
|
||||
We rely on the high level APIs exposed by this wonderful library called
|
||||
[libsodium](https://libsodium.gitbook.io/doc/).
|
||||
|
||||
#### Key Generation
|
||||
|
||||
[`crypto_secretbox_keygen`](https://libsodium.gitbook.io/doc/public-key_cryptography/sealed_boxes)
|
||||
is used to generate all random keys within the application. Your `masterKey`,
|
||||
`recoveryKey`, `authKey`, `tokenKey` are all 256-bit keys generated using this
|
||||
API.
|
||||
|
||||
#### Key Derivation
|
||||
|
||||
[`crypto_pwhash`](https://libsodium.gitbook.io/doc/password_hashing/default_phf)
|
||||
is used to derive your `keyEncryptionKey` from your password.
|
||||
|
||||
`crypto_pwhash_OPSLIMIT_SENSITIVE` and `crypto_pwhash_MEMLIMIT_SENSITIVE` are
|
||||
used as the limits for computation and memory respectively. If the operation
|
||||
fails due to insufficient memory, the former is doubled and the latter is halved
|
||||
progressively, until a key can be derived. If during this process the memory
|
||||
limit is reduced to a value less than `crypto_pwhash_MEMLIMIT_MIN`, the client
|
||||
will not let you register from that device.
|
||||
|
||||
Internally, this uses [Argon2
|
||||
v1.3](https://github.com/P-H-C/phc-winner-argon2/raw/master/argon2-specs.pdf),
|
||||
which is regarded as [one of the best hashing
|
||||
algorithms](https://en.wikipedia.org/wiki/Argon2) currently available.
|
||||
|
||||
#### Symmetric Encryption
|
||||
|
||||
[`crypto_secretbox_easy`](https://libsodium.gitbook.io/doc/secret-key_cryptography/secretbox)
|
||||
is used to encrypt your `masterKey`, `recoveryKey`, `authKey` and `tokenKey`s.
|
||||
Internally, this uses
|
||||
[XSalsa20](https://libsodium.gitbook.io/doc/advanced/stream_ciphers/xsalsa20)
|
||||
stream cipher with [Poly1305
|
||||
MAC](https://datatracker.ietf.org/doc/html/rfc8439#section-2.5) for
|
||||
authentication.
|
||||
|
||||
[`crypto_secretstream_*`](https://libsodium.gitbook.io/doc/secret-key_cryptography/secretstream)
|
||||
APIs are used to encrypt your token data. Internally, this uses
|
||||
[XChaCha20](https://libsodium.gitbook.io/doc/advanced/stream_ciphers/xchacha20)
|
||||
stream cipher with [Poly1305
|
||||
MAC](https://datatracker.ietf.org/doc/html/rfc8439#section-2.5) for
|
||||
authentication.
|
||||
|
||||
#### Salt & Nonce Generation
|
||||
|
||||
[`randombytes_buf`](https://libsodium.gitbook.io/doc/generating_random_data) is
|
||||
used to generate a new salt/nonce every time data needs to be hashed/encrypted.
|
||||
|
||||
---
|
||||
|
||||
## Further Details
|
||||
|
||||
Thank you for reading this far! For implementation details, we request you to
|
||||
checkout [our code](https://github.com/ente-io).
|
||||
|
||||
If you'd like to help us improve this document, kindly email
|
||||
[security@ente.io](mailto:security@ente.io).
|
||||
17
architecture/key-derivation.svg
Normal file
|
After Width: | Height: | Size: 76 KiB |
33
architecture/recovery.svg
Normal file
|
After Width: | Height: | Size: 130 KiB |
21
architecture/sharing.svg
Normal file
|
After Width: | Height: | Size: 123 KiB |
26
architecture/token-encryption.svg
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
assets/2.0x/loading_photos_background.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
assets/2.0x/loading_photos_background_dark.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
assets/3.0x/loading_photos_background.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
assets/3.0x/loading_photos_background_dark.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
assets/loading_photos_background.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
assets/loading_photos_background_dark.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
@@ -50,6 +50,8 @@ PODS:
|
||||
- flutter_inappwebview/Core (0.0.1):
|
||||
- Flutter
|
||||
- OrderedSet (~> 5.0)
|
||||
- flutter_local_notifications (0.0.1):
|
||||
- Flutter
|
||||
- flutter_native_splash (0.0.1):
|
||||
- Flutter
|
||||
- flutter_secure_storage (3.3.1):
|
||||
@@ -62,8 +64,6 @@ PODS:
|
||||
- FMDB (2.7.5):
|
||||
- FMDB/standard (= 2.7.5)
|
||||
- FMDB/standard (2.7.5)
|
||||
- in_app_purchase (0.0.1):
|
||||
- Flutter
|
||||
- local_auth (0.0.1):
|
||||
- Flutter
|
||||
- move_to_background (0.0.1):
|
||||
@@ -81,6 +81,11 @@ PODS:
|
||||
- SDWebImage (5.13.4):
|
||||
- SDWebImage/Core (= 5.13.4)
|
||||
- SDWebImage/Core (5.13.4)
|
||||
- Sentry/HybridSDK (7.30.2)
|
||||
- sentry_flutter (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- Sentry/HybridSDK (= 7.30.2)
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_ios (0.0.1):
|
||||
@@ -101,16 +106,17 @@ DEPENDENCIES:
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_email_sender (from `.symlinks/plugins/flutter_email_sender/ios`)
|
||||
- flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`)
|
||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- flutter_sodium (from `.symlinks/plugins/flutter_sodium/ios`)
|
||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||
- in_app_purchase (from `.symlinks/plugins/in_app_purchase/ios`)
|
||||
- local_auth (from `.symlinks/plugins/local_auth/ios`)
|
||||
- move_to_background (from `.symlinks/plugins/move_to_background/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||
- qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`)
|
||||
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||
@@ -125,6 +131,7 @@ SPEC REPOS:
|
||||
- OrderedSet
|
||||
- Reachability
|
||||
- SDWebImage
|
||||
- Sentry
|
||||
- SwiftyGif
|
||||
- Toast
|
||||
|
||||
@@ -143,6 +150,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_email_sender/ios"
|
||||
flutter_inappwebview:
|
||||
:path: ".symlinks/plugins/flutter_inappwebview/ios"
|
||||
flutter_local_notifications:
|
||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||
flutter_native_splash:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
flutter_secure_storage:
|
||||
@@ -151,8 +160,6 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_sodium/ios"
|
||||
fluttertoast:
|
||||
:path: ".symlinks/plugins/fluttertoast/ios"
|
||||
in_app_purchase:
|
||||
:path: ".symlinks/plugins/in_app_purchase/ios"
|
||||
local_auth:
|
||||
:path: ".symlinks/plugins/local_auth/ios"
|
||||
move_to_background:
|
||||
@@ -163,6 +170,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||
qr_code_scanner:
|
||||
:path: ".symlinks/plugins/qr_code_scanner/ios"
|
||||
sentry_flutter:
|
||||
:path: ".symlinks/plugins/sentry_flutter/ios"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_ios:
|
||||
@@ -182,12 +191,12 @@ SPEC CHECKSUMS:
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
|
||||
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
|
||||
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
|
||||
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
||||
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
|
||||
flutter_sodium: c84426b4de738514b5b66cfdeb8a06634e72fe0b
|
||||
fluttertoast: 16fbe6039d06a763f3533670197d01fc73459037
|
||||
fluttertoast: 74526702fea2c060ea55dde75895b7e1bde1c86b
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
in_app_purchase: 3e2155afa9d03d4fa32d9e62d567885080ce97d6
|
||||
local_auth: 1740f55d7af0a2e2a8684ce225fe79d8931e808c
|
||||
move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d
|
||||
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
|
||||
@@ -197,6 +206,8 @@ SPEC CHECKSUMS:
|
||||
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
|
||||
Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96
|
||||
SDWebImage: e5cc87bf736e60f49592f307bdf9e157189298a3
|
||||
Sentry: 9be48e341494bc976c963b05aa4a8ca48308c684
|
||||
sentry_flutter: 544e6376e35b00eef9f0864f8bb7f10a0e204993
|
||||
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
|
||||
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
|
||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||
|
||||
@@ -58,5 +58,7 @@
|
||||
<false/>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>Please allow auth to lock itself with FaceID or TouchID</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Please allow auth to pick a file to import data from</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -10,10 +10,11 @@ import 'package:ente_auth/events/signed_in_event.dart';
|
||||
import 'package:ente_auth/events/signed_out_event.dart';
|
||||
import "package:ente_auth/l10n/l10n.dart";
|
||||
import "package:ente_auth/onboarding/view/onboarding_page.dart";
|
||||
import 'package:ente_auth/services/update_service.dart';
|
||||
import 'package:ente_auth/ui/home_page.dart';
|
||||
import 'package:ente_auth/ui/settings/app_update_dialog.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import "package:flutter/material.dart";
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import "package:flutter_localizations/flutter_localizations.dart";
|
||||
|
||||
class App extends StatefulWidget {
|
||||
@@ -39,6 +40,21 @@ class _AppState extends State<App> {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
UpdateService.instance.shouldUpdate().then((shouldUpdate) {
|
||||
if (shouldUpdate) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AppUpdateDialog(
|
||||
UpdateService.instance.getLatestVersionInfo(),
|
||||
);
|
||||
},
|
||||
barrierColor: Colors.black.withOpacity(0.85),
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -62,7 +78,6 @@ class _AppState extends State<App> {
|
||||
theme: lightTheme,
|
||||
darkTheme: dartTheme,
|
||||
debugShowCheckedModeBanner: false,
|
||||
builder: EasyLoading.init(),
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
@@ -80,7 +95,6 @@ class _AppState extends State<App> {
|
||||
theme: lightThemeData,
|
||||
darkTheme: darkThemeData,
|
||||
debugShowCheckedModeBanner: false,
|
||||
builder: EasyLoading.init(),
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
|
||||
@@ -3,10 +3,11 @@ import 'dart:io' as io;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:bip39/bip39.dart' as bip39;
|
||||
import 'package:convert/convert.dart';
|
||||
import 'package:ente_auth/core/constants.dart';
|
||||
// import 'package:ente_auth/core/error-reporting/super_logging.dart';
|
||||
import 'package:ente_auth/core/errors.dart';
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
// ignore: import_of_legacy_library_into_null_safe
|
||||
import 'package:ente_auth/events/signed_in_event.dart';
|
||||
import 'package:ente_auth/events/signed_out_event.dart';
|
||||
import 'package:ente_auth/models/key_attributes.dart';
|
||||
@@ -14,13 +15,10 @@ import 'package:ente_auth/models/key_gen_result.dart';
|
||||
import 'package:ente_auth/models/private_key_attributes.dart';
|
||||
import 'package:ente_auth/store/authenticator_db.dart';
|
||||
import 'package:ente_auth/utils/crypto_util.dart';
|
||||
// import 'package:ente_auth/utils/validator_util.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class Configuration {
|
||||
Configuration._privateConstructor();
|
||||
@@ -43,7 +41,6 @@ class Configuration {
|
||||
static const userIDKey = "user_id";
|
||||
static const hasMigratedSecureStorageToFirstUnlockKey =
|
||||
"has_migrated_secure_storage_to_first_unlock";
|
||||
static const anonymousUserIDKey = "anonymous_user_id";
|
||||
|
||||
final kTempFolderDeletionTimeBuffer = const Duration(days: 1).inMicroseconds;
|
||||
|
||||
@@ -120,16 +117,17 @@ class Configuration {
|
||||
}
|
||||
await _migrateSecurityStorageToFirstUnlock();
|
||||
}
|
||||
// SuperLogging.setUserID(await _getOrCreateAnonymousUserID());
|
||||
}
|
||||
|
||||
Future<void> logout({bool autoLogout = false}) async {
|
||||
_logger.info("Logging out");
|
||||
await _preferences.clear();
|
||||
await _secureStorage.deleteAll(iOptions: _secureStorageOptionsIOS);
|
||||
await AuthenticatorDB.instance.clearTable();
|
||||
_key = null;
|
||||
_cachedToken = null;
|
||||
_secretKey = null;
|
||||
_authSecretKey = null;
|
||||
Bus.instance.fire(SignedOutEvent());
|
||||
}
|
||||
|
||||
@@ -141,8 +139,9 @@ class Configuration {
|
||||
final recoveryKey = CryptoUtil.generateKey();
|
||||
|
||||
// Encrypt master key and recovery key with each other
|
||||
final encryptedMasterKey = CryptoUtil.encryptSync(masterKey, recoveryKey);
|
||||
final encryptedRecoveryKey = CryptoUtil.encryptSync(recoveryKey, masterKey);
|
||||
final encryptedMasterKey = await CryptoUtil.encrypt(masterKey, recoveryKey);
|
||||
final encryptedRecoveryKey =
|
||||
await CryptoUtil.encrypt(recoveryKey, masterKey);
|
||||
|
||||
// Derive a key from the password that will be used to encrypt and
|
||||
// decrypt the master key
|
||||
@@ -154,31 +153,31 @@ class Configuration {
|
||||
|
||||
// Encrypt the key with this derived key
|
||||
final encryptedKeyData =
|
||||
CryptoUtil.encryptSync(masterKey, derivedKeyResult.key);
|
||||
await CryptoUtil.encrypt(masterKey, derivedKeyResult.key);
|
||||
|
||||
// Generate a public-private keypair and encrypt the latter
|
||||
final keyPair = await CryptoUtil.generateKeyPair();
|
||||
final encryptedSecretKeyData =
|
||||
CryptoUtil.encryptSync(keyPair.sk, masterKey);
|
||||
await CryptoUtil.encrypt(keyPair.secretKey.extractBytes(), masterKey);
|
||||
|
||||
final attributes = KeyAttributes(
|
||||
Sodium.bin2base64(kekSalt),
|
||||
Sodium.bin2base64(encryptedKeyData.encryptedData!),
|
||||
Sodium.bin2base64(encryptedKeyData.nonce!),
|
||||
Sodium.bin2base64(keyPair.pk),
|
||||
Sodium.bin2base64(encryptedSecretKeyData.encryptedData!),
|
||||
Sodium.bin2base64(encryptedSecretKeyData.nonce!),
|
||||
base64Encode(kekSalt),
|
||||
base64Encode(encryptedKeyData.encryptedData!),
|
||||
base64Encode(encryptedKeyData.nonce!),
|
||||
base64Encode(keyPair.publicKey),
|
||||
base64Encode(encryptedSecretKeyData.encryptedData!),
|
||||
base64Encode(encryptedSecretKeyData.nonce!),
|
||||
derivedKeyResult.memLimit,
|
||||
derivedKeyResult.opsLimit,
|
||||
Sodium.bin2base64(encryptedMasterKey.encryptedData!),
|
||||
Sodium.bin2base64(encryptedMasterKey.nonce!),
|
||||
Sodium.bin2base64(encryptedRecoveryKey.encryptedData!),
|
||||
Sodium.bin2base64(encryptedRecoveryKey.nonce!),
|
||||
base64Encode(encryptedMasterKey.encryptedData!),
|
||||
base64Encode(encryptedMasterKey.nonce!),
|
||||
base64Encode(encryptedRecoveryKey.encryptedData!),
|
||||
base64Encode(encryptedRecoveryKey.nonce!),
|
||||
);
|
||||
final privateAttributes = PrivateKeyAttributes(
|
||||
Sodium.bin2base64(masterKey),
|
||||
Sodium.bin2hex(recoveryKey),
|
||||
Sodium.bin2base64(keyPair.sk),
|
||||
base64Encode(masterKey),
|
||||
hex.encode(recoveryKey),
|
||||
base64Encode(keyPair.secretKey.extractBytes()),
|
||||
);
|
||||
return KeyGenResult(attributes, privateAttributes);
|
||||
}
|
||||
@@ -197,14 +196,14 @@ class Configuration {
|
||||
|
||||
// Encrypt the key with this derived key
|
||||
final encryptedKeyData =
|
||||
CryptoUtil.encryptSync(masterKey!, derivedKeyResult.key);
|
||||
await CryptoUtil.encrypt(masterKey!, derivedKeyResult.key);
|
||||
|
||||
final existingAttributes = getKeyAttributes();
|
||||
|
||||
return existingAttributes!.copyWith(
|
||||
kekSalt: Sodium.bin2base64(kekSalt),
|
||||
encryptedKey: Sodium.bin2base64(encryptedKeyData.encryptedData!),
|
||||
keyDecryptionNonce: Sodium.bin2base64(encryptedKeyData.nonce!),
|
||||
kekSalt: base64Encode(kekSalt),
|
||||
encryptedKey: base64Encode(encryptedKeyData.encryptedData!),
|
||||
keyDecryptionNonce: base64Encode(encryptedKeyData.nonce!),
|
||||
memLimit: derivedKeyResult.memLimit,
|
||||
opsLimit: derivedKeyResult.opsLimit,
|
||||
);
|
||||
@@ -223,7 +222,7 @@ class Configuration {
|
||||
_logger.info('state validation done');
|
||||
final kek = await CryptoUtil.deriveKey(
|
||||
utf8.encode(password) as Uint8List,
|
||||
Sodium.base642bin(attributes.kekSalt),
|
||||
base64.decode(attributes.kekSalt),
|
||||
attributes.memLimit,
|
||||
attributes.opsLimit,
|
||||
).onError((e, s) {
|
||||
@@ -234,33 +233,31 @@ class Configuration {
|
||||
_logger.info('user-key done');
|
||||
Uint8List key;
|
||||
try {
|
||||
key = CryptoUtil.decryptSync(
|
||||
Sodium.base642bin(attributes.encryptedKey),
|
||||
key = await CryptoUtil.decrypt(
|
||||
base64.decode(attributes.encryptedKey),
|
||||
kek,
|
||||
Sodium.base642bin(attributes.keyDecryptionNonce),
|
||||
base64.decode(attributes.keyDecryptionNonce),
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.severe('master-key failed, incorrect password?', e);
|
||||
throw Exception("Incorrect password");
|
||||
}
|
||||
_logger.info("master-key done");
|
||||
await setKey(Sodium.bin2base64(key));
|
||||
final secretKey = CryptoUtil.decryptSync(
|
||||
Sodium.base642bin(attributes.encryptedSecretKey),
|
||||
await setKey(base64Encode(key));
|
||||
final secretKey = await CryptoUtil.decrypt(
|
||||
base64.decode(attributes.encryptedSecretKey),
|
||||
key,
|
||||
Sodium.base642bin(attributes.secretKeyDecryptionNonce),
|
||||
base64.decode(attributes.secretKeyDecryptionNonce),
|
||||
);
|
||||
_logger.info("secret-key done");
|
||||
await setSecretKey(Sodium.bin2base64(secretKey));
|
||||
await setSecretKey(base64Encode(secretKey));
|
||||
final token = CryptoUtil.openSealSync(
|
||||
Sodium.base642bin(getEncryptedToken()!),
|
||||
Sodium.base642bin(attributes.publicKey),
|
||||
base64.decode(getEncryptedToken()!),
|
||||
base64.decode(attributes.publicKey),
|
||||
secretKey,
|
||||
);
|
||||
_logger.info('appToken done');
|
||||
await setToken(
|
||||
Sodium.bin2base64(token, variant: Sodium.base64VariantUrlsafe),
|
||||
);
|
||||
await setToken(base64Url.encode(token));
|
||||
}
|
||||
|
||||
Future<void> recover(String recoveryKey) async {
|
||||
@@ -277,29 +274,27 @@ class Configuration {
|
||||
Uint8List masterKey;
|
||||
try {
|
||||
masterKey = await CryptoUtil.decrypt(
|
||||
Sodium.base642bin(attributes!.masterKeyEncryptedWithRecoveryKey),
|
||||
Sodium.hex2bin(recoveryKey),
|
||||
Sodium.base642bin(attributes.masterKeyDecryptionNonce),
|
||||
base64.decode(attributes!.masterKeyEncryptedWithRecoveryKey),
|
||||
Uint8List.fromList(hex.decode(recoveryKey)),
|
||||
base64.decode(attributes.masterKeyDecryptionNonce),
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.severe(e);
|
||||
rethrow;
|
||||
}
|
||||
await setKey(Sodium.bin2base64(masterKey));
|
||||
final secretKey = CryptoUtil.decryptSync(
|
||||
Sodium.base642bin(attributes.encryptedSecretKey),
|
||||
await setKey(base64Encode(masterKey));
|
||||
final secretKey = await CryptoUtil.decrypt(
|
||||
base64.decode(attributes.encryptedSecretKey),
|
||||
masterKey,
|
||||
Sodium.base642bin(attributes.secretKeyDecryptionNonce),
|
||||
base64.decode(attributes.secretKeyDecryptionNonce),
|
||||
);
|
||||
await setSecretKey(Sodium.bin2base64(secretKey));
|
||||
await setSecretKey(base64Encode(secretKey));
|
||||
final token = CryptoUtil.openSealSync(
|
||||
Sodium.base642bin(getEncryptedToken()!),
|
||||
Sodium.base642bin(attributes.publicKey),
|
||||
base64.decode(getEncryptedToken()!),
|
||||
base64.decode(attributes.publicKey),
|
||||
secretKey,
|
||||
);
|
||||
await setToken(
|
||||
Sodium.bin2base64(token, variant: Sodium.base64VariantUrlsafe),
|
||||
);
|
||||
await setToken(base64Url.encode(token));
|
||||
}
|
||||
|
||||
String getHttpEndpoint() {
|
||||
@@ -403,23 +398,23 @@ class Configuration {
|
||||
}
|
||||
|
||||
Uint8List? getKey() {
|
||||
return _key == null ? null : Sodium.base642bin(_key!);
|
||||
return _key == null ? null : base64.decode(_key!);
|
||||
}
|
||||
|
||||
Uint8List? getSecretKey() {
|
||||
return _secretKey == null ? null : Sodium.base642bin(_secretKey!);
|
||||
return _secretKey == null ? null : base64.decode(_secretKey!);
|
||||
}
|
||||
|
||||
Uint8List? getAuthSecretKey() {
|
||||
return _authSecretKey == null ? null : Sodium.base642bin(_authSecretKey!);
|
||||
return _authSecretKey == null ? null : base64.decode(_authSecretKey!);
|
||||
}
|
||||
|
||||
Uint8List getRecoveryKey() {
|
||||
Future<Uint8List> getRecoveryKey() async {
|
||||
final keyAttributes = getKeyAttributes()!;
|
||||
return CryptoUtil.decryptSync(
|
||||
Sodium.base642bin(keyAttributes.recoveryKeyEncryptedWithMasterKey),
|
||||
getKey(),
|
||||
Sodium.base642bin(keyAttributes.recoveryKeyDecryptionNonce),
|
||||
return CryptoUtil.decrypt(
|
||||
base64.decode(keyAttributes.recoveryKeyEncryptedWithMasterKey),
|
||||
getKey()!,
|
||||
base64.decode(keyAttributes.recoveryKeyDecryptionNonce),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -494,12 +489,4 @@ class Configuration {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _getOrCreateAnonymousUserID() async {
|
||||
if (!_preferences.containsKey(anonymousUserIDKey)) {
|
||||
//ignore: prefer_const_constructors
|
||||
await _preferences.setString(anonymousUserIDKey, Uuid().v4());
|
||||
}
|
||||
return _preferences.getString(anonymousUserIDKey)!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,15 @@
|
||||
const int thumbnailSmallSize = 256;
|
||||
const int thumbnailQuality = 50;
|
||||
const int thumbnailLargeSize = 512;
|
||||
const int compressedThumbnailResolution = 1080;
|
||||
const int thumbnailDataLimit = 100 * 1024;
|
||||
const String sentryDSN =
|
||||
"https://2235e5c99219488ea93da34b9ac1cb68@sentry.ente.io/4";
|
||||
const String sentryDebugDSN =
|
||||
"https://ca5e686dd7f149d9bf94e620564cceba@sentry.ente.io/3";
|
||||
"https://ed4ddd6309b847ba8849935e26e9b648@sentry.ente.io/9";
|
||||
const String sentryTunnel = "https://sentry-reporter.ente.io";
|
||||
const String roadmapURL = "https://roadmap.ente.io";
|
||||
const int microSecondsInDay = 86400000000;
|
||||
const int android11SDKINT = 30;
|
||||
const int galleryLoadStartTime = -8000000000000000; // Wednesday, March 6, 1748
|
||||
const int galleryLoadEndTime = 9223372036854775807; // 2^63 -1
|
||||
|
||||
// used to identify which ente file are available in app cache
|
||||
// todo: 6Jun22: delete old media identifier after 3 months
|
||||
const String oldSharedMediaIdentifier = 'ente-shared://';
|
||||
const String sharedMediaIdentifier = 'ente-shared-media://';
|
||||
|
||||
const int maxLivePhotoToastCount = 2;
|
||||
const String livePhotoToastCounterKey = "show_live_photo_toast";
|
||||
|
||||
const thumbnailDiskLoadDeferDuration = Duration(milliseconds: 40);
|
||||
const thumbnailServerLoadDeferDuration = Duration(milliseconds: 80);
|
||||
|
||||
// 256 bit key maps to 24 words
|
||||
// https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#Generating_the_mnemonic
|
||||
const mnemonicKeyWordCount = 24;
|
||||
|
||||
@@ -7,13 +7,18 @@ import 'dart:collection';
|
||||
import 'dart:core';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/core/logging/tunneled_transport.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
typedef FutureOrVoidCallback = FutureOr<void> Function();
|
||||
|
||||
@@ -141,65 +146,62 @@ class SuperLogging {
|
||||
/// The current super logging configuration
|
||||
static LogConfig config;
|
||||
|
||||
static SharedPreferences _preferences;
|
||||
|
||||
static const keyShouldReportErrors = "should_report_errors";
|
||||
|
||||
static const keyAnonymousUserID = "anonymous_user_id";
|
||||
|
||||
static Future<void> main([LogConfig config]) async {
|
||||
config ??= LogConfig();
|
||||
|
||||
SuperLogging.config = config;
|
||||
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
appVersion ??= await getAppVersion();
|
||||
final isFDroidClient = await isFDroidBuild();
|
||||
if (isFDroidClient) {
|
||||
config.sentryDsn = null;
|
||||
config.tunnel = null;
|
||||
}
|
||||
_preferences = await SharedPreferences.getInstance();
|
||||
|
||||
final enable = config.enableInDebugMode || kReleaseMode;
|
||||
sentryIsEnabled = enable && config.sentryDsn != null && !isFDroidClient;
|
||||
fileIsEnabled = enable && config.logDirPath != null;
|
||||
appVersion ??= await getAppVersion();
|
||||
|
||||
final loggingEnabled = config.enableInDebugMode || kReleaseMode;
|
||||
sentryIsEnabled =
|
||||
loggingEnabled && config.sentryDsn != null && shouldReportErrors();
|
||||
fileIsEnabled = loggingEnabled && config.logDirPath != null;
|
||||
|
||||
if (fileIsEnabled) {
|
||||
await setupLogDir();
|
||||
}
|
||||
if (sentryIsEnabled) {
|
||||
setupSentry();
|
||||
}
|
||||
|
||||
Logger.root.level = Level.ALL;
|
||||
Logger.root.onRecord.listen(onLogRecord);
|
||||
|
||||
if (isFDroidClient) {
|
||||
assert(
|
||||
sentryIsEnabled == false,
|
||||
"sentry dsn should be disabled for "
|
||||
"f-droid config ${config.sentryDsn} & ${config.tunnel}",
|
||||
);
|
||||
if (sentryIsEnabled) {
|
||||
setupSentry();
|
||||
} else {
|
||||
$.info("Sentry is disabled");
|
||||
}
|
||||
|
||||
if (!enable) {
|
||||
if (!loggingEnabled) {
|
||||
$.info("detected debug mode; sentry & file logging disabled.");
|
||||
}
|
||||
if (fileIsEnabled) {
|
||||
$.info("log file for today: $logFile with prefix ${config.prefix}");
|
||||
}
|
||||
if (sentryIsEnabled) {
|
||||
$.info("sentry uploader started");
|
||||
}
|
||||
|
||||
if (config.body == null) return;
|
||||
|
||||
if (enable && sentryIsEnabled) {
|
||||
// await SentryFlutter.init(
|
||||
// (options) {
|
||||
// options.dsn = config.sentryDsn;
|
||||
// options.httpClient = http.Client();
|
||||
// if (config.tunnel != null) {
|
||||
// options.transport =
|
||||
// TunneledTransport(Uri.parse(config.tunnel), options);
|
||||
// }
|
||||
// },
|
||||
// appRunner: () => config.body(),
|
||||
// );
|
||||
if (loggingEnabled && sentryIsEnabled) {
|
||||
await SentryFlutter.init(
|
||||
(options) {
|
||||
options.dsn = config.sentryDsn;
|
||||
options.httpClient = http.Client();
|
||||
if (config.tunnel != null) {
|
||||
options.transport =
|
||||
TunneledTransport(Uri.parse(config.tunnel), options);
|
||||
}
|
||||
},
|
||||
appRunner: () => config.body(),
|
||||
);
|
||||
} else {
|
||||
await config.body();
|
||||
}
|
||||
@@ -207,21 +209,21 @@ class SuperLogging {
|
||||
|
||||
static void setUserID(String userID) async {
|
||||
if (config?.sentryDsn != null) {
|
||||
// Sentry.configureScope((scope) => scope.user = SentryUser(id: userID));
|
||||
Sentry.configureScope((scope) => scope.user = SentryUser(id: userID));
|
||||
$.info("setting sentry user ID to: $userID");
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _sendErrorToSentry(Object error, StackTrace stack) async {
|
||||
// try {
|
||||
// await Sentry.captureException(
|
||||
// error,
|
||||
// stackTrace: stack,
|
||||
// );
|
||||
// } catch (e) {
|
||||
// $.info('Sending report to sentry.io failed: $e');
|
||||
// $.info('Original error: $error');
|
||||
// }
|
||||
try {
|
||||
await Sentry.captureException(
|
||||
error,
|
||||
stackTrace: stack,
|
||||
);
|
||||
} catch (e) {
|
||||
$.info('Sending report to sentry.io failed: $e');
|
||||
$.info('Original error: $error');
|
||||
}
|
||||
}
|
||||
|
||||
static String _lastExtraLines = '';
|
||||
@@ -249,8 +251,16 @@ class SuperLogging {
|
||||
}
|
||||
|
||||
// add error to sentry queue
|
||||
if (sentryIsEnabled && rec.error != null) {
|
||||
_sendErrorToSentry(rec.error, null);
|
||||
if (sentryIsEnabled) {
|
||||
if (rec.error != null) {
|
||||
_sendErrorToSentry(rec.error, null);
|
||||
} else if (rec.level == Level.SEVERE || rec.level == Level.SHOUT) {
|
||||
if (rec.error != null) {
|
||||
_sendErrorToSentry(rec.error, null);
|
||||
} else {
|
||||
_sendErrorToSentry(rec.message, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,17 +295,19 @@ class SuperLogging {
|
||||
static bool sentryIsEnabled;
|
||||
|
||||
static Future<void> setupSentry() async {
|
||||
$.info("Setting up sentry");
|
||||
SuperLogging.setUserID(await _getOrCreateAnonymousUserID());
|
||||
await for (final error in sentryQueueControl.stream.asBroadcastStream()) {
|
||||
// try {
|
||||
// Sentry.captureException(
|
||||
// error,
|
||||
// );
|
||||
// } catch (e) {
|
||||
// $.fine(
|
||||
// "sentry upload failed; will retry after ${config.sentryRetryDelay}",
|
||||
// );
|
||||
// doSentryRetry(error);
|
||||
// }
|
||||
try {
|
||||
Sentry.captureException(
|
||||
error,
|
||||
);
|
||||
} catch (e) {
|
||||
$.fine(
|
||||
"sentry upload failed; will retry after ${config.sentryRetryDelay}",
|
||||
);
|
||||
doSentryRetry(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,6 +316,26 @@ class SuperLogging {
|
||||
sentryQueueControl.add(error);
|
||||
}
|
||||
|
||||
static bool shouldReportErrors() {
|
||||
if (_preferences.containsKey(keyShouldReportErrors)) {
|
||||
return _preferences.getBool(keyShouldReportErrors);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> setShouldReportErrors(bool value) {
|
||||
return _preferences.setBool(keyShouldReportErrors, value);
|
||||
}
|
||||
|
||||
static Future<String> _getOrCreateAnonymousUserID() async {
|
||||
if (!_preferences.containsKey(keyAnonymousUserID)) {
|
||||
//ignore: prefer_const_constructors
|
||||
await _preferences.setString(keyAnonymousUserID, Uuid().v4());
|
||||
}
|
||||
return _preferences.getString(keyAnonymousUserID);
|
||||
}
|
||||
|
||||
/// The log file currently in use.
|
||||
static File logFile;
|
||||
|
||||
@@ -372,6 +404,6 @@ class SuperLogging {
|
||||
return false;
|
||||
}
|
||||
final pkgName = (await PackageInfo.fromPlatform()).packageName;
|
||||
return pkgName.startsWith("io.ente.photos.fdroid");
|
||||
return pkgName.endsWith("fdroid");
|
||||
}
|
||||
}
|
||||
|
||||
141
lib/core/logging/tunneled_transport.dart
Normal file
@@ -0,0 +1,141 @@
|
||||
// @dart=2.9
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:sentry/sentry.dart';
|
||||
|
||||
/// A transport is in charge of sending the event to the Sentry server.
|
||||
class TunneledTransport implements Transport {
|
||||
final Uri _tunnel;
|
||||
final SentryOptions _options;
|
||||
|
||||
final Dsn _dsn;
|
||||
|
||||
_CredentialBuilder _credentialBuilder;
|
||||
|
||||
final Map<String, String> _headers;
|
||||
|
||||
factory TunneledTransport(Uri tunnel, SentryOptions options) {
|
||||
return TunneledTransport._(tunnel, options);
|
||||
}
|
||||
|
||||
TunneledTransport._(this._tunnel, this._options)
|
||||
: _dsn = Dsn.parse(_options.dsn),
|
||||
_headers = _buildHeaders(
|
||||
_options.platformChecker.isWeb,
|
||||
_options.sdk.identifier,
|
||||
) {
|
||||
_credentialBuilder = _CredentialBuilder(
|
||||
_dsn,
|
||||
_options.sdk.identifier,
|
||||
_options.clock,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SentryId> send(SentryEnvelope envelope) async {
|
||||
final streamedRequest = await _createStreamedRequest(envelope);
|
||||
final response = await _options.httpClient
|
||||
.send(streamedRequest)
|
||||
.then(Response.fromStream);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
// body guard to not log the error as it has performance impact to allocate
|
||||
// the body String.
|
||||
if (_options.debug) {
|
||||
_options.logger(
|
||||
SentryLevel.error,
|
||||
'API returned an error, statusCode = ${response.statusCode}, '
|
||||
'body = ${response.body}',
|
||||
);
|
||||
}
|
||||
return const SentryId.empty();
|
||||
} else {
|
||||
_options.logger(
|
||||
SentryLevel.debug,
|
||||
'Envelope ${envelope.header.eventId ?? "--"} was sent successfully.',
|
||||
);
|
||||
}
|
||||
|
||||
final eventId = json.decode(response.body)['id'];
|
||||
if (eventId == null) {
|
||||
return null;
|
||||
}
|
||||
return SentryId.fromId(eventId);
|
||||
}
|
||||
|
||||
Future<StreamedRequest> _createStreamedRequest(
|
||||
SentryEnvelope envelope,
|
||||
) async {
|
||||
final streamedRequest = StreamedRequest('POST', _tunnel);
|
||||
envelope
|
||||
.envelopeStream(_options)
|
||||
.listen(streamedRequest.sink.add)
|
||||
.onDone(streamedRequest.sink.close);
|
||||
|
||||
streamedRequest.headers.addAll(_credentialBuilder.configure(_headers));
|
||||
|
||||
return streamedRequest;
|
||||
}
|
||||
}
|
||||
|
||||
class _CredentialBuilder {
|
||||
final String _authHeader;
|
||||
|
||||
final ClockProvider _clock;
|
||||
|
||||
int get timestamp => _clock().millisecondsSinceEpoch;
|
||||
|
||||
_CredentialBuilder._(String authHeader, ClockProvider clock)
|
||||
: _authHeader = authHeader,
|
||||
_clock = clock;
|
||||
|
||||
factory _CredentialBuilder(
|
||||
Dsn dsn,
|
||||
String sdkIdentifier,
|
||||
ClockProvider clock,
|
||||
) {
|
||||
final authHeader = _buildAuthHeader(
|
||||
publicKey: dsn.publicKey,
|
||||
secretKey: dsn.secretKey,
|
||||
sdkIdentifier: sdkIdentifier,
|
||||
);
|
||||
|
||||
return _CredentialBuilder._(authHeader, clock);
|
||||
}
|
||||
|
||||
static String _buildAuthHeader({
|
||||
String publicKey,
|
||||
String secretKey,
|
||||
String sdkIdentifier,
|
||||
}) {
|
||||
var header = 'Sentry sentry_version=7, sentry_client=$sdkIdentifier, '
|
||||
'sentry_key=$publicKey';
|
||||
|
||||
if (secretKey != null) {
|
||||
header += ', sentry_secret=$secretKey';
|
||||
}
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
Map<String, String> configure(Map<String, String> headers) {
|
||||
return headers
|
||||
..addAll(
|
||||
<String, String>{
|
||||
'X-Sentry-Auth': '$_authHeader, sentry_timestamp=$timestamp'
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, String> _buildHeaders(bool isWeb, String sdkIdentifier) {
|
||||
final headers = {'Content-Type': 'application/x-sentry-envelope'};
|
||||
// NOTE(lejard_h) overriding user agent on VM and Flutter not sure why
|
||||
// for web it use browser user agent
|
||||
if (!isWeb) {
|
||||
headers['User-Agent'] = sdkIdentifier;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
@@ -11,13 +11,17 @@ class Network {
|
||||
late Dio _dio;
|
||||
|
||||
Future<void> init() async {
|
||||
await FkUserAgent.init();
|
||||
String userAgent = "";
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
await FkUserAgent.init();
|
||||
userAgent = FkUserAgent.userAgent ?? "";
|
||||
}
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
_dio = Dio(
|
||||
BaseOptions(
|
||||
connectTimeout: kConnectTimeout,
|
||||
headers: {
|
||||
HttpHeaders.userAgentHeader: FkUserAgent.userAgent,
|
||||
HttpHeaders.userAgentHeader: userAgent,
|
||||
'X-Client-Version': packageInfo.version,
|
||||
'X-Client-Package': packageInfo.packageName,
|
||||
},
|
||||
|
||||
@@ -224,6 +224,14 @@ extension CustomColorScheme on ColorScheme {
|
||||
Color get inverseBackgroundColor =>
|
||||
brightness != Brightness.light ? backgroundBaseLight : backgroundBaseDark;
|
||||
|
||||
Color get fabForegroundColor => brightness == Brightness.light
|
||||
? const Color.fromRGBO(255, 255, 255, 1)
|
||||
: const Color.fromRGBO(40, 40, 40, 1);
|
||||
|
||||
Color get fabBackgroundColor => brightness != Brightness.light
|
||||
? const Color.fromRGBO(255, 255, 255, 1)
|
||||
: const Color.fromRGBO(40, 40, 40, 1);
|
||||
|
||||
Color get defaultTextColor =>
|
||||
brightness == Brightness.light ? textBaseLight : textBaseDark;
|
||||
|
||||
@@ -344,6 +352,10 @@ extension CustomColorScheme on ColorScheme {
|
||||
? Colors.black.withOpacity(0.32)
|
||||
: Colors.black.withOpacity(0.64);
|
||||
|
||||
Color get codeCardBackgroundColor => brightness == Brightness.light
|
||||
? const Color.fromRGBO(246, 246, 246, 1)
|
||||
: const Color.fromRGBO(40, 40, 40, 0.6);
|
||||
|
||||
EnteTheme get enteTheme =>
|
||||
brightness == Brightness.light ? lightTheme : darkTheme;
|
||||
}
|
||||
|
||||
@@ -11,12 +11,13 @@
|
||||
"importScanQrCode": "Scan a QR Code",
|
||||
"importEnterSetupKey": "Enter a setup key",
|
||||
"importAccountPageTitle": "Enter account details",
|
||||
"accountNameHint": "Account name",
|
||||
"accountKeyHint" : "Your key",
|
||||
"codeIssuerHint": "Issuer",
|
||||
"codeSecretKeyHint" : "Secret Key",
|
||||
"codeAccountHint": "Account (you@domain.com)",
|
||||
"accountKeyType": "Type of key",
|
||||
"timeBasedKeyType": "Time based (TOTP)",
|
||||
"counterBasedKeyType": "Counter based (HOTP)",
|
||||
"importAddAction": "Add",
|
||||
"saveAction": "Save",
|
||||
|
||||
"existingUser": "Existing User",
|
||||
"newUser" : "New to ente"
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
// @dart=2.9
|
||||
import 'dart:io';
|
||||
|
||||
import "package:ente_auth/app/view/app.dart";
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/core/constants.dart';
|
||||
import 'package:ente_auth/core/logging/super_logging.dart';
|
||||
import 'package:ente_auth/core/network.dart';
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/services/authenticator_service.dart';
|
||||
import 'package:ente_auth/services/billing_service.dart';
|
||||
import 'package:ente_auth/services/notification_service.dart';
|
||||
import 'package:ente_auth/services/preference_service.dart';
|
||||
import 'package:ente_auth/services/update_service.dart';
|
||||
import 'package:ente_auth/services/user_remote_flag_service.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
@@ -14,14 +19,19 @@ import 'package:ente_auth/ui/tools/app_lock.dart';
|
||||
import 'package:ente_auth/ui/tools/lock_screen.dart';
|
||||
import 'package:ente_auth/utils/crypto_util.dart';
|
||||
import "package:flutter/material.dart";
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:window_size/window_size.dart';
|
||||
|
||||
final _logger = Logger("main");
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
|
||||
setWindowTitle("ente Auth");
|
||||
setWindowMinSize(const Size(375, 750));
|
||||
setWindowMaxSize(const Size(375, 750));
|
||||
}
|
||||
await _runInForeground();
|
||||
}
|
||||
|
||||
@@ -29,6 +39,7 @@ Future<void> _runInForeground() async {
|
||||
return await _runWithLogs(() async {
|
||||
_logger.info("Starting app in foreground");
|
||||
await _init(false, via: 'mainMethod');
|
||||
UpdateService.instance.showUpdateNotification();
|
||||
runApp(
|
||||
AppLock(
|
||||
builder: (args) => const App(),
|
||||
@@ -47,6 +58,7 @@ Future _runWithLogs(Function() function, {String prefix = ""}) async {
|
||||
body: function,
|
||||
logDirPath: (await getApplicationSupportDirectory()).path + "/logs",
|
||||
maxLogFiles: 5,
|
||||
sentryDsn: sentryDSN,
|
||||
enableInDebugMode: true,
|
||||
prefix: prefix,
|
||||
),
|
||||
@@ -54,14 +66,15 @@ Future _runWithLogs(Function() function, {String prefix = ""}) async {
|
||||
}
|
||||
|
||||
Future<void> _init(bool bool, {String via}) async {
|
||||
InAppPurchaseConnection.enablePendingPurchases();
|
||||
CryptoUtil.init();
|
||||
await CryptoUtil.init();
|
||||
await PreferenceService.instance.init();
|
||||
await CodeStore.instance.init();
|
||||
await Configuration.instance.init();
|
||||
await Network.instance.init();
|
||||
await UserService.instance.init();
|
||||
await UserRemoteFlagService.instance.init();
|
||||
await UpdateService.instance.init();
|
||||
await AuthenticatorService.instance.init();
|
||||
await BillingService.instance.init();
|
||||
await NotificationService.instance.init();
|
||||
await UpdateService.instance.init();
|
||||
}
|
||||
|
||||
7
lib/models/authenticator/entity_result.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
class EntityResult {
|
||||
final int generatedID;
|
||||
final String rawData;
|
||||
final bool hasSynced;
|
||||
|
||||
EntityResult(this.generatedID, this.rawData, this.hasSynced);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ class Code {
|
||||
static const defaultDigits = 6;
|
||||
static const defaultPeriod = 30;
|
||||
|
||||
int? id;
|
||||
int? generatedID;
|
||||
final String account;
|
||||
final String issuer;
|
||||
final int digits;
|
||||
@@ -13,6 +13,7 @@ class Code {
|
||||
final Algorithm algorithm;
|
||||
final Type type;
|
||||
final String rawData;
|
||||
bool? hasSynced;
|
||||
|
||||
Code(
|
||||
this.account,
|
||||
@@ -23,24 +24,28 @@ class Code {
|
||||
this.algorithm,
|
||||
this.type,
|
||||
this.rawData, {
|
||||
this.id,
|
||||
this.generatedID,
|
||||
});
|
||||
|
||||
static Code fromAccountAndSecret(String account, String secret) {
|
||||
static Code fromAccountAndSecret(
|
||||
String account,
|
||||
String issuer,
|
||||
String secret,
|
||||
) {
|
||||
return Code(
|
||||
account,
|
||||
"",
|
||||
issuer,
|
||||
defaultDigits,
|
||||
defaultPeriod,
|
||||
secret,
|
||||
Algorithm.sha1,
|
||||
Type.totp,
|
||||
"otpauth://totp/" +
|
||||
account +
|
||||
issuer +
|
||||
":" +
|
||||
account +
|
||||
"?algorithm=SHA1&digits=6&issuer=" +
|
||||
account +
|
||||
issuer +
|
||||
"period=30&secret=" +
|
||||
secret,
|
||||
);
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import "package:ente_auth/l10n/l10n.dart";
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
// ignore: import_of_legacy_library_into_null_safe
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/totp_util.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class SetupEnterSecretKeyPage extends StatefulWidget {
|
||||
SetupEnterSecretKeyPage({Key? key}) : super(key: key);
|
||||
final Code? code;
|
||||
|
||||
SetupEnterSecretKeyPage({this.code, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<SetupEnterSecretKeyPage> createState() =>
|
||||
@@ -14,9 +16,27 @@ class SetupEnterSecretKeyPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
final _accountController = TextEditingController();
|
||||
final _secretController =
|
||||
TextEditingController(text: kDebugMode ? "JBSWY3DPEHPK3PXP" : "");
|
||||
late TextEditingController _issuerController;
|
||||
late TextEditingController _accountController;
|
||||
late TextEditingController _secretController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_issuerController = TextEditingController(
|
||||
text: widget.code != null
|
||||
? Uri.decodeFull(widget.code!.issuer).trim()
|
||||
: null,
|
||||
);
|
||||
_accountController = TextEditingController(
|
||||
text: widget.code != null
|
||||
? Uri.decodeFull(widget.code!.account).trim()
|
||||
: null,
|
||||
);
|
||||
_secretController = TextEditingController(
|
||||
text: widget.code != null ? widget.code!.secret : null,
|
||||
);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -40,9 +60,9 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
return null;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.accountNameHint,
|
||||
hintText: l10n.codeIssuerHint,
|
||||
),
|
||||
controller: _accountController,
|
||||
controller: _issuerController,
|
||||
autofocus: true,
|
||||
),
|
||||
const SizedBox(
|
||||
@@ -57,10 +77,26 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
return null;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.accountKeyHint,
|
||||
hintText: l10n.codeSecretKeyHint,
|
||||
),
|
||||
controller: _secretController,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
TextFormField(
|
||||
// The validator receives the text that the user has entered.
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Please enter some text";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.codeAccountHint,
|
||||
),
|
||||
controller: _accountController,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 40,
|
||||
),
|
||||
@@ -76,10 +112,14 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
try {
|
||||
final code = Code.fromAccountAndSecret(
|
||||
_accountController.text.trim(),
|
||||
_issuerController.text.trim(),
|
||||
_secretController.text.trim(),
|
||||
);
|
||||
// Verify the validity of the code
|
||||
getTotp(code);
|
||||
if (widget.code != null) {
|
||||
code.generatedID = widget.code!.generatedID;
|
||||
}
|
||||
Navigator.of(context).pop(code);
|
||||
} catch (e) {
|
||||
_showIncorrectDetailsDialog(context);
|
||||
@@ -90,7 +130,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
horizontal: 16.0,
|
||||
vertical: 4,
|
||||
),
|
||||
child: Text(l10n.importAddAction),
|
||||
child: Text(l10n.saveAction),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -11,11 +11,11 @@ import 'package:ente_auth/events/signed_in_event.dart';
|
||||
import 'package:ente_auth/gateway/authenticator.dart';
|
||||
import 'package:ente_auth/models/authenticator/auth_entity.dart';
|
||||
import 'package:ente_auth/models/authenticator/auth_key.dart';
|
||||
import 'package:ente_auth/models/authenticator/entity_result.dart';
|
||||
import 'package:ente_auth/models/authenticator/local_auth_entity.dart';
|
||||
import 'package:ente_auth/store/authenticator_db.dart';
|
||||
import 'package:ente_auth/utils/crypto_util.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
@@ -44,22 +44,33 @@ class AuthenticatorService {
|
||||
});
|
||||
}
|
||||
|
||||
Future<Map<int, String>> getAllIDtoStringMap() async {
|
||||
Future<List<EntityResult>> getEntities() async {
|
||||
final List<LocalAuthEntity> result = await _db.getAll();
|
||||
final Map<int, String> entries = <int, String>{};
|
||||
final List<EntityResult> entities = [];
|
||||
if (result.isEmpty) {
|
||||
return entries;
|
||||
return entities;
|
||||
}
|
||||
final key = await getOrCreateAuthDataKey();
|
||||
for (LocalAuthEntity e in result) {
|
||||
final decryptedValue = await CryptoUtil.decryptChaCha(
|
||||
Sodium.base642bin(e.encryptedData),
|
||||
key,
|
||||
Sodium.base642bin(e.header),
|
||||
);
|
||||
entries[e.generatedID] = utf8.decode(decryptedValue);
|
||||
try {
|
||||
final decryptedValue = await CryptoUtil.decryptChaCha(
|
||||
base64.decode(e.encryptedData),
|
||||
key,
|
||||
base64.decode(e.header),
|
||||
);
|
||||
final hasSynced = !(e.id == null || e.shouldSync);
|
||||
entities.add(
|
||||
EntityResult(
|
||||
e.generatedID,
|
||||
utf8.decode(decryptedValue),
|
||||
hasSynced,
|
||||
),
|
||||
);
|
||||
} catch (e, s) {
|
||||
_logger.severe(e, s);
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
return entities;
|
||||
}
|
||||
|
||||
Future<int> addEntry(String plainText, bool shouldSync) async {
|
||||
@@ -68,8 +79,8 @@ class AuthenticatorService {
|
||||
utf8.encode(plainText) as Uint8List,
|
||||
key,
|
||||
);
|
||||
String encryptedData = Sodium.bin2base64(encryptedKeyData.encryptedData!);
|
||||
String header = Sodium.bin2base64(encryptedKeyData.header!);
|
||||
String encryptedData = base64Encode(encryptedKeyData.encryptedData!);
|
||||
String header = base64Encode(encryptedKeyData.header!);
|
||||
final insertedID = await _db.insert(encryptedData, header);
|
||||
if (shouldSync) {
|
||||
unawaited(sync());
|
||||
@@ -77,21 +88,27 @@ class AuthenticatorService {
|
||||
return insertedID;
|
||||
}
|
||||
|
||||
Future<void> updateEntry(int generatedID, String plainText) async {
|
||||
Future<void> updateEntry(
|
||||
int generatedID,
|
||||
String plainText,
|
||||
bool shouldSync,
|
||||
) async {
|
||||
var key = await getOrCreateAuthDataKey();
|
||||
final encryptedKeyData = await CryptoUtil.encryptChaCha(
|
||||
utf8.encode(plainText) as Uint8List,
|
||||
key,
|
||||
);
|
||||
String encryptedData = Sodium.bin2base64(encryptedKeyData.encryptedData!);
|
||||
String header = Sodium.bin2base64(encryptedKeyData.header!);
|
||||
String encryptedData = base64Encode(encryptedKeyData.encryptedData!);
|
||||
String header = base64Encode(encryptedKeyData.header!);
|
||||
final int affectedRows =
|
||||
await _db.updateEntry(generatedID, encryptedData, header);
|
||||
assert(
|
||||
affectedRows == 1,
|
||||
"updateEntry should have updated exactly one row",
|
||||
);
|
||||
unawaited(sync());
|
||||
if (shouldSync) {
|
||||
unawaited(sync());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteEntry(int genID) async {
|
||||
@@ -109,15 +126,9 @@ class AuthenticatorService {
|
||||
Future<void> sync() async {
|
||||
try {
|
||||
_logger.info("Sync");
|
||||
_logger.info("State of DB before sync");
|
||||
await _printDBState();
|
||||
await _remoteToLocalSync();
|
||||
_logger.info("remote fetch completed");
|
||||
_logger.info("State of DB after remoteToLocal sync");
|
||||
await _printDBState();
|
||||
await _localToRemoteSync();
|
||||
_logger.info("State of DB after localToRemote sync");
|
||||
await _printDBState();
|
||||
_logger.info("local push completed");
|
||||
Bus.instance.fire(CodesUpdatedEvent());
|
||||
} catch (e) {
|
||||
@@ -128,24 +139,27 @@ class AuthenticatorService {
|
||||
Future<void> _remoteToLocalSync() async {
|
||||
_logger.info('Initiating remote to local sync');
|
||||
final int lastSyncTime = _prefs.getInt(_lastEntitySyncTime) ?? 0;
|
||||
_logger.info("Current synctime is " + lastSyncTime.toString());
|
||||
const int fetchLimit = 500;
|
||||
final List<AuthEntity> result =
|
||||
await _gateway.getDiff(lastSyncTime, limit: fetchLimit);
|
||||
_logger.info(result.length.toString() + " entries fetched from remote");
|
||||
if (result.isEmpty) {
|
||||
return;
|
||||
} else {
|
||||
_logger.info(result.length.toString() + " entries fetched from remote");
|
||||
}
|
||||
final maxSyncTime = result.map((e) => e.updatedAt).reduce(max);
|
||||
List<String> deletedIDs =
|
||||
result.where((element) => element.isDeleted).map((e) => e.id).toList();
|
||||
_logger.info(deletedIDs.length.toString() + " entries deleted");
|
||||
result.removeWhere((element) => element.isDeleted);
|
||||
await _db.insertOrReplace(result);
|
||||
if (deletedIDs.isNotEmpty) {
|
||||
await _db.deleteByIDs(ids: deletedIDs);
|
||||
}
|
||||
_prefs.setInt(_lastEntitySyncTime, maxSyncTime);
|
||||
_logger.info("Setting synctime to " + maxSyncTime.toString());
|
||||
if (result.length == fetchLimit) {
|
||||
_logger.info("Diff limit reached, pulling again");
|
||||
await _remoteToLocalSync();
|
||||
}
|
||||
}
|
||||
@@ -180,6 +194,10 @@ class AuthenticatorService {
|
||||
await _db.updateLocalEntity(entity.copyWith(shouldSync: false));
|
||||
}
|
||||
}
|
||||
if (pendingUpdate.isNotEmpty) {
|
||||
_logger.info("Initiating remote sync since local entries were pushed");
|
||||
await _remoteToLocalSync();
|
||||
}
|
||||
}
|
||||
|
||||
Future<Uint8List> getOrCreateAuthDataKey() async {
|
||||
@@ -188,35 +206,26 @@ class AuthenticatorService {
|
||||
}
|
||||
try {
|
||||
final AuthKey response = await _gateway.getKey();
|
||||
final authKey = CryptoUtil.decryptSync(
|
||||
Sodium.base642bin(response.encryptedKey),
|
||||
_config.getKey(),
|
||||
Sodium.base642bin(response.header),
|
||||
final authKey = await CryptoUtil.decrypt(
|
||||
base64.decode(response.encryptedKey),
|
||||
_config.getKey()!,
|
||||
base64.decode(response.header),
|
||||
);
|
||||
await _config.setAuthSecretKey(Sodium.bin2base64(authKey));
|
||||
await _config.setAuthSecretKey(base64Encode(authKey));
|
||||
return authKey;
|
||||
} on AuthenticatorKeyNotFound catch (e) {
|
||||
_logger.info("AuthenticatorKeyNotFound generating key ${e.stackTrace}");
|
||||
final key = CryptoUtil.generateKey();
|
||||
final encryptedKeyData = CryptoUtil.encryptSync(key, _config.getKey()!);
|
||||
final encryptedKeyData = await CryptoUtil.encrypt(key, _config.getKey()!);
|
||||
await _gateway.createKey(
|
||||
Sodium.bin2base64(encryptedKeyData.encryptedData!),
|
||||
Sodium.bin2base64(encryptedKeyData.nonce!),
|
||||
base64Encode(encryptedKeyData.encryptedData!),
|
||||
base64Encode(encryptedKeyData.nonce!),
|
||||
);
|
||||
await _config.setAuthSecretKey(Sodium.bin2base64(key));
|
||||
await _config.setAuthSecretKey(base64Encode(key));
|
||||
return key;
|
||||
} catch (e, s) {
|
||||
_logger.severe("Failed to getOrCreateAuthDataKey", e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _printDBState() async {
|
||||
_logger.info("_____");
|
||||
final entities = await _db.getAll();
|
||||
for (final entity in entities) {
|
||||
_logger.info(entity.id);
|
||||
}
|
||||
_logger.info("_____");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import 'package:ente_auth/core/errors.dart';
|
||||
import 'package:ente_auth/core/network.dart';
|
||||
import 'package:ente_auth/models/billing_plan.dart';
|
||||
import 'package:ente_auth/models/subscription.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
const kWebPaymentRedirectUrl = "https://payments.ente.io/frameRedirect";
|
||||
@@ -36,33 +35,6 @@ class BillingService {
|
||||
Future<BillingPlans> _future;
|
||||
|
||||
Future<void> init() async {
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
InAppPurchaseConnection.enablePendingPurchases();
|
||||
// if (Platform.isIOS && kDebugMode) {
|
||||
// await FlutterInappPurchase.instance.initConnection;
|
||||
// FlutterInappPurchase.instance.clearTransactionIOS();
|
||||
// }
|
||||
InAppPurchaseConnection.instance.purchaseUpdatedStream
|
||||
.listen((purchases) {
|
||||
if (_isOnSubscriptionPage) {
|
||||
return;
|
||||
}
|
||||
for (final purchase in purchases) {
|
||||
if (purchase.status == PurchaseStatus.purchased) {
|
||||
verifySubscription(
|
||||
purchase.productID,
|
||||
purchase.verificationData.serverVerificationData,
|
||||
).then((response) {
|
||||
if (response != null) {
|
||||
InAppPurchaseConnection.instance.completePurchase(purchase);
|
||||
}
|
||||
});
|
||||
} else if (Platform.isIOS && purchase.pendingCompletePurchase) {
|
||||
InAppPurchaseConnection.instance.completePurchase(purchase);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void clearCache() {
|
||||
|
||||
@@ -65,6 +65,10 @@ class LocalAuthenticationService {
|
||||
}
|
||||
|
||||
Future<bool> _isLocalAuthSupportedOnDevice() async {
|
||||
return await LocalAuthentication().isDeviceSupported();
|
||||
try {
|
||||
return await LocalAuthentication().isDeviceSupported();
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +1,56 @@
|
||||
// import 'dart:io';
|
||||
//
|
||||
// import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
//
|
||||
// class NotificationService {
|
||||
// static final NotificationService instance =
|
||||
// NotificationService._privateConstructor();
|
||||
//
|
||||
// NotificationService._privateConstructor();
|
||||
// final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin =
|
||||
// FlutterLocalNotificationsPlugin();
|
||||
//
|
||||
// Future<void> init() async {
|
||||
// if (!Platform.isAndroid) {
|
||||
// return;
|
||||
// }
|
||||
// const AndroidInitializationSettings initializationSettingsAndroid =
|
||||
// AndroidInitializationSettings('notification_icon');
|
||||
// const InitializationSettings initializationSettings =
|
||||
// InitializationSettings(
|
||||
// android: initializationSettingsAndroid,
|
||||
// );
|
||||
// await _flutterLocalNotificationsPlugin.initialize(
|
||||
// initializationSettings,
|
||||
// onSelectNotification: selectNotification,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// Future selectNotification(String? payload) async {}
|
||||
//
|
||||
// Future<void> showNotification(String title, String message) async {
|
||||
// if (!Platform.isAndroid) {
|
||||
// return;
|
||||
// }
|
||||
// const AndroidNotificationDetails androidPlatformChannelSpecifics =
|
||||
// AndroidNotificationDetails(
|
||||
// 'io.ente.photos',
|
||||
// 'ente',
|
||||
// channelDescription: 'ente alerts',
|
||||
// importance: Importance.max,
|
||||
// priority: Priority.high,
|
||||
// showWhen: false,
|
||||
// );
|
||||
// const NotificationDetails platformChannelSpecifics =
|
||||
// NotificationDetails(android: androidPlatformChannelSpecifics);
|
||||
// await _flutterLocalNotificationsPlugin.show(
|
||||
// 0,
|
||||
// title,
|
||||
// message,
|
||||
// platformChannelSpecifics,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
|
||||
class NotificationService {
|
||||
static final NotificationService instance =
|
||||
NotificationService._privateConstructor();
|
||||
|
||||
NotificationService._privateConstructor();
|
||||
final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
Future<void> init() async {
|
||||
if (!Platform.isAndroid) {
|
||||
return;
|
||||
}
|
||||
const AndroidInitializationSettings initializationSettingsAndroid =
|
||||
AndroidInitializationSettings('notification_icon');
|
||||
const InitializationSettings initializationSettings =
|
||||
InitializationSettings(
|
||||
android: initializationSettingsAndroid,
|
||||
);
|
||||
await _flutterLocalNotificationsPlugin.initialize(
|
||||
initializationSettings,
|
||||
);
|
||||
final implementation =
|
||||
_flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>();
|
||||
if (implementation != null) {
|
||||
implementation.requestPermission();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showNotification(String title, String message) async {
|
||||
if (!Platform.isAndroid) {
|
||||
return;
|
||||
}
|
||||
const AndroidNotificationDetails androidPlatformChannelSpecifics =
|
||||
AndroidNotificationDetails(
|
||||
'io.ente.auth',
|
||||
'auth',
|
||||
channelDescription: 'auth alerts',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
showWhen: false,
|
||||
);
|
||||
const NotificationDetails platformChannelSpecifics =
|
||||
NotificationDetails(android: androidPlatformChannelSpecifics);
|
||||
await _flutterLocalNotificationsPlugin.show(
|
||||
0,
|
||||
title,
|
||||
message,
|
||||
platformChannelSpecifics,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
27
lib/services/preference_service.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class PreferenceService {
|
||||
PreferenceService._privateConstructor();
|
||||
static final PreferenceService instance =
|
||||
PreferenceService._privateConstructor();
|
||||
|
||||
late final SharedPreferences _prefs;
|
||||
|
||||
static const kHasShownCoachMarkKey = "has_shown_coach_mark";
|
||||
|
||||
Future<void> init() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
}
|
||||
|
||||
bool hasShownCoachMark() {
|
||||
if (_prefs.containsKey(kHasShownCoachMarkKey)) {
|
||||
return _prefs.getBool(kHasShownCoachMarkKey)!;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setHasShownCoachMark(bool value) {
|
||||
return _prefs.setBool(kHasShownCoachMarkKey, value);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/core/constants.dart';
|
||||
import 'package:ente_auth/core/network.dart';
|
||||
import 'package:ente_auth/services/notification_service.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
@@ -55,33 +57,33 @@ class UpdateService {
|
||||
return _latestVersion;
|
||||
}
|
||||
|
||||
// Future<void> showUpdateNotification() async {
|
||||
// if (!isIndependent()) {
|
||||
// return;
|
||||
// }
|
||||
// final shouldUpdate = await this.shouldUpdate();
|
||||
// final lastNotificationShownTime =
|
||||
// _prefs.getInt(kUpdateAvailableShownTimeKey) ?? 0;
|
||||
// final now = DateTime.now().microsecondsSinceEpoch;
|
||||
// final hasBeen3DaysSinceLastNotification =
|
||||
// (now - lastNotificationShownTime) > (3 * microSecondsInDay);
|
||||
// if (shouldUpdate &&
|
||||
// hasBeen3DaysSinceLastNotification &&
|
||||
// _latestVersion.shouldNotify) {
|
||||
// NotificationService.instance.showNotification(
|
||||
// "update available",
|
||||
// "click to install our best version yet",
|
||||
// );
|
||||
// await _prefs.setInt(kUpdateAvailableShownTimeKey, now);
|
||||
// } else {
|
||||
// _logger.info("Debouncing notification");
|
||||
// }
|
||||
// }
|
||||
Future<void> showUpdateNotification() async {
|
||||
if (!isIndependent()) {
|
||||
return;
|
||||
}
|
||||
final shouldUpdate = await this.shouldUpdate();
|
||||
final lastNotificationShownTime =
|
||||
_prefs.getInt(kUpdateAvailableShownTimeKey) ?? 0;
|
||||
final now = DateTime.now().microsecondsSinceEpoch;
|
||||
final hasBeen3DaysSinceLastNotification =
|
||||
(now - lastNotificationShownTime) > (3 * microSecondsInDay);
|
||||
if (shouldUpdate &&
|
||||
hasBeen3DaysSinceLastNotification &&
|
||||
_latestVersion.shouldNotify) {
|
||||
NotificationService.instance.showNotification(
|
||||
"Update available",
|
||||
"Click to install our best version yet",
|
||||
);
|
||||
await _prefs.setInt(kUpdateAvailableShownTimeKey, now);
|
||||
} else {
|
||||
_logger.info("Debouncing notification");
|
||||
}
|
||||
}
|
||||
|
||||
Future<LatestVersionInfo> _getLatestVersionInfo() async {
|
||||
final response = await Network.instance
|
||||
.getDio()
|
||||
.get("https://ente.io/release-info/independent.json");
|
||||
.get("https://ente.io/release-info/auth-independent.json");
|
||||
return LatestVersionInfo.fromMap(response.data["latestVersion"]);
|
||||
}
|
||||
|
||||
@@ -89,18 +91,7 @@ class UpdateService {
|
||||
if (Platform.isIOS) {
|
||||
return false;
|
||||
}
|
||||
if (!kDebugMode &&
|
||||
_packageInfo.packageName != "io.ente.auth.independent") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isIndependentFlavor() {
|
||||
if (Platform.isIOS) {
|
||||
return false;
|
||||
}
|
||||
return _packageInfo.packageName.startsWith("io.ente.auth.independent");
|
||||
return kDebugMode || _packageInfo.packageName.endsWith("independent");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
// @dart=2.9
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:bip39/bip39.dart' as bip39;
|
||||
import 'package:convert/convert.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/core/constants.dart';
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
import 'package:ente_auth/core/network.dart';
|
||||
import 'package:ente_auth/events/user_details_changed_event.dart';
|
||||
@@ -22,7 +27,6 @@ import 'package:ente_auth/utils/crypto_util.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class UserService {
|
||||
@@ -620,11 +624,19 @@ class UserService {
|
||||
await dialog.show();
|
||||
String secret;
|
||||
try {
|
||||
secret = Sodium.bin2base64(
|
||||
if (recoveryKey.contains(' ')) {
|
||||
if (recoveryKey.split(' ').length != mnemonicKeyWordCount) {
|
||||
throw AssertionError(
|
||||
'recovery code should have $mnemonicKeyWordCount words',
|
||||
);
|
||||
}
|
||||
recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
|
||||
}
|
||||
secret = base64Encode(
|
||||
await CryptoUtil.decrypt(
|
||||
Sodium.base642bin(encryptedSecret),
|
||||
Sodium.hex2bin(recoveryKey.trim()),
|
||||
Sodium.base642bin(secretDecryptionNonce),
|
||||
base64.decode(encryptedSecret),
|
||||
hex.decode(recoveryKey.trim()),
|
||||
base64.decode(secretDecryptionNonce),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
import 'package:ente_auth/events/codes_updated_event.dart';
|
||||
import 'package:ente_auth/models/authenticator/entity_result.dart';
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/services/authenticator_service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
@@ -19,15 +20,19 @@ class CodeStore {
|
||||
}
|
||||
|
||||
Future<List<Code>> getAllCodes() async {
|
||||
final Map<int, String> rawCodesMap =
|
||||
await _authenticatorService.getAllIDtoStringMap();
|
||||
final List<EntityResult> entities =
|
||||
await _authenticatorService.getEntities();
|
||||
final List<Code> codes = [];
|
||||
for (final entry in rawCodesMap.entries) {
|
||||
final decodeJson = jsonDecode(entry.value);
|
||||
for (final entity in entities) {
|
||||
final decodeJson = jsonDecode(entity.rawData);
|
||||
final code = Code.fromRawData(decodeJson);
|
||||
code.id = entry.key;
|
||||
code.generatedID = entity.generatedID;
|
||||
code.hasSynced = entity.hasSynced;
|
||||
codes.add(code);
|
||||
}
|
||||
codes.sort((c1, c2) {
|
||||
return c1.issuer.toLowerCase().compareTo(c2.issuer.toLowerCase());
|
||||
});
|
||||
return codes;
|
||||
}
|
||||
|
||||
@@ -36,21 +41,33 @@ class CodeStore {
|
||||
bool shouldSync = true,
|
||||
}) async {
|
||||
final codes = await getAllCodes();
|
||||
bool isExistingCode = false;
|
||||
for (final existingCode in codes) {
|
||||
if (existingCode == code) {
|
||||
_logger.info("Found duplicate code, skipping add");
|
||||
return;
|
||||
} else if (existingCode.generatedID == code.generatedID) {
|
||||
isExistingCode = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
code.id = await _authenticatorService.addEntry(
|
||||
jsonEncode(code.rawData),
|
||||
shouldSync,
|
||||
);
|
||||
if (isExistingCode) {
|
||||
await _authenticatorService.updateEntry(
|
||||
code.generatedID!,
|
||||
jsonEncode(code.rawData),
|
||||
shouldSync,
|
||||
);
|
||||
} else {
|
||||
code.generatedID = await _authenticatorService.addEntry(
|
||||
jsonEncode(code.rawData),
|
||||
shouldSync,
|
||||
);
|
||||
}
|
||||
Bus.instance.fire(CodesUpdatedEvent());
|
||||
}
|
||||
|
||||
Future<void> removeCode(Code code) async {
|
||||
await _authenticatorService.deleteEntry(code.id!);
|
||||
await _authenticatorService.deleteEntry(code.generatedID!);
|
||||
Bus.instance.fire(CodesUpdatedEvent());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ class _ChangeEmailDialogState extends State<ChangeEmailDialog> {
|
||||
child: const Text(
|
||||
"Verify",
|
||||
style: TextStyle(
|
||||
color: Colors.green,
|
||||
color: Colors.purple,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
|
||||
@@ -11,7 +11,6 @@ import 'package:ente_auth/ui/common/gradient_button.dart';
|
||||
import 'package:ente_auth/utils/crypto_util.dart';
|
||||
import 'package:ente_auth/utils/email_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
|
||||
class DeleteAccountPage extends StatelessWidget {
|
||||
const DeleteAccountPage({
|
||||
@@ -165,8 +164,8 @@ class DeleteAccountPage extends StatelessWidget {
|
||||
return;
|
||||
}
|
||||
final decryptChallenge = CryptoUtil.openSealSync(
|
||||
Sodium.base642bin(response.encryptedChallenge),
|
||||
Sodium.base642bin(Configuration.instance.getKeyAttributes().publicKey),
|
||||
base64.decode(response.encryptedChallenge),
|
||||
base64.decode(Configuration.instance.getKeyAttributes().publicKey),
|
||||
Configuration.instance.getSecretKey(),
|
||||
);
|
||||
final challengeResponseStr = utf8.decode(decryptChallenge);
|
||||
|
||||
@@ -208,9 +208,9 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
|
||||
}
|
||||
|
||||
List<Widget> _saveOptions(BuildContext context, String recoveryKey) {
|
||||
final List<Widget> childrens = [];
|
||||
final List<Widget> children = [];
|
||||
if (!_hasTriedToSave) {
|
||||
childrens.add(
|
||||
children.add(
|
||||
ElevatedButton(
|
||||
style: Theme.of(context).colorScheme.optionalActionButtonStyle,
|
||||
onPressed: () async {
|
||||
@@ -219,10 +219,10 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
|
||||
child: const Text('Do this later'),
|
||||
),
|
||||
);
|
||||
childrens.add(const SizedBox(height: 10));
|
||||
children.add(const SizedBox(height: 10));
|
||||
}
|
||||
|
||||
childrens.add(
|
||||
children.add(
|
||||
GradientButton(
|
||||
onTap: () async {
|
||||
await _shareRecoveryKey(recoveryKey);
|
||||
@@ -231,8 +231,8 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
|
||||
),
|
||||
);
|
||||
if (_hasTriedToSave) {
|
||||
childrens.add(const SizedBox(height: 10));
|
||||
childrens.add(
|
||||
children.add(const SizedBox(height: 10));
|
||||
children.add(
|
||||
ElevatedButton(
|
||||
child: Text(widget.doneText),
|
||||
onPressed: () async {
|
||||
@@ -241,8 +241,8 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
childrens.add(const SizedBox(height: 12));
|
||||
return childrens;
|
||||
children.add(const SizedBox(height: 12));
|
||||
return children;
|
||||
}
|
||||
|
||||
Future _shareRecoveryKey(String recoveryKey) async {
|
||||
|
||||
@@ -68,7 +68,7 @@ class _RecoveryPageState extends State<RecoveryPage> {
|
||||
);
|
||||
} catch (e) {
|
||||
await dialog.hide();
|
||||
String errMessage = 'the recovery key you entered is incorrect';
|
||||
String errMessage = 'The recovery key you entered is incorrect';
|
||||
if (e is AssertionError) {
|
||||
errMessage = '$errMessage : ${e.message}';
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:bip39/bip39.dart' as bip39;
|
||||
import 'package:convert/convert.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
@@ -16,7 +17,6 @@ import 'package:ente_auth/ui/common/gradient_button.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/navigation_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class VerifyRecoveryPage extends StatefulWidget {
|
||||
@@ -36,7 +36,7 @@ class _VerifyRecoveryPageState extends State<VerifyRecoveryPage> {
|
||||
try {
|
||||
final String inputKey = _recoveryKey.text.trim();
|
||||
final String recoveryKey =
|
||||
Sodium.bin2hex(Configuration.instance.getRecoveryKey());
|
||||
hex.encode(await Configuration.instance.getRecoveryKey());
|
||||
final String recoveryKeyWords = bip39.entropyToMnemonic(recoveryKey);
|
||||
if (inputKey == recoveryKey || inputKey == recoveryKeyWords) {
|
||||
try {
|
||||
@@ -97,7 +97,7 @@ class _VerifyRecoveryPageState extends State<VerifyRecoveryPage> {
|
||||
if (hasAuthenticated) {
|
||||
String recoveryKey;
|
||||
try {
|
||||
recoveryKey = Sodium.bin2hex(Configuration.instance.getRecoveryKey());
|
||||
recoveryKey = hex.encode(await Configuration.instance.getRecoveryKey());
|
||||
routeToPage(
|
||||
context,
|
||||
RecoveryKeyPage(
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/onboarding/view/setup_enter_secret_key_page.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import 'package:ente_auth/utils/totp_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animation_progress_bar/flutter_animation_progress_bar.dart';
|
||||
// import 'package:flutter_animation_progress_bar/flutter_animation_progress_bar.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
|
||||
class CodeWidget extends StatefulWidget {
|
||||
@@ -48,92 +49,180 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Slidable(
|
||||
key: ValueKey(widget.code.hashCode),
|
||||
endActionPane: ActionPane(
|
||||
motion: const ScrollMotion(),
|
||||
children: [
|
||||
SlidableAction(
|
||||
onPressed: _onDeletePressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
foregroundColor: const Color(0xFFFE4A49),
|
||||
icon: Icons.delete,
|
||||
label: 'Delete',
|
||||
),
|
||||
],
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
FlutterClipboard.copy(_getTotp())
|
||||
.then((value) => showToast(context, "Copied to clipboard"));
|
||||
},
|
||||
child: SizedBox(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FAProgressBar(
|
||||
currentValue: _timeRemaining / widget.code.period * 100,
|
||||
size: 4,
|
||||
animatedDuration: const Duration(milliseconds: 200),
|
||||
progressColor: Colors.orange,
|
||||
changeColorValue: 40,
|
||||
changeProgressColor: Colors.green,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: Text(
|
||||
Uri.decodeFull(widget.code.issuer),
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(left: 16, right: 16, bottom: 8, top: 8),
|
||||
child: Slidable(
|
||||
key: ValueKey(widget.code.hashCode),
|
||||
endActionPane: ActionPane(
|
||||
motion: const ScrollMotion(),
|
||||
children: [
|
||||
SlidableAction(
|
||||
onPressed: _onEditPressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.inverseBackgroundColor,
|
||||
icon: Icons.edit_outlined,
|
||||
label: 'Edit',
|
||||
padding: const EdgeInsets.only(left: 4, right: 0),
|
||||
spacing: 8,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: _onDeletePressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
foregroundColor: const Color(0xFFFE4A49),
|
||||
icon: Icons.delete,
|
||||
label: 'Delete',
|
||||
padding: const EdgeInsets.only(left: 0, right: 0),
|
||||
spacing: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(right: 10),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.codeCardBackgroundColor,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
customBorder: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
onTap: () {
|
||||
_copyToClipboard();
|
||||
},
|
||||
onLongPress: () {
|
||||
_copyToClipboard();
|
||||
},
|
||||
child: SizedBox(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FAProgressBar(
|
||||
currentValue:
|
||||
_timeRemaining / widget.code.period * 100,
|
||||
size: 4,
|
||||
animatedDuration: const Duration(milliseconds: 200),
|
||||
progressColor: Colors.orange,
|
||||
changeColorValue: 40,
|
||||
changeProgressColor: Colors.green,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
Uri.decodeFull(widget.code.issuer).trim(),
|
||||
style:
|
||||
Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
Uri.decodeFull(
|
||||
widget.code.account,
|
||||
).trim(),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.caption
|
||||
?.copyWith(
|
||||
fontSize: 12,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
widget.code.hasSynced != null &&
|
||||
widget.code.hasSynced!
|
||||
? Container()
|
||||
: const Icon(
|
||||
Icons.sync_disabled,
|
||||
size: 20,
|
||||
color: Colors.amber,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
_getTotp(),
|
||||
style: const TextStyle(fontSize: 24),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"next",
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
),
|
||||
Text(
|
||||
_getNextTotp(),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.only(right: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"next",
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
_getTotp(),
|
||||
style: const TextStyle(fontSize: 24),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_getNextTotp(),
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _copyToClipboard() {
|
||||
FlutterClipboard.copy(_getTotp()).then(
|
||||
(value) => showToast(context, "Copied to clipboard"),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onEditPressed(_) async {
|
||||
final Code? code = await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return SetupEnterSecretKeyPage(code: widget.code);
|
||||
},
|
||||
),
|
||||
);
|
||||
if (code != null) {
|
||||
CodeStore.instance.addCode(code);
|
||||
}
|
||||
}
|
||||
|
||||
void _onDeletePressed(_) {
|
||||
final AlertDialog alert = AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
@@ -195,16 +284,4 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
return "Error";
|
||||
}
|
||||
}
|
||||
|
||||
Color _getProgressColor() {
|
||||
final progress = _timeRemaining / widget.code.period;
|
||||
if (progress > 0.6) {
|
||||
return Colors.green;
|
||||
} else if (progress > 0.4) {
|
||||
return Colors.yellow;
|
||||
} else if (progress > 2) {
|
||||
return Colors.orange;
|
||||
}
|
||||
return Colors.red;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
@@ -9,6 +10,7 @@ import 'package:ente_auth/events/codes_updated_event.dart';
|
||||
import "package:ente_auth/l10n/l10n.dart";
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/onboarding/view/setup_enter_secret_key_page.dart';
|
||||
import 'package:ente_auth/services/preference_service.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/ui/code_widget.dart';
|
||||
@@ -41,7 +43,6 @@ class _HomePageState extends State<HomePage> {
|
||||
_loadCodes();
|
||||
_streamSubscription = Bus.instance.on<CodesUpdatedEvent>().listen((event) {
|
||||
_loadCodes();
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -122,7 +123,11 @@ class _HomePageState extends State<HomePage> {
|
||||
appBar: AppBar(
|
||||
title: const Text('ente Authenticator'),
|
||||
),
|
||||
floatingActionButton: !_hasLoaded || _codes.isEmpty ? null : _getFab(),
|
||||
floatingActionButton: !_hasLoaded ||
|
||||
_codes.isEmpty ||
|
||||
!PreferenceService.instance.hasShownCoachMark()
|
||||
? null
|
||||
: _getFab(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -132,12 +137,22 @@ class _HomePageState extends State<HomePage> {
|
||||
if (_codes.isEmpty) {
|
||||
return _getEmptyState();
|
||||
} else {
|
||||
return ListView.builder(
|
||||
final list = ListView.builder(
|
||||
itemBuilder: ((context, index) {
|
||||
return CodeWidget(_codes[index]);
|
||||
}),
|
||||
itemCount: _codes.length,
|
||||
);
|
||||
if (!PreferenceService.instance.hasShownCoachMark()) {
|
||||
return Stack(
|
||||
children: [
|
||||
list,
|
||||
_getCoachMarkWidget(),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return list;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return const EnteLoadingWidget();
|
||||
@@ -152,8 +167,8 @@ class _HomePageState extends State<HomePage> {
|
||||
childPadding: const EdgeInsets.all(5),
|
||||
spaceBetweenChildren: 4,
|
||||
tooltip: 'Add Code',
|
||||
foregroundColor: Theme.of(context).colorScheme.background,
|
||||
backgroundColor: Theme.of(context).colorScheme.inverseBackgroundColor,
|
||||
foregroundColor: Theme.of(context).colorScheme.fabForegroundColor,
|
||||
backgroundColor: Theme.of(context).colorScheme.fabBackgroundColor,
|
||||
overlayOpacity: 0.5,
|
||||
overlayColor: Theme.of(context).colorScheme.background,
|
||||
elevation: 8.0,
|
||||
@@ -161,16 +176,16 @@ class _HomePageState extends State<HomePage> {
|
||||
children: [
|
||||
SpeedDialChild(
|
||||
child: const Icon(Icons.qr_code),
|
||||
foregroundColor: Theme.of(context).colorScheme.background,
|
||||
backgroundColor: Theme.of(context).colorScheme.inverseBackgroundColor,
|
||||
label: 'Scan a QR Code',
|
||||
foregroundColor: Theme.of(context).colorScheme.fabForegroundColor,
|
||||
backgroundColor: Theme.of(context).colorScheme.fabBackgroundColor,
|
||||
labelWidget: const SpeedDialLabelWidget("Scan a QR Code"),
|
||||
onTap: _redirectToScannerPage,
|
||||
),
|
||||
SpeedDialChild(
|
||||
child: const Icon(Icons.keyboard),
|
||||
foregroundColor: Theme.of(context).colorScheme.background,
|
||||
backgroundColor: Theme.of(context).colorScheme.inverseBackgroundColor,
|
||||
label: 'Enter details manually',
|
||||
foregroundColor: Theme.of(context).colorScheme.fabForegroundColor,
|
||||
backgroundColor: Theme.of(context).colorScheme.fabBackgroundColor,
|
||||
labelWidget: const SpeedDialLabelWidget("Enter details manually"),
|
||||
onTap: _redirectToManualEntryPage,
|
||||
),
|
||||
],
|
||||
@@ -223,4 +238,89 @@ class _HomePageState extends State<HomePage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getCoachMarkWidget() {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
await PreferenceService.instance.setHasShownCoachMark(true);
|
||||
setState(() {});
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
color: Theme.of(context).colorScheme.background.withOpacity(0.1),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 8, sigmaY: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.swipe_left,
|
||||
size: 42,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
Text(
|
||||
"Swipe left to edit or remove codes",
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 36,
|
||||
),
|
||||
SizedBox(
|
||||
width: 160,
|
||||
child: OutlinedButton(
|
||||
onPressed: () async {
|
||||
await PreferenceService.instance
|
||||
.setHasShownCoachMark(true);
|
||||
setState(() {});
|
||||
},
|
||||
child: const Text("OK"),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SpeedDialLabelWidget extends StatelessWidget {
|
||||
final String label;
|
||||
|
||||
const SpeedDialLabelWidget(
|
||||
this.label, {
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(4),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).colorScheme.fabBackgroundColor,
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.fabForegroundColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,11 @@ class ScannerPageState extends State<ScannerPage> {
|
||||
|
||||
void _onQRViewCreated(QRViewController controller) {
|
||||
this.controller = controller;
|
||||
// h4ck to remove black screen on Android scanners: https://github.com/juliuscanute/qr_code_scanner/issues/560#issuecomment-1159611301
|
||||
if (Platform.isAndroid) {
|
||||
controller.pauseCamera();
|
||||
controller.resumeCamera();
|
||||
}
|
||||
controller.scannedDataStream.listen((scanData) {
|
||||
try {
|
||||
final code = Code.fromRawData(scanData.code!);
|
||||
|
||||
@@ -28,11 +28,6 @@ class AboutSectionWidget extends StatelessWidget {
|
||||
Widget _getSectionOptions(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
sectionOptionSpacing,
|
||||
const AboutMenuItemWidget(
|
||||
title: "FAQ",
|
||||
url: "https://ente.io/faq",
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
const AboutMenuItemWidget(
|
||||
title: "Terms",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @dart=2.9
|
||||
|
||||
import 'package:convert/convert.dart';
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/services/local_authentication_service.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
@@ -13,7 +14,6 @@ import 'package:ente_auth/ui/settings/common_settings.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/navigation_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
|
||||
class AccountSectionWidget extends StatelessWidget {
|
||||
AccountSectionWidget({Key key}) : super(key: key);
|
||||
@@ -48,7 +48,7 @@ class AccountSectionWidget extends StatelessWidget {
|
||||
String recoveryKey;
|
||||
try {
|
||||
recoveryKey =
|
||||
Sodium.bin2base64(Configuration.instance.getRecoveryKey());
|
||||
hex.encode(await Configuration.instance.getRecoveryKey());
|
||||
} catch (e) {
|
||||
showGenericErrorDialog(context);
|
||||
return;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// @dart=2.9
|
||||
|
||||
// import 'package:open_file/open_file.dart';
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/core/network.dart';
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/services/update_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
// import 'package:open_file/open_file.dart';
|
||||
|
||||
class AppUpdateDialog extends StatefulWidget {
|
||||
final LatestVersionInfo latestVersionInfo;
|
||||
@@ -25,7 +25,12 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
|
||||
changelog.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 4, 0, 4),
|
||||
child: Text("- " + log, style: Theme.of(context).textTheme.caption),
|
||||
child: Text(
|
||||
"- " + log,
|
||||
style: Theme.of(context).textTheme.caption.copyWith(
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// @dart=2.9
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/ui/settings/common_settings.dart';
|
||||
import 'package:ente_auth/ui/settings/settings_section_title.dart';
|
||||
import 'package:ente_auth/ui/settings/settings_text_item.dart';
|
||||
import 'package:expandable/expandable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
|
||||
class DebugSectionWidget extends StatelessWidget {
|
||||
const DebugSectionWidget({Key key}) : super(key: key);
|
||||
@@ -48,7 +49,7 @@ class DebugSectionWidget extends StatelessWidget {
|
||||
"Key",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(Sodium.bin2base64(Configuration.instance.getKey())),
|
||||
Text(base64Encode(Configuration.instance.getKey())),
|
||||
const Padding(padding: EdgeInsets.all(12)),
|
||||
const Text(
|
||||
"Encrypted Key",
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
// @dart=2.9
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/services/update_service.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/components/captioned_text_widget.dart';
|
||||
import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart';
|
||||
@@ -33,19 +30,6 @@ class SocialSectionWidget extends StatelessWidget {
|
||||
const SocialsMenuItemWidget("Reddit", "https://reddit.com/r/enteio"),
|
||||
sectionOptionSpacing,
|
||||
];
|
||||
if (!UpdateService.instance.isIndependent()) {
|
||||
options.addAll(
|
||||
[
|
||||
SocialsMenuItemWidget(
|
||||
"Rate us! ✨",
|
||||
Platform.isAndroid
|
||||
? "https://play.google.com/store/apps/details?id=io.ente.photos"
|
||||
: "https://apps.apple.com/in/app/ente-photos/id1542026904",
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
],
|
||||
);
|
||||
}
|
||||
return Column(children: options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
// @dart=2.9
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/core/constants.dart';
|
||||
import 'package:ente_auth/core/logging/super_logging.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/components/captioned_text_widget.dart';
|
||||
import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart';
|
||||
import 'package:ente_auth/ui/components/menu_item_widget.dart';
|
||||
import 'package:ente_auth/ui/components/toggle_switch_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/common_settings.dart';
|
||||
import 'package:ente_auth/utils/email_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SupportSectionWidget extends StatelessWidget {
|
||||
class SupportSectionWidget extends StatefulWidget {
|
||||
const SupportSectionWidget({Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<SupportSectionWidget> createState() => _SupportSectionWidgetState();
|
||||
}
|
||||
|
||||
class _SupportSectionWidgetState extends State<SupportSectionWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ExpandableMenuItemWidget(
|
||||
@@ -24,8 +29,6 @@ class SupportSectionWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _getSectionOptions(BuildContext context) {
|
||||
final String bugsEmail =
|
||||
Platform.isAndroid ? "android-bugs@ente.io" : "ios-bugs@ente.io";
|
||||
return Column(
|
||||
children: [
|
||||
sectionOptionSpacing,
|
||||
@@ -49,14 +52,27 @@ class SupportSectionWidget extends StatelessWidget {
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
await sendLogs(context, "Report bug", bugsEmail);
|
||||
await sendLogs(context, "Report bug", "auth@ente.io");
|
||||
},
|
||||
onDoubleTap: () async {
|
||||
final zipFilePath = await getZippedLogsFile(context);
|
||||
await shareLogs(context, bugsEmail, zipFilePath);
|
||||
await shareLogs(context, "auth@ente.io", zipFilePath);
|
||||
},
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: const CaptionedTextWidget(
|
||||
title: "Crash & error reporting",
|
||||
),
|
||||
trailingSwitch: ToggleSwitchWidget(
|
||||
value: SuperLogging.shouldReportErrors(),
|
||||
onChanged: (value) async {
|
||||
await SuperLogging.setShouldReportErrors(value);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ class _LockScreenState extends State<LockScreen> {
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Opacity(
|
||||
opacity: 0.2,
|
||||
opacity: 0.3,
|
||||
child: Image.asset('assets/loading_photos_background.png'),
|
||||
),
|
||||
SizedBox(
|
||||
|
||||
@@ -20,10 +20,6 @@ class TwoFactorAuthenticationPage extends StatefulWidget {
|
||||
class _TwoFactorAuthenticationPageState
|
||||
extends State<TwoFactorAuthenticationPage> {
|
||||
final _pinController = TextEditingController();
|
||||
final _pinPutDecoration = BoxDecoration(
|
||||
border: Border.all(color: const Color.fromRGBO(45, 194, 98, 1.0)),
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
);
|
||||
String _code = "";
|
||||
LifecycleEventHandler _lifecycleEventHandler;
|
||||
|
||||
@@ -62,6 +58,16 @@ class _TwoFactorAuthenticationPageState
|
||||
}
|
||||
|
||||
Widget _getBody() {
|
||||
final pinPutDecoration = BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context)
|
||||
.inputDecorationTheme
|
||||
.focusedBorder
|
||||
.borderSide
|
||||
.color,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -89,15 +95,12 @@ class _TwoFactorAuthenticationPageState
|
||||
});
|
||||
},
|
||||
controller: _pinController,
|
||||
submittedFieldDecoration: _pinPutDecoration.copyWith(
|
||||
submittedFieldDecoration: pinPutDecoration.copyWith(
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
),
|
||||
selectedFieldDecoration: _pinPutDecoration,
|
||||
followingFieldDecoration: _pinPutDecoration.copyWith(
|
||||
selectedFieldDecoration: pinPutDecoration,
|
||||
followingFieldDecoration: pinPutDecoration.copyWith(
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
border: Border.all(
|
||||
color: const Color.fromRGBO(45, 194, 98, 0.5),
|
||||
),
|
||||
),
|
||||
inputDecoration: const InputDecoration(
|
||||
focusedBorder: InputBorder.none,
|
||||
|
||||
@@ -1,183 +1,106 @@
|
||||
import 'dart:io' as io;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:computer/computer.dart';
|
||||
import 'package:ente_auth/models/derived_key_result.dart';
|
||||
import 'package:ente_auth/models/encryption_result.dart';
|
||||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:sodium_libs/sodium_libs.dart';
|
||||
|
||||
const int encryptionChunkSize = 4 * 1024 * 1024;
|
||||
final int decryptionChunkSize =
|
||||
encryptionChunkSize + Sodium.cryptoSecretstreamXchacha20poly1305Abytes;
|
||||
|
||||
Uint8List cryptoSecretboxEasy(Map<String, dynamic> args) {
|
||||
return Sodium.cryptoSecretboxEasy(args["source"], args["nonce"], args["key"]);
|
||||
Future<Uint8List> cryptoSecretboxEasy(Map<String, dynamic> args) async {
|
||||
final sodium = await SodiumInit.init();
|
||||
return sodium.crypto.secretBox
|
||||
.easy(message: args["source"], nonce: args["nonce"], key: args["key"]);
|
||||
}
|
||||
|
||||
Uint8List cryptoSecretboxOpenEasy(Map<String, dynamic> args) {
|
||||
return Sodium.cryptoSecretboxOpenEasy(
|
||||
args["cipher"],
|
||||
args["nonce"],
|
||||
args["key"],
|
||||
Future<Uint8List> cryptoSecretboxOpenEasy(Map<String, dynamic> args) async {
|
||||
final sodium = await SodiumInit.init();
|
||||
return sodium.crypto.secretBox.openEasy(
|
||||
cipherText: args["cipher"],
|
||||
nonce: args["nonce"],
|
||||
key: SecureKey.fromList(sodium, args["key"]),
|
||||
);
|
||||
}
|
||||
|
||||
Uint8List cryptoPwHash(Map<String, dynamic> args) {
|
||||
return Sodium.cryptoPwhash(
|
||||
Sodium.cryptoSecretboxKeybytes,
|
||||
args["password"],
|
||||
args["salt"],
|
||||
args["opsLimit"],
|
||||
args["memLimit"],
|
||||
Sodium.cryptoPwhashAlgDefault,
|
||||
);
|
||||
Future<Uint8List> cryptoPwHash(Map<String, dynamic> args) async {
|
||||
final sodium = await SodiumInit.init();
|
||||
Logger("CryptoUtil").info("Sodium initialized: " + sodium.version.toString());
|
||||
return CryptoUtil.sodium.crypto.pwhash
|
||||
.call(
|
||||
outLen: CryptoUtil.sodium.crypto.secretBox.keyBytes,
|
||||
password: args["password"],
|
||||
salt: args["salt"],
|
||||
opsLimit: args["opsLimit"],
|
||||
memLimit: args["memLimit"],
|
||||
)
|
||||
.extractBytes();
|
||||
}
|
||||
|
||||
Uint8List cryptoGenericHash(Map<String, dynamic> args) {
|
||||
final sourceFile = io.File(args["sourceFilePath"]);
|
||||
final sourceFileLength = sourceFile.lengthSync();
|
||||
final inputFile = sourceFile.openSync(mode: io.FileMode.read);
|
||||
final state =
|
||||
Sodium.cryptoGenerichashInit(null, Sodium.cryptoGenerichashBytesMax);
|
||||
var bytesRead = 0;
|
||||
bool isDone = false;
|
||||
while (!isDone) {
|
||||
var chunkSize = encryptionChunkSize;
|
||||
if (bytesRead + chunkSize >= sourceFileLength) {
|
||||
chunkSize = sourceFileLength - bytesRead;
|
||||
isDone = true;
|
||||
}
|
||||
final buffer = inputFile.readSync(chunkSize);
|
||||
bytesRead += chunkSize;
|
||||
Sodium.cryptoGenerichashUpdate(state, buffer);
|
||||
Future<EncryptionResult> chachaEncryptData(Map<String, dynamic> args) async {
|
||||
final sodium = await SodiumInit.init();
|
||||
|
||||
Stream<SecretStreamPlainMessage> getStream(Uint8List data) async* {
|
||||
yield SecretStreamPlainMessage(data, tag: SecretStreamMessageTag.finalPush);
|
||||
}
|
||||
inputFile.closeSync();
|
||||
return Sodium.cryptoGenerichashFinal(state, Sodium.cryptoGenerichashBytesMax);
|
||||
}
|
||||
|
||||
EncryptionResult chachaEncryptData(Map<String, dynamic> args) {
|
||||
final initPushResult =
|
||||
Sodium.cryptoSecretstreamXchacha20poly1305InitPush(args["key"]);
|
||||
final encryptedData = Sodium.cryptoSecretstreamXchacha20poly1305Push(
|
||||
initPushResult.state,
|
||||
args["source"],
|
||||
null,
|
||||
Sodium.cryptoSecretstreamXchacha20poly1305TagFinal,
|
||||
final resultStream = sodium.crypto.secretStream.pushEx(
|
||||
key: SecureKey.fromList(sodium, args["key"]),
|
||||
messageStream: getStream(args["source"]),
|
||||
);
|
||||
return EncryptionResult(
|
||||
encryptedData: encryptedData,
|
||||
header: initPushResult.header,
|
||||
);
|
||||
}
|
||||
|
||||
Future<EncryptionResult> chachaEncryptFile(Map<String, dynamic> args) async {
|
||||
final encryptionStartTime = DateTime.now().millisecondsSinceEpoch;
|
||||
final logger = Logger("ChaChaEncrypt");
|
||||
final sourceFile = io.File(args["sourceFilePath"]);
|
||||
final destinationFile = io.File(args["destinationFilePath"]);
|
||||
final sourceFileLength = await sourceFile.length();
|
||||
logger.info("Encrypting file of size " + sourceFileLength.toString());
|
||||
|
||||
final inputFile = sourceFile.openSync(mode: io.FileMode.read);
|
||||
final key = args["key"] ?? Sodium.cryptoSecretstreamXchacha20poly1305Keygen();
|
||||
final initPushResult =
|
||||
Sodium.cryptoSecretstreamXchacha20poly1305InitPush(key);
|
||||
var bytesRead = 0;
|
||||
var tag = Sodium.cryptoSecretstreamXchacha20poly1305TagMessage;
|
||||
while (tag != Sodium.cryptoSecretstreamXchacha20poly1305TagFinal) {
|
||||
var chunkSize = encryptionChunkSize;
|
||||
if (bytesRead + chunkSize >= sourceFileLength) {
|
||||
chunkSize = sourceFileLength - bytesRead;
|
||||
tag = Sodium.cryptoSecretstreamXchacha20poly1305TagFinal;
|
||||
Uint8List? header, encryptedData;
|
||||
await for (final value in resultStream) {
|
||||
if (header == null) {
|
||||
header = value.message;
|
||||
continue;
|
||||
} else {
|
||||
encryptedData = value.message;
|
||||
}
|
||||
final buffer = inputFile.readSync(chunkSize);
|
||||
bytesRead += chunkSize;
|
||||
final encryptedData = Sodium.cryptoSecretstreamXchacha20poly1305Push(
|
||||
initPushResult.state,
|
||||
buffer,
|
||||
null,
|
||||
tag,
|
||||
);
|
||||
await destinationFile.writeAsBytes(encryptedData, mode: io.FileMode.append);
|
||||
}
|
||||
inputFile.closeSync();
|
||||
|
||||
logger.info(
|
||||
"Encryption time: " +
|
||||
(DateTime.now().millisecondsSinceEpoch - encryptionStartTime)
|
||||
.toString(),
|
||||
);
|
||||
|
||||
return EncryptionResult(key: key, header: initPushResult.header);
|
||||
return EncryptionResult(encryptedData: encryptedData, header: header);
|
||||
}
|
||||
|
||||
Future<void> chachaDecryptFile(Map<String, dynamic> args) async {
|
||||
final logger = Logger("ChaChaDecrypt");
|
||||
final decryptionStartTime = DateTime.now().millisecondsSinceEpoch;
|
||||
final sourceFile = io.File(args["sourceFilePath"]);
|
||||
final destinationFile = io.File(args["destinationFilePath"]);
|
||||
final sourceFileLength = await sourceFile.length();
|
||||
logger.info("Decrypting file of size " + sourceFileLength.toString());
|
||||
Future<Uint8List> chachaDecryptData(Map<String, dynamic> args) async {
|
||||
final sodium = await SodiumInit.init();
|
||||
|
||||
final inputFile = sourceFile.openSync(mode: io.FileMode.read);
|
||||
final pullState = Sodium.cryptoSecretstreamXchacha20poly1305InitPull(
|
||||
args["header"],
|
||||
args["key"],
|
||||
);
|
||||
|
||||
var bytesRead = 0;
|
||||
var tag = Sodium.cryptoSecretstreamXchacha20poly1305TagMessage;
|
||||
while (tag != Sodium.cryptoSecretstreamXchacha20poly1305TagFinal) {
|
||||
var chunkSize = decryptionChunkSize;
|
||||
if (bytesRead + chunkSize >= sourceFileLength) {
|
||||
chunkSize = sourceFileLength - bytesRead;
|
||||
}
|
||||
final buffer = inputFile.readSync(chunkSize);
|
||||
bytesRead += chunkSize;
|
||||
final pullResult =
|
||||
Sodium.cryptoSecretstreamXchacha20poly1305Pull(pullState, buffer, null);
|
||||
await destinationFile.writeAsBytes(pullResult.m, mode: io.FileMode.append);
|
||||
tag = pullResult.tag;
|
||||
Stream<SecretStreamCipherMessage> getStream() async* {
|
||||
yield SecretStreamCipherMessage(args["header"]);
|
||||
yield SecretStreamCipherMessage(args["source"]);
|
||||
}
|
||||
inputFile.closeSync();
|
||||
|
||||
logger.info(
|
||||
"ChaCha20 Decryption time: " +
|
||||
(DateTime.now().millisecondsSinceEpoch - decryptionStartTime)
|
||||
.toString(),
|
||||
final resultStream = sodium.crypto.secretStream.pullEx(
|
||||
key: SecureKey.fromList(sodium, args["key"]),
|
||||
cipherStream: getStream(),
|
||||
);
|
||||
}
|
||||
|
||||
Uint8List chachaDecryptData(Map<String, dynamic> args) {
|
||||
final pullState = Sodium.cryptoSecretstreamXchacha20poly1305InitPull(
|
||||
args["header"],
|
||||
args["key"],
|
||||
);
|
||||
final pullResult = Sodium.cryptoSecretstreamXchacha20poly1305Pull(
|
||||
pullState,
|
||||
args["source"],
|
||||
null,
|
||||
);
|
||||
return pullResult.m;
|
||||
await for (final result in resultStream) {
|
||||
return result.message;
|
||||
}
|
||||
return Uint8List(0);
|
||||
}
|
||||
|
||||
class CryptoUtil {
|
||||
static final Computer _computer = Computer();
|
||||
static late Sodium sodium;
|
||||
|
||||
static init() {
|
||||
static init() async {
|
||||
_computer.turnOn(workersCount: 4);
|
||||
// Sodium.init();
|
||||
sodium = await SodiumInit.init();
|
||||
Logger("CryptoUtil")
|
||||
.info("Sodium initialized: " + sodium.version.toString());
|
||||
}
|
||||
|
||||
static EncryptionResult encryptSync(Uint8List source, Uint8List key) {
|
||||
final nonce = Sodium.randombytesBuf(Sodium.cryptoSecretboxNoncebytes);
|
||||
static Future<EncryptionResult> encrypt(
|
||||
Uint8List source,
|
||||
Uint8List key,
|
||||
) async {
|
||||
final nonce = sodium.randombytes.buf(sodium.crypto.secretBox.nonceBytes);
|
||||
|
||||
final args = <String, dynamic>{};
|
||||
args["source"] = source;
|
||||
args["nonce"] = nonce;
|
||||
args["key"] = key;
|
||||
final encryptedData = cryptoSecretboxEasy(args);
|
||||
final encryptedData = await cryptoSecretboxEasy(args);
|
||||
|
||||
return EncryptionResult(
|
||||
key: key,
|
||||
nonce: nonce,
|
||||
@@ -190,24 +113,12 @@ class CryptoUtil {
|
||||
Uint8List key,
|
||||
Uint8List nonce,
|
||||
) async {
|
||||
final args = <String, dynamic>{};
|
||||
args["cipher"] = cipher;
|
||||
args["nonce"] = nonce;
|
||||
args["key"] = key;
|
||||
return _computer.compute(cryptoSecretboxOpenEasy, param: args);
|
||||
}
|
||||
|
||||
static Uint8List decryptSync(
|
||||
Uint8List cipher,
|
||||
Uint8List? key,
|
||||
Uint8List nonce,
|
||||
) {
|
||||
assert(key != null, "key can not be null");
|
||||
final args = <String, dynamic>{};
|
||||
args["cipher"] = cipher;
|
||||
args["nonce"] = nonce;
|
||||
args["key"] = key;
|
||||
return cryptoSecretboxOpenEasy(args);
|
||||
final sodium = await SodiumInit.init();
|
||||
return sodium.crypto.secretBox.openEasy(
|
||||
cipherText: cipher,
|
||||
nonce: nonce,
|
||||
key: SecureKey.fromList(sodium, key),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<EncryptionResult> encryptChaCha(
|
||||
@@ -229,45 +140,19 @@ class CryptoUtil {
|
||||
args["source"] = source;
|
||||
args["key"] = key;
|
||||
args["header"] = header;
|
||||
return _computer.compute(chachaDecryptData, param: args);
|
||||
}
|
||||
|
||||
static Future<EncryptionResult> encryptFile(
|
||||
String sourceFilePath,
|
||||
String destinationFilePath, {
|
||||
Uint8List? key,
|
||||
}) {
|
||||
final args = <String, dynamic>{};
|
||||
args["sourceFilePath"] = sourceFilePath;
|
||||
args["destinationFilePath"] = destinationFilePath;
|
||||
args["key"] = key;
|
||||
return _computer.compute(chachaEncryptFile, param: args);
|
||||
}
|
||||
|
||||
static Future<void> decryptFile(
|
||||
String sourceFilePath,
|
||||
String destinationFilePath,
|
||||
Uint8List header,
|
||||
Uint8List key,
|
||||
) {
|
||||
final args = <String, dynamic>{};
|
||||
args["sourceFilePath"] = sourceFilePath;
|
||||
args["destinationFilePath"] = destinationFilePath;
|
||||
args["header"] = header;
|
||||
args["key"] = key;
|
||||
return _computer.compute(chachaDecryptFile, param: args);
|
||||
return chachaDecryptData(args);
|
||||
}
|
||||
|
||||
static Uint8List generateKey() {
|
||||
return Sodium.cryptoSecretboxKeygen();
|
||||
return sodium.crypto.secretBox.keygen().extractBytes();
|
||||
}
|
||||
|
||||
static Uint8List getSaltToDeriveKey() {
|
||||
return Sodium.randombytesBuf(Sodium.cryptoPwhashSaltbytes);
|
||||
return sodium.randombytes.buf(sodium.crypto.pwhash.saltBytes);
|
||||
}
|
||||
|
||||
static Future<KeyPair> generateKeyPair() async {
|
||||
return Sodium.cryptoBoxKeypair();
|
||||
return sodium.crypto.box.keyPair();
|
||||
}
|
||||
|
||||
static Uint8List openSealSync(
|
||||
@@ -275,11 +160,15 @@ class CryptoUtil {
|
||||
Uint8List publicKey,
|
||||
Uint8List secretKey,
|
||||
) {
|
||||
return Sodium.cryptoBoxSealOpen(input, publicKey, secretKey);
|
||||
return sodium.crypto.box.sealOpen(
|
||||
cipherText: input,
|
||||
publicKey: publicKey,
|
||||
secretKey: SecureKey.fromList(sodium, secretKey),
|
||||
);
|
||||
}
|
||||
|
||||
static Uint8List sealSync(Uint8List input, Uint8List publicKey) {
|
||||
return Sodium.cryptoBoxSeal(input, publicKey);
|
||||
return sodium.crypto.box.seal(message: input, publicKey: publicKey);
|
||||
}
|
||||
|
||||
static Future<DerivedKeyResult> deriveSensitiveKey(
|
||||
@@ -287,11 +176,11 @@ class CryptoUtil {
|
||||
Uint8List salt,
|
||||
) async {
|
||||
final logger = Logger("pwhash");
|
||||
int memLimit = Sodium.cryptoPwhashMemlimitSensitive;
|
||||
int opsLimit = Sodium.cryptoPwhashOpslimitSensitive;
|
||||
int memLimit = sodium.crypto.pwhash.memLimitSensitive;
|
||||
int opsLimit = sodium.crypto.pwhash.opsLimitSensitive;
|
||||
Uint8List key;
|
||||
while (memLimit > Sodium.cryptoPwhashMemlimitMin &&
|
||||
opsLimit < Sodium.cryptoPwhashOpslimitMax) {
|
||||
while (memLimit > sodium.crypto.pwhash.memLimitMin &&
|
||||
opsLimit < sodium.crypto.pwhash.opsLimitMax) {
|
||||
try {
|
||||
key = await deriveKey(password, salt, memLimit, opsLimit);
|
||||
return DerivedKeyResult(key, memLimit, opsLimit);
|
||||
@@ -309,24 +198,27 @@ class CryptoUtil {
|
||||
Uint8List salt,
|
||||
int memLimit,
|
||||
int opsLimit,
|
||||
) {
|
||||
return _computer.compute(
|
||||
cryptoPwHash,
|
||||
param: {
|
||||
"password": password,
|
||||
"salt": salt,
|
||||
"memLimit": memLimit,
|
||||
"opsLimit": opsLimit,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static Future<Uint8List> getHash(io.File source) {
|
||||
return _computer.compute(
|
||||
cryptoGenericHash,
|
||||
param: {
|
||||
"sourceFilePath": source.path,
|
||||
},
|
||||
);
|
||||
) async {
|
||||
final sodium = await SodiumInit.init();
|
||||
Logger("CryptoUtil")
|
||||
.info("Sodium initialized: " + sodium.version.toString());
|
||||
return sodium.crypto.pwhash
|
||||
.call(
|
||||
outLen: CryptoUtil.sodium.crypto.secretBox.keyBytes,
|
||||
password: Int8List.fromList(password),
|
||||
salt: salt,
|
||||
opsLimit: opsLimit,
|
||||
memLimit: memLimit,
|
||||
)
|
||||
.extractBytes();
|
||||
// return _computer.compute(
|
||||
// cryptoPwHash,
|
||||
// param: {
|
||||
// "password": password,
|
||||
// "salt": salt,
|
||||
// "memLimit": memLimit,
|
||||
// "opsLimit": opsLimit,
|
||||
// },
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,9 @@ Future<void> sendLogs(
|
||||
content.addAll(
|
||||
[
|
||||
const Text(
|
||||
"This will send across logs to help us debug your issue. Please note that file names will be included to help track issues with specific files.",
|
||||
"This will send across logs to help us debug your issue. "
|
||||
"While we take precautions to ensure that sensitive information is not "
|
||||
"logged, we encourage you to view these logs before sharing them.",
|
||||
style: TextStyle(
|
||||
height: 1.5,
|
||||
fontSize: 16,
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
|
||||
Future showToast(
|
||||
@@ -11,31 +8,16 @@ Future showToast(
|
||||
toastLength = Toast.LENGTH_LONG,
|
||||
iOSDismissOnTap = true,
|
||||
}) async {
|
||||
if (Platform.isAndroid) {
|
||||
await Fluttertoast.cancel();
|
||||
return Fluttertoast.showToast(
|
||||
msg: message,
|
||||
toastLength: toastLength,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
timeInSecForIosWeb: 1,
|
||||
backgroundColor: Theme.of(context).colorScheme.toastBackgroundColor,
|
||||
textColor: Theme.of(context).colorScheme.toastTextColor,
|
||||
fontSize: 16.0,
|
||||
);
|
||||
} else {
|
||||
EasyLoading.instance
|
||||
..backgroundColor = Theme.of(context).colorScheme.toastBackgroundColor
|
||||
..indicatorColor = Theme.of(context).colorScheme.toastBackgroundColor
|
||||
..textColor = Theme.of(context).colorScheme.toastTextColor
|
||||
..userInteractions = true
|
||||
..loadingStyle = EasyLoadingStyle.custom;
|
||||
return EasyLoading.showToast(
|
||||
message,
|
||||
duration: Duration(seconds: (toastLength == Toast.LENGTH_LONG ? 5 : 2)),
|
||||
toastPosition: EasyLoadingToastPosition.bottom,
|
||||
dismissOnTap: iOSDismissOnTap,
|
||||
);
|
||||
}
|
||||
await Fluttertoast.cancel();
|
||||
return Fluttertoast.showToast(
|
||||
msg: message,
|
||||
toastLength: toastLength,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
timeInSecForIosWeb: 1,
|
||||
backgroundColor: Theme.of(context).colorScheme.toastBackgroundColor,
|
||||
textColor: Theme.of(context).colorScheme.toastTextColor,
|
||||
fontSize: 16.0,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showShortToast(context, String message) {
|
||||
|
||||
@@ -7,13 +7,25 @@
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||
#include <sentry_flutter/sentry_flutter_plugin.h>
|
||||
#include <sodium_libs/sodium_libs_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
#include <window_size/window_size_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) sentry_flutter_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "SentryFlutterPlugin");
|
||||
sentry_flutter_plugin_register_with_registrar(sentry_flutter_registrar);
|
||||
g_autoptr(FlPluginRegistrar) sodium_libs_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "SodiumLibsPlugin");
|
||||
sodium_libs_plugin_register_with_registrar(sodium_libs_registrar);
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) window_size_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowSizePlugin");
|
||||
window_size_plugin_register_with_registrar(window_size_registrar);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
flutter_secure_storage_linux
|
||||
sentry_flutter
|
||||
sodium_libs
|
||||
url_launcher_linux
|
||||
window_size
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||
@@ -6,21 +6,29 @@ import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import connectivity_macos
|
||||
import flutter_local_notifications
|
||||
import flutter_secure_storage_macos
|
||||
import package_info_plus_macos
|
||||
import path_provider_macos
|
||||
import sentry_flutter
|
||||
import share_plus_macos
|
||||
import shared_preferences_macos
|
||||
import sodium_libs
|
||||
import sqflite
|
||||
import url_launcher_macos
|
||||
import window_size
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin"))
|
||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||
FlutterSecureStorageMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageMacosPlugin"))
|
||||
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SodiumLibsPlugin.register(with: registry.registrar(forPlugin: "SodiumLibsPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
WindowSizePlugin.register(with: registry.registrar(forPlugin: "WindowSizePlugin"))
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ PODS:
|
||||
- connectivity_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- Reachability
|
||||
- flutter_local_notifications (0.0.1):
|
||||
- FlutterMacOS
|
||||
- flutter_secure_storage_macos (3.3.1):
|
||||
- FlutterMacOS
|
||||
- FlutterMacOS (1.0.0)
|
||||
@@ -13,35 +15,54 @@ PODS:
|
||||
- path_provider_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- Reachability (3.2)
|
||||
- Sentry/HybridSDK (7.30.2)
|
||||
- sentry_flutter (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- Sentry/HybridSDK (= 7.30.2)
|
||||
- share_plus_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- shared_preferences_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- Sodium (0.9.1)
|
||||
- sodium_libs (1.2.0):
|
||||
- FlutterMacOS
|
||||
- Sodium
|
||||
- sqflite (0.0.2):
|
||||
- FlutterMacOS
|
||||
- FMDB (>= 2.7.5)
|
||||
- url_launcher_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- window_size (0.0.2):
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- connectivity_macos (from `Flutter/ephemeral/.symlinks/plugins/connectivity_macos/macos`)
|
||||
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
|
||||
- flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`)
|
||||
- path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`)
|
||||
- sentry_flutter (from `Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos`)
|
||||
- share_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos`)
|
||||
- shared_preferences_macos (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos`)
|
||||
- sodium_libs (from `Flutter/ephemeral/.symlinks/plugins/sodium_libs/macos`)
|
||||
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
- window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- FMDB
|
||||
- Reachability
|
||||
- Sentry
|
||||
- Sodium
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
connectivity_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/connectivity_macos/macos
|
||||
flutter_local_notifications:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
|
||||
flutter_secure_storage_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos
|
||||
FlutterMacOS:
|
||||
@@ -50,27 +71,39 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos
|
||||
path_provider_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos
|
||||
sentry_flutter:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos
|
||||
share_plus_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos
|
||||
shared_preferences_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos
|
||||
sodium_libs:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sodium_libs/macos
|
||||
sqflite:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos
|
||||
url_launcher_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||
window_size:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/window_size/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
connectivity_macos: 5dae6ee11d320fac7c05f0d08bd08fc32b5514d9
|
||||
flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4
|
||||
flutter_secure_storage_macos: 6ceee8fbc7f484553ad17f79361b556259df89aa
|
||||
FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c
|
||||
path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
|
||||
Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96
|
||||
Sentry: 9be48e341494bc976c963b05aa4a8ca48308c684
|
||||
sentry_flutter: 544e6376e35b00eef9f0864f8bb7f10a0e204993
|
||||
share_plus_macos: 853ee48e7dce06b633998ca0735d482dd671ade4
|
||||
shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727
|
||||
Sodium: 23d11554ecd556196d313cf6130d406dfe7ac6da
|
||||
sodium_libs: f6ceec5c5f48fb59fc88a8513e9d3f92a7ae92f7
|
||||
sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea
|
||||
url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3
|
||||
window_size: 339dafa0b27a95a62a843042038fa6c3c48de195
|
||||
|
||||
PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
/* Begin PBXFileReference section */
|
||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
||||
33CC10ED2044A3C60003C045 /* authenticator.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = authenticator.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
33CC10ED2044A3C60003C045 /* auth.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = auth.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
||||
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||
@@ -94,7 +94,6 @@
|
||||
4F2F733D93DB4D2D82767271 /* Pods-Runner.release.xcconfig */,
|
||||
B347CC163E4E13C897729F91 /* Pods-Runner.profile.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -123,7 +122,7 @@
|
||||
33CC10EE2044A3C60003C045 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33CC10ED2044A3C60003C045 /* authenticator.app */,
|
||||
33CC10ED2044A3C60003C045 /* auth.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -193,7 +192,7 @@
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
productReference = 33CC10ED2044A3C60003C045 /* authenticator.app */;
|
||||
productReference = 33CC10ED2044A3C60003C045 /* auth.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
@@ -419,8 +418,10 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = 6Z68YJY9Q2;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@@ -545,8 +546,10 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = 6Z68YJY9Q2;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@@ -565,8 +568,10 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = 6Z68YJY9Q2;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "authenticator.app"
|
||||
BuildableName = "auth.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
@@ -31,7 +31,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "authenticator.app"
|
||||
BuildableName = "auth.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
@@ -54,7 +54,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "authenticator.app"
|
||||
BuildableName = "auth.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
@@ -71,7 +71,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "authenticator.app"
|
||||
BuildableName = "auth.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
|
||||
@@ -6,9 +6,14 @@
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)io.ente.auth</string>
|
||||
<string>$(AppIdentifierPrefix)io.ente.photos</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)io.ente.auth</string>
|
||||
<string>$(AppIdentifierPrefix)io.ente.photos</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
113
pubspec.lock
@@ -219,12 +219,12 @@ packages:
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
convert:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: convert
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
version: "3.1.1"
|
||||
coverage:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -260,6 +260,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dbus
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.7.4"
|
||||
device_info:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -384,13 +391,6 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "8.0.1"
|
||||
flutter_easyloading:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_easyloading
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
flutter_email_sender:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -412,6 +412,27 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.9.3"
|
||||
flutter_local_notifications:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_local_notifications
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "12.0.3"
|
||||
flutter_local_notifications_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
flutter_local_notifications_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_localizations:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -480,13 +501,6 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
flutter_sodium:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_sodium
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
flutter_speed_dial:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -494,13 +508,6 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.2.0"
|
||||
flutter_spinkit:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_spinkit
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.1.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -524,7 +531,14 @@ packages:
|
||||
name: fluttertoast
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "8.0.9"
|
||||
version: "8.1.1"
|
||||
freezed_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: freezed_annotation
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -595,13 +609,6 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
in_app_purchase:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: in_app_purchase
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.2"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -924,6 +931,20 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
sentry:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sentry
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.15.1"
|
||||
sentry_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sentry_flutter
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.15.1"
|
||||
share_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1055,6 +1076,20 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
sodium:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sodium
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
sodium_libs:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sodium_libs
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.3"
|
||||
source_gen:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1174,6 +1209,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.16"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timezone
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.9.0"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1300,6 +1342,15 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.6.1"
|
||||
window_size:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "plugins/window_size"
|
||||
ref: a738913c8ce2c9f47515382d40827e794a334274
|
||||
resolved-ref: a738913c8ce2c9f47515382d40827e794a334274
|
||||
url: "https://github.com/google/flutter-desktop-embedding.git"
|
||||
source: git
|
||||
version: "0.1.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
81
pubspec.yaml
@@ -1,6 +1,6 @@
|
||||
name: ente_auth
|
||||
description: ente two-factor authenticator
|
||||
version: 1.0.7+7
|
||||
version: 1.0.21+21
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
@@ -8,64 +8,69 @@ environment:
|
||||
|
||||
dependencies:
|
||||
adaptive_theme: ^3.1.0 # done
|
||||
bloc: ^8.0.3 #done
|
||||
bip39: ^1.0.6 #done
|
||||
bloc: ^8.0.3 #done
|
||||
clipboard: ^0.1.3
|
||||
collection: # dart
|
||||
computer: ^2.0.0
|
||||
confetti: ^0.7.0
|
||||
connectivity: ^3.0.3
|
||||
convert: ^3.1.1
|
||||
cupertino_icons: ^1.0.0
|
||||
device_info: ^2.0.2
|
||||
dio: ^4.0.6
|
||||
dotted_border: ^2.0.0+2
|
||||
email_validator: ^2.0.1
|
||||
event_bus: ^2.0.0
|
||||
dio: ^4.0.6
|
||||
expandable: ^5.0.1
|
||||
expansion_tile_card: ^2.0.0
|
||||
file_picker: ^4.6.1
|
||||
fk_user_agent: ^2.1.0
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_animation_progress_bar: ^2.2.1
|
||||
flutter_bloc: ^8.0.1
|
||||
flutter_email_sender: ^5.1.0
|
||||
flutter_inappwebview: ^5.7.1
|
||||
flutter_launcher_icons: ^0.9.3
|
||||
dotted_border: ^2.0.0+2
|
||||
in_app_purchase: ^0.5.2
|
||||
flutter_secure_storage: ^6.0.0
|
||||
flutter_animation_progress_bar: ^2.2.1
|
||||
flutter_slidable: ^2.0.0
|
||||
file_picker: ^4.6.1
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_bloc: ^8.0.1
|
||||
flutter_native_splash: ^2.2.13
|
||||
local_auth: ^1.1.5
|
||||
pinput: ^1.2.2
|
||||
password_strength: ^0.2.0
|
||||
flutter_sodium: ^0.2.0
|
||||
flutter_windowmanager: ^0.2.0
|
||||
flutter_local_notifications: ^12.0.3
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
# sentry:
|
||||
# path: thirdparty/sentry-dart/dart
|
||||
# sentry_flutter:
|
||||
# path: thirdparty/sentry-dart/flutter
|
||||
json_annotation: ^4.5.0
|
||||
fluttertoast: ^8.0.6
|
||||
flutter_native_splash: ^2.2.13
|
||||
flutter_secure_storage: ^6.0.0
|
||||
flutter_slidable: ^2.0.0
|
||||
flutter_speed_dial: ^6.2.0
|
||||
flutter_windowmanager: ^0.2.0
|
||||
fluttertoast: ^8.1.1
|
||||
google_nav_bar: ^5.0.5 #supported
|
||||
http: ^0.13.4
|
||||
move_to_background: ^1.0.2
|
||||
otp: ^3.1.1
|
||||
path_provider: ^2.0.11
|
||||
intl: ^0.17.0
|
||||
qr_code_scanner: ^1.0.1
|
||||
sqflite: ^2.1.0
|
||||
share_plus: ^4.4.0
|
||||
package_info_plus: ^1.0.1
|
||||
shared_preferences: ^2.0.5
|
||||
flutter_easyloading: ^3.0.5
|
||||
uuid: ^3.0.4
|
||||
url_launcher: ^6.1.5
|
||||
json_annotation: ^4.5.0
|
||||
local_auth: ^1.1.5
|
||||
logging: ^1.0.1
|
||||
move_to_background: ^1.0.2
|
||||
# open_file: ^3.2.1 Disabled to please PlayStore overlords
|
||||
otp: ^3.1.1
|
||||
package_info_plus: ^1.0.1
|
||||
password_strength: ^0.2.0
|
||||
path_provider: ^2.0.11
|
||||
pinput: ^1.2.2
|
||||
qr_code_scanner: ^1.0.1
|
||||
sentry: ^6.12.1
|
||||
sentry_flutter: ^6.12.1
|
||||
share_plus: ^4.4.0
|
||||
shared_preferences: ^2.0.5
|
||||
sodium: ^1.2.2
|
||||
sodium_libs: ^1.2.3
|
||||
sqflite: ^2.1.0
|
||||
step_progress_indicator: ^1.0.2
|
||||
confetti: ^0.7.0
|
||||
clipboard: ^0.1.3
|
||||
flutter_speed_dial: ^6.2.0
|
||||
url_launcher: ^6.1.5
|
||||
uuid: ^3.0.4
|
||||
window_size:
|
||||
git:
|
||||
url: https://github.com/google/flutter-desktop-embedding.git
|
||||
path: plugins/window_size
|
||||
ref: a738913c8ce2c9f47515382d40827e794a334274
|
||||
|
||||
dev_dependencies:
|
||||
bloc_test: ^9.0.3
|
||||
|
||||
BIN
screenshots/screenshots.png
Normal file
|
After Width: | Height: | Size: 363 KiB |
1
web/sodium.js
Normal file
@@ -7,11 +7,20 @@
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
#include <sentry_flutter/sentry_flutter_plugin.h>
|
||||
#include <sodium_libs/sodium_libs_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
#include <window_size/window_size_plugin.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
SentryFlutterPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SentryFlutterPlugin"));
|
||||
SodiumLibsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SodiumLibsPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
WindowSizePluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("WindowSizePlugin"));
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
flutter_secure_storage_windows
|
||||
sentry_flutter
|
||||
sodium_libs
|
||||
url_launcher_windows
|
||||
window_size
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||