flowcrypt / flowcrypt-android Goto Github PK
View Code? Open in Web Editor NEWFlowCrypt Android App
Home Page: https://flowcrypt.com
License: Other
FlowCrypt Android App
Home Page: https://flowcrypt.com
License: Other
Let's add functionality to the archive + delete icons.
The Delete icon should move it to trash folder.
After clicking, it should wait on the message screen until a success or failure happens.
https://github.com/tomholub/cryptup-android-prototype/wiki/JavaScript-functions-and-specs
other useful functions:
We can retrieve a user info from GoogleSignInResult. This information can be shown on the left side panel (such as in the Gmail).
GoogleSignInResult result = Auth.GoogleSignInApi.getSignInResultFromIntent(data);
GoogleSignInAccount acct = result.getSignInAccount();
String personName = acct.getDisplayName();
String personGivenName = acct.getGivenName();
String personFamilyName = acct.getFamilyName();
String personEmail = acct.getEmail();
String personId = acct.getId();
Uri personPhoto = acct.getPhotoUrl();
After I experienced error described in #30, I removed the original email and added correct email.
But I could not send the message because the send button was missing.
https://github.com/tomholub/cryptup-android-prototype/wiki/load-private-key-from-backup
Turns out we can cheat a little (and I think we should) - Gmail has a few IMAP extensions that will make using IMAP easier. So while we focus on Gmail, it may make sense to utilize them (for example Gmail search). You can use my query to look for backups:
String query = js.api_gmail_query_backups("[email protected]");
// will return
from:tom@cryptup.org to:tom@cryptup.org (subject:"Your CryptUp Backup" OR subject: "Your CryptUP Backup" OR subject: "All you need to know about CryptUP (contains a backup)" OR subject: "CryptUP Account Backup") -is:spam
You can search this way based on https://developers.google.com/gmail/imap/imap-extensions which is great - there won't be any incompatibilities in how the app searches for backups.
The flow to test keys is as follows:
// fetch all backed up keys
// get pass phrase from user
// normalize key to prevent unnecessary formatting errors
String normalized_armored_key = js.crypto_key_normalize(raw_armored_key);
// read each key into object
PgpKey prv = js.crypto_key_read(normalized_armored_key);
// confirm that all found keys are indeed private keys
// and verify that the pass phrase matches the key and save if matches
if(prv.isPrivate() == true && js.crypto_key_decrypt(prv, passphrase).getBoolean("success") == true) {
// save key and pass phrase it in Android secret storage / keychain
saveKeyToStorage(normalized_armored_key, passphrase);
}
Just list the keys, their primary email listed inside the key (I will surface that for you) and when was the key created (that is also encoded in the key itself, I will surface that for you also)
Clicking on them won't do anything yet.
The "+" button will let user import a private key from a file, similar to when account is set up. The user has to confirm the pass phrase of that key. You can probably reuse one of the setup screens.
Before adding that key to database, double check that the longid
of the key is not already in the database. If it's already there, show error Key already added, skipping.
Add https://github.com/ACRA/acra for debug builds.
unread messages: sender and subject should be bold
read messages: both sender and subject should be standard (non bold)
Just display the text version of the message using m.getText()
. Gmail always supplies a text equivalent of a message, even when the sender only sent HTML, however you should check that m.getText() != null
just in case - that could happen if the message is empty.
For now, don't worry about the icons on the top except for "back"
I have exposed Js.mime_decode to work with raw MIME messages as follows:
mime_message.writeTo(buffer)
String mime_stream = buffer.toString(); // just guessing
// decode and get information you need
MimeMessage m = js.mime_decode(mime_stream);
System.out.println("Date:" + m.getStringHeader("date"));
System.out.println("Subject:" + m.getStringHeader("subject"));
System.out.println("From: " + m.getAddressHeader("from")[0].getAddress() + ", " + m.getAddressHeader("from")[0].getName());
for (MimeAddress a: m.getAddressHeader("to")) {
System.out.println("To: " + a.getAddress() + ", " + a.getName());
}
System.out.println(m.getText());
System.out.println(m.getHtml());
If you try this with String mime_stream = read(context.getAssets().open("sample/message.mime"));
then it will print the following
04-28 14:53:34.755 17974-17974/com.flowcrypt.email.debug I/System.out: Date:Tue, 25 Apr 2017 15:17:11 +0000
04-28 14:53:34.755 17974-17974/com.flowcrypt.email.debug I/System.out: Subject:Check this out - your first encrypted message (feel free to reply)
04-28 14:53:34.757 17974-17974/com.flowcrypt.email.debug I/System.out: From: [email protected], Tom at CryptUp
04-28 14:53:34.757 17974-17974/com.flowcrypt.email.debug I/System.out: To: [email protected],
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: The text below is encrypted. Let me know if you are having trouble reading it.
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: -----BEGIN PGP MESSAGE-----
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: Version: OpenPGP.js v2.5.3
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: Comment: http://openpgpjs.org
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: wcFMA+ADv/5v4RgKAQ//UwAu4dKYH8LfaNEvMH7E6RpVcxRQ16sTc0eWbV9w
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: O+h+okaos1JqITQCj1z0MmTAjK4TxMCNdIhY/Hh0tZVrVWBiKPKdFXTNlygx
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: /ZOEaT9U+jKCi5YU8+g2p4W7v+YNNSW1x/CWoxvxPkBFuvAE8KBH16ubWzWE
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: 745cslhxeXzniQ7FF2Q3TttOXL+QHcb8Ia1f2VRhpOTNUzCG8+LWn4RwA4xW
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: 13Esv2MNJgdr3haCUUy/WUDprpiOWFaPF6njbXkZbpizURzJIQstq+Uu7qM2
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: XgWxlhYkZIVbGA3i1FXJZSoYS9+ILV7CchQuM4hdxQgk3QyTbRLT+T4DJPaE
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: hjy/bZN5H38t2KnZoJ0is6oQPbm79D80dy5cWrrOkdLU2sJLlJlSWuEixYv0
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: fAq3PNDE5CW8RdkuCaxQxC0qSJsJWvntCsrvuDDAWJWa1S/FvLYKaU4Xj3cs
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: bRuQQmNZ5gcx8ztOK6yp+N1vs7bL2S5WBm6ecSDIomDMgHai0hBIbtCI8fYV
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: HaP6gsWgl2PA1Kk26RIaoADuEkjGaSHJWeBhh1OXoZ7SOEL5SpIdRA7eUhvs
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: 8TvOqqVDfT1e03YvTAuLS6hJBk8SgrMy26dEnTGlSlzAO+Y42A0PVfWKzhs/
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: HblfL09kiSYuCxIqOk1Ih4lx3KJ/ISiMwn8tw8C1b0vSwVMBTphUuklj0iEB
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: /YycDVUl4jK44jopHuvpJfvJTVKwoNG86ypbcYSlzQ7ElUp1EJ2Z9dMA4oM7
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: 9PioAZG33LjriutXYHfS/JMdEIIlVTxbhkrlgdgYzyf9ptgj2VKWsMuaSaUN
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: oiBViJFsUaHZmcjifqhoDXLfVZUvT8r+BSiSseNx5pRvyPnjj7A34aZ9HIh+
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: bkgOgSUVGmPH0eL5T0a88mEu0iN4ahLfxN+8LID18+mSv7d9RuJ7cor46gc4
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: zL9mESewqdnWuveeTViTCCWr7DWEMSfHL8xRhdJU3metZJCo4DTHkP5QSZo9
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: ZzFUUxgW0Q0QQKwRhVdwC5m3Ddehu8mAAHpRlNmrChyGw60lgSbzkdXePdhW
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: 7MlZpLaVfFRekXaVUbf+Kp7Q62+NoxnbndrP7AnLPxKfOWHl6az+j3leJ1w9
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: TvoQufCvFAXlOoJsDAc0rAKewGg3mvwbVwP8pVMK0rsqp2stUYFH1ZkGZJ5C
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: QF10bfVwST7/DJKhJLfzbA8AhxYr8y0y0CHFzWBbofWBd/IFh57fBfHPIcpo
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: +PPFpqA8RD//tROLgcboCHhijFamssa2yZx//V9tFsH8jo45agW8ScfxjoO2
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: vT56K5RcJnyZkFMaxqqKa6qBJ7XXL3nn2ePquNwj3zO59yVoF2AeoipJCTLE
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: C/jf1vpjTJlbneWAjUWjm8+SlfCY1V/oERo=
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: =UCCQ
04-28 14:53:34.758 17974-17974/com.flowcrypt.email.debug I/System.out: -----END PGP MESSAGE-----
04-28 14:53:34.759 17974-17974/com.flowcrypt.email.debug I/System.out: null
The flow for now will be very similar to our original prototype. There will be just one recipient, and you will look up their pubkey and then encrypt the message for that pubkey.
crypto_message_encrypt(String pubkeys[], String text, Boolean armor)
- always choose armor
as true
Now when you encrypted the text, you can build a mime message. For this, use js.mime_encode
. Here is an example:
PgpContact[] to = new PgpContact[]{new PgpContact("[email protected]", "Tom at CryptUp")};
PgpContact from = new PgpContact("[email protected]", "Me Myself");
String msg = mime_encode("this is the message that I'm sending", to, from, "this is the subject", null);
System.out.println(msg);
For now, we will supply it with PgpContact
this simple way based on user input. Actually, because user only enters email (not name), you can create PgpContact this way:
new PgpContact("[email protected]", null);
Later on, PgpContact objects will be records from the database. They will already contain information about public keys and more. We will talk about this more when we design the database of contacts.
The code above will print a message like this. This MIME message string is ready to be uploaded (sent) through SMTP.
05-05 23:38:13.793 4326-4613/com.flowcrypt.email.debug I/System.out: Content-Type: multipart/mixed;
05-05 23:38:13.793 4326-4613/com.flowcrypt.email.debug I/System.out: boundary="----sinikael-?=_1-14940490937100.26064400487557826"
05-05 23:38:13.793 4326-4613/com.flowcrypt.email.debug I/System.out: To: Tom at CryptUp <[email protected]>
05-05 23:38:13.793 4326-4613/com.flowcrypt.email.debug I/System.out: From: Me Myself <[email protected]>
05-05 23:38:13.793 4326-4613/com.flowcrypt.email.debug I/System.out: Subject: this is the subject
05-05 23:38:13.793 4326-4613/com.flowcrypt.email.debug I/System.out: Date: Sat, 06 May 2017 05:38:13 +0000
05-05 23:38:13.793 4326-4613/com.flowcrypt.email.debug I/System.out: Message-Id: <[email protected]>
05-05 23:38:13.793 4326-4613/com.flowcrypt.email.debug I/System.out: MIME-Version: 1.0
05-05 23:38:13.793 4326-4613/com.flowcrypt.email.debug I/System.out: ------sinikael-?=_1-14940490937100.26064400487557826
05-05 23:38:13.794 4326-4613/com.flowcrypt.email.debug I/System.out: Content-Type: text/plain
05-05 23:38:13.794 4326-4613/com.flowcrypt.email.debug I/System.out: Content-Transfer-Encoding: quoted-printable
05-05 23:38:13.794 4326-4613/com.flowcrypt.email.debug I/System.out: this is the message thar I'm sending
This is more or less the structure of the contact table that will hold public keys and info about them:
CREATE TABLE IF NOT EXISTS contact (
`email` VARCHAR(100) NOT NULL,
`name` VARCHAR(50) NOT NULL,
`pubkey` BLOB DEFAULT NULL,
`has_pgp` BOOLEAN NOT NULL,
`client` varchar(20) DEFAULT NULL,
`attested` BOOLEAN DEFAULT NULL,
`fingerprint` varchar(40) DEFAULT NULL,
`longid` varchar(16) DEFAULT NULL,
`keywords` varchar(100) DEFAULT NULL,
`last_use` INTEGER DEFAULT 0,
PRIMARY KEY(email),
INDEX (name),
INDEX(has_pgp),
INDEX(longid),
INDEX(last_use)
);
It mirrors PgpContact
class with some added indexes.
Do not try to decide these fields manually one by one when saving records. Instead, create a PgpContact
object using one of the shorter constructors:
new PgpContact(email, name);
- use before you searched that person's pubkey on the APInew PgpContact(js, email, name, pubkey, client, attested);
- use after you have searched that personThe constructor will fill the remaining values.
Field details:
email
: email addressname
: name parsed from email headers, if available, or nullpubkey
: api_lookup_result.pubkey (or null)client
: one of flowcrypt
, pgp
or null
. See below.attested
: api_lookup_result.attested (true or false)Client:
api_lookup_result.pubkey == null
: nullapi_lookup_result.pubkey != null && api_lookup_result.has_cryptup == true
: "flowcrypt"api_lookup_result.pubkey != null && api_lookup_result.has_cryptup == false
: "pgp"When composing email, the moment the user leaves the "To" field, look up the email in the database:
a) if there is a record for that email and has_pgp==true, you can use the `pubkey` instead of querying Attester
b) if there is a record but `has_pgp==false`, do `attester.cryptup.io/lookup/email` API call to see if you can now get the pubkey. If a pubkey is available, save it back to the database.
c) no record in the db found:
c.1: save an empty record eg `new PgpContact(email, null);` - this means we don't know if they have PGP yet
c.2: look up the email on `attester.cryptup.io/lookup/email`
c.3: if pubkey comes back, create something like `new PgpContact(js, email, null, pubkey, client, attested);`. The PgpContact constructor will define has_pgp, longid, fingerprint, etc for you. Then save that object into database.
c.4: if no pubkey found, create `new PgpContact(js, email, null, null, null, null);` - this means we know they don't currently have PGP
When sending an email or reply (user clicked on send button):
By now, you have already saved the contact to database, regardless if they have PGP or not.
Update last_use with amount of seconds since the epoch (current time).
When pulling sent email (sent folder):
For each sent email that you process and show in a list, collect "to", "cc" and "bcc" fields to retrieve email-name pairs. Then either insert (if no matching row) or update (if have matching row but missing name) the database records.
This is because the user may have sent these emails from before he started using FlowCrypt, or maybe sent them from other client, so they may not be in the database or the database may have missing names.
for email, name in email_name_pairs:
if email in db:
if db_row.name is null and bool(name) == true:
"save that person's name into the existing DB record"
else:
"save that email, name pair into DB like so: new PgpContact(email, name);"
All of these database records will be used to look up email addresses as the person is typing it in compose (later).
Most important security rule:
This concept is called TOFU (trust on first use) and it's a crucial rule to follow to keep the system secure.
Don't show any error, just instead of "restore from backup" screen, show a "setup screen" with three buttons:
After complete #31, need to migrate to use EmailSyncService in the settings backup activity.
The EmailSyncService will be used for:
Adding this so it doesn't get lost. If fixed, please close
This was when I was setting up the app on an account that has no backup and I imported my own key during setup.
Happened just after entering my pass phrase.
USER_COMMENT=
ANDROID_VERSION=7.0
APP_VERSION_NAME=1.0.0_1__04_07_2017_21_27
BRAND=motorola
PHONE_MODEL=Moto G (4)
CUSTOM_DATA=
STACK_TRACE=java.lang.RuntimeException: Unable to start activity ComponentInfo{com.flowcrypt.email.debug/com.flowcrypt.email.ui.activity.EmailManagerActivity}: java.lang.IllegalArgumentException: You must pass an Account to this activity.
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2659)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2724)
at android.app.ActivityThread.-wrap12(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1473)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6123)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:867)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:757)
Caused by: java.lang.IllegalArgumentException: You must pass an Account to this activity.
at com.flowcrypt.email.ui.activity.EmailManagerActivity.onCreate(EmailManagerActivity.java:126)
at android.app.Activity.performCreate(Activity.java:6672)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1140)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2612)
... 9 more
java.lang.IllegalArgumentException: You must pass an Account to this activity.
at com.flowcrypt.email.ui.activity.EmailManagerActivity.onCreate(EmailManagerActivity.java:126)
at android.app.Activity.performCreate(Activity.java:6672)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1140)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2612)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2724)
at android.app.ActivityThread.-wrap12(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1473)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6123)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:867)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:757)
I will add more details and specify this later, for now just sample bits of code.
For crypto_message_decrypt
to work, Js
needs to be passed something that implements StorageConnectorInterface
:
public interface StorageConnectorInterface {
PgpContact findPgpContact(String longid);
PgpContact[] findPgpContacts(String longid[]); if two contacts requested and only one found, will still return array of 2, eg [PgpContact, null] or [null, PgpContact] depending which one is missing
PgpKeyInfo getPgpPrivateKey(String longid);
PgpKeyInfo[] getFilteredPgpPrivateKeys(String longid[]); // if 2 keys requested and only one found, will return array of 1: [PgpKey]
PgpKeyInfo[] getAllPgpPrivateKeys();
String getPassphrase(String longid);
}
Passed in like this:
try {
SampleStorageConnector storage = new SampleStorageConnector(this);
Js js = new Js(this, storage);
} catch (IOException e) {
System.err.println(e.getMessage());
}
Then it will decrypt messages like this:
PgpDecrypted m = js.crypto_message_decrypt(read(context.getAssets().open("sample/message.asc")));
if(m.isSuccess()) {
System.out.println(m.isEncrypted());
System.out.println(m.getContent());
System.out.println(m.getSignature());
} else {
System.err.println(m.getFormatError());
System.err.println(Arrays.toString(m.getMissingPassphraseLongids()));
System.err.println(m.countFormatErrors());
System.err.println(m.countKeyMismatchErrors());
System.err.println(m.countPotentiallyMatchingKeys());
System.err.println(m.countUnsecureMdcErrors());
System.err.println(Arrays.toString(m.getOtherErrors()));
System.err.println(Arrays.toString(m.getEncryptedForLongids()));
}
Which will output:
04-30 20:51:34.867 16914-16914/com.flowcrypt.email.debug I/System.out: true
04-30 20:51:34.867 16914-16914/com.flowcrypt.email.debug I/System.out: CryptUp lets you send and receive encrypted messages and attachments with Gmail. Even if the recipient doesn't use CryptUp or Gmail.
04-30 20:51:34.867 16914-16914/com.flowcrypt.email.debug I/System.out: This message is inside a green frame because it's protected with end-to-end encryption. Only you and your recipients can read the contents.
04-30 20:51:34.867 16914-16914/com.flowcrypt.email.debug I/System.out: I'm available anytime to answer your questions, listen to suggestions or help in any way.
04-30 20:51:34.867 16914-16914/com.flowcrypt.email.debug I/System.out: If you just want to test this out, try hitting the reply button and send me a "hello".
04-30 20:51:34.867 16914-16914/com.flowcrypt.email.debug I/System.out: Cheers,
04-30 20:51:34.867 16914-16914/com.flowcrypt.email.debug I/System.out: Tom
04-30 20:51:34.867 16914-16914/com.flowcrypt.email.debug I/System.out: null
Often, messages are complicated and have several different parts in them. I call each part a MessageBlock
, here are the types:
public static String TYPE_TEXT = "text";
public static String TYPE_PGP_MESSAGE = "message";
public static String TYPE_PGP_PUBLIC_KEY = "public_key";
public static String TYPE_PGP_SIGNED_MESSAGE = "signed_message";
public static String TYPE_PGP_PASSWORD_MESSAGE = "password_message";
public static String TYPE_ATTEST_PACKET = "attest_packet";
public static String TYPE_VERIFICATION = "cryptup_verification";
For example, a single message may have the following structure:
That is why we need a more sophisticated way to parse and display messages. Each message will be parsed and displayed as a sequence of bloks, and each block type will be processed differently before rendering it.
I have exposed some functionality very similar to what I do in the browser plugin and desktop apps.
Instead of using js.mime_decode
, we will start using js.mime_process
. It's very similar, except instead of getBody
and getHtml
, you will use getBlocks
and then treat each block based on the type. Then you render all the blocks. Something like this:
ProcessedMime pm = js.mime_process(rawMessage);
System.out.println("From:" + pm.getAddressHeader("from")[0].getAddress());
MessageBlock[] blocks = pm.getBlocks();
for (MessageBlock block : blocks) {
if (Objects.equals(block.getType(), MessageBlock.TYPE_TEXT)) {
// should be displayed
System.out.println(block.getContent());
} else if (Objects.equals(block.getType(), MessageBlock.TYPE_PGP_MESSAGE)) {
// should be decrypted, then displayed
System.out.println(block.getContent());
} else {
// should display a block with Not Implemented notice in it:
System.out.println("FlowCrypt Android cannot process " + block.getType() + " (not implemented)");
}
}
Here is the key. js.mime_process
will expect to work with first 200kb of each mime message. I have tested this on desktop apps that also use IMAP: you can request first 200kb of a message, and if the message is smaller, you'll get the whole MIME message. If larger, you'll get only the first part of it. js.mime_process
will happily work with a cropped message, because 200kb will definitely contain all important parts in the message body, and only large attachments will be cropped. This should work in 99% of the cases, and later we can fix and improve the remaining 1%.
Here is how I do the SMTP query in JavaScript (we'll do something similar in Java):
imap.listMessages(path, uid, ['RFC822.SIZE', 'BODY[]<0.204800>'], {byUid: true}).then(messages => {
resolve(messages[0]['body[]']) // first 200kb of mime message
}, reject)
This will create IMAP UID FETCH xxxxxxxxxxxxx (RFC822.SIZE BODY[]<0.204800>)
or a similar IMAP command. The relevant spec is here: https://tools.ietf.org/html/rfc3501#section-6.4.5
This will take care of displaying the message contents.
For any actual attachments, we will later use IMAP BODYSTRUCTURE call along with fetching individual attachments as user clicks on them.
I've done some tests on IMAP desktop app I'm developing and I think it will be very robust.
Should show (no subject)
in both inbox as well as when opening the message
Can wait for later
Here's an outline of what we should do:
KeyStoreEncryption
You should have a table called something like key
. The table should have roughly the following structure:
CREATE TABLE IF NOT EXISTS `key` (
`longid` VARCHAR(16) NOT NULL,
`source` VARCHAR(20) NOT NULL,
`public` BLOB NOT NULL,
`private` BLOB NOT NULL,
`passphrase` varchar(100) DEFAULT NULL,
PRIMARY KEY(longid)
);
Explaining each column:
longid
: get this when recovering the key with js.crypto_key_longid()
source
: any of following strings: "backup", "new", "import". For now, will always be backup
public
: armored public key. get with key.toPublic().armor()
private
: armored private key, that was first encrypted using KeyStoreEncryption. Something like: String KeyStoreEncryption.encrypt(String normalized_armored_private_key);
passphrase
: also encrypted eg String KeyStoreEncryption.encrypt(String passphrase);
. This column may stay empty (a null). For now though, you will always fill it with the encrypted pass phrase.Unfortunately, KeyStore cannot just store any string and encrypt it - we have to do it ourselves and use KeyStore to do it.
Basically, we will create a new keypair in KeyStore and then use that keypair to encrypt and decrypt sensitive information. When creating the keypair, you will have to assign a an alias to it. You can probably pick something static like "flowcrypt_main".
The details are outlined here: http://www.androidauthority.com/use-android-keystore-store-passwords-sensitive-information-623779/
KeyStore specs if you need them: https://developer.android.com/training/articles/keystore.html
This functionality should be put as methods into a class, something like:
class KeyStoreEncryption {
private String alias = "flowcrypt_main";
public String encrypt(String plain_data);
public String decrypt(String encrypted_data);
}
The details of how you structure this is of course up to you. The result will be that private
and passphrase
fields in the database will be encrypted using the KeyStore keypair, and therefore protected by user's Android passcode.
Anytime you save or load the private key from database, you'll need to use that class.
This will make it possible to connect this to SecurityStorageConnector
and look up private keys and pass phrases based on longid
as originally intended
When I send out a new message (I tested this with Reply) and then go to my account on mail.google.com, the sent message shows as unread.
Sent messages (new ones or replies) should be marked as read when I send them myself.
Not sure if there are some parameters to set in SMTP (that would be better) or if the message needs to be marked as read in IMAP.
Either way, something should be done so that people don't get confused.
Was trying to select and copy some text, not possible in received messages
Can wait a little
Need to add a writer of REST logs to sd card for debugging HTTP requests. This gives an opportunity on any build to catch problems with network requests. Use a retrofit.
Need to add a LeakCanary to fix problems with memory leaks. This necessary before the first release.
https://github.com/square/leakcanary.
Anywhere there is a question mark, it should take the user to a help screen. When the user is done with the help screen, it should take them back to whichever screen they were at before it.
Submitting feedback to the api:
POST /help/feedback {
"email" (<type 'str'>) # user email or "[email protected]"
"message" (<type 'str'>) # user feedback message text
}, response(200): {
"sent" (True, False) # True if message was sent successfully
"text" (<type 'str'>) # User friendly success or error text
}
We need to add a logic of caching message details if the message not big. The Gmail on Android not loads message details everytime when we open the new screen with the message. And I think Gmail cached message details when we load new messages.
This requires a detailed study.
See basic environment setup / answered questions:
https://github.com/tomholub/cryptup-android-prototype/wiki/App-values-and-environment-setup
You can just rename this current project in this repo if that's ok.
We have to do some speed improvements. I'm currently also working on a Desktop app for Windows/Linux/Macos that also uses IMAP, so I have a little better understanding of this now.
Dealing with folders:
This will mean we don't have to wait extra time while the app is pulling list of folders every time.
Displaying messages: loading a message takes way too long. I suspect we are creating a new connection for each screen?
Can wait until other things are done
Need to create a debug.keystore and add it to the project repo.
Got to plug in the one from Java instead
crypto: {
getRandomValues: function (buf) { // NOT SECURE - for testing only
for(var i=0; i<buf.length; i++) {
buf[i] = Math.round(Math.random() * 255);
}
},
},
Modifies launcher icons on debug build
https://github.com/gfx/gradle-android-ribbonizer-plugin
Today I spent some time exploring the application.
And it turned out that in the process of displaying detailed information about the message from the raw MIME message, more than 70% takes the process of decrypting.
I mean this code: js.crypto_message_decrypt(decryptedText);
com.flowcrypt.email.ui.loader.DecryptMessageAsyncTaskLoader
/**
* Decrypt a message if it encrypted. At now will be decrypted only a simple text.
*
* @param js The Js object which used to decrypt a message text.
* @param mimeMessage The MimeMessage object.
* @return <tt>String</tt> Return a decrypted or original text.
*/
@SuppressWarnings("deprecation")
private String decryptMessageIfNeed(Js js, MimeMessage mimeMessage) {
if (TextUtils.isEmpty(mimeMessage.getText())) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return Html.fromHtml(mimeMessage.getHtml(), Html.FROM_HTML_MODE_LEGACY).toString();
} else {
return Html.fromHtml(mimeMessage.getHtml()).toString();
}
} else {
String decryptedText = js.crypto_armor_clip(mimeMessage.getText());
if (decryptedText != null) {
PgpDecrypted pgpDecrypted = js.crypto_message_decrypt(decryptedText);
try {
return pgpDecrypted != null ? pgpDecrypted.getContent() : "";
} catch (Exception e) {
e.printStackTrace();
return mimeMessage.getText();
}
} else {
return mimeMessage.getText();
}
}
}
For example, loading and displaying a small encrypted message takes approximately 7 seconds, Of which:
This is normal?
When I used https://attester.cryptup.io/lookup/email, I saw that this API is case sensitive for email parameter.
Do I have to manually check that I send the parameter in lowercase? Or maybe it's better for API to solve this issue? As far as I know, it does not matter how to write email address. (For example "[email protected]" = "[email protected]")
For now the only action will be "reply". In the future, we will add "reply all" and "forward", but not yet.
We will not quote the previous message when replying. It sometimes creates trouble, just respond a fresh new message - with the right headers, so that it's assigned to the same email thread.
I'm inspecting my code to find out how I do it with the reply MIME headers - will add info.
On the top, there are standard material design tabs. The whole space below is a HTML container. Please create 4 assets:
And load them based on the tab. The first tab that opens automatically is privacy.
Somehow, the text didn't get show there.. here is screenshot with the privacy policy in it:
privacy.htm
<style>.text {padding: 10px;font-family: sans-serif; color:#555;} h1 {font-size: 20px;}</style>
<div class="text">
<h1>CryptUp Privacy Policy</h1>
<p>The goal is that CryptUp will have access to as little information as necessary. The details are outlined below.</p>
<p>This document is the first draft and it is written in Plain English as opposed to Legal Language so that it can be readily understood. Please send me feedback if any section needs clarification or clearer language.</p>
<h2>Terminology</h2>
<ul>
<li>CRYPTUP or WE - CryptUp's developers, employees, legal entity or server software that is under our direct control</li>
<li>YOU - the user of this software bound by its licence and terms of use</li>
<li>SERVER HARDWARE - a 3rd party server infrastructure provided as a service to CRYPTUP. This may include cloud servers and related services</li>
<li>CRYPTUP SERVER - server software holding user account related information, and allowing storage of encrypted MESSAGE when needed. This software runs on SERVER HARDWARE</li>
<li>ATTESTER - server software necessary to make encrypted PGP communication smooth. ATTESTER helps distribute and verify PUBLIC KEYS and runs on SERVER HARDWARE</li>
<li>PUBLIC KEY - Cryptographic information needed in order to encrypt data for someone</li>
<li>PRIVATE KEY - Cryptographic information needed to open or decrypt previously encrypted data for its corresponding PUBLIC KEY. Additionally protected with a PASS PHRASE</li>
<li>PASS PHRASE - A long textual string that is difficult to guess or brute force, used to protect the PRIVATE KEY</li>
<li>BRUTE FORCE ATTACK - A method of unlocking encrypted material without breaking the underlying encryption, by using vast amounts of computational power to guess all possible combinations of a given PASS PHRASE or MESSAGE PASSWORD. This attack method can be combined with other methods such as a dictionary attack (the use of words commonly found in passwords). The success rate of such attack can vary from very high (simple, short PASS PHRASE or MESSAGE PASSWORD) through very low (long, complicated, uncommon and hard to guess PASS PHRASE or MESSAGE PASSWORD) to effectively impossible with current technology (complex, very long, random PASS PHRASE or MESSAGE PASSWORD)</li>
<li>LOCAL MACHINE - general term for hardware used by end users such as a computer, laptop, tablet or a phone</li>
<li>LOCAL APP - CryptUp software that runs on LOCAL MACHINE. This may take a form of a browser extension or a native application</li>
<li>LOCAL APP CONTACTS - Collection of contacts who use encryption, their PUBLIC KEYs and related info stored in LOCAL APP</li>
<li>WEB APP - CryptUp software which is served on the web through a link</li>
<li>PRIVATE WEB APP - is a WEB APP that produces or keeps sensitive MESSAGE CONTENT exclusively on LOCAL MACHINE</li>
<!--<li>HASH - data fingerprint that is always consistent for the same data and does not reveal any information about the data itself. https://en.wikipedia.org/wiki/Cryptographic_hash_function</li>-->
<li>EMAIL PROVIDER - person, company or their software who ensures delivery of your email. This could be Google, Yahoo, Microsoft, your or other person's private email server, or any other that can fulfill this function</li>
<li>ACCESS TOKEN - set of tokens and information needed to access user's account, eg through their EMAIL PROVIDER</li>
<li>COMPATIBLE SOFTWARE - Software from other vendors which is able to follow the OpenPGP standard of asymmetric key encryption. LOCAL APP or PRIVATE WEB APP Will attempt to find out which recipients use COMPATIBLE SOFTWARE by first searching your LOCAL APP CONTACTS and if not found, performing a lookup with ATTESTER. For the purpose of this document, recipients are considered as having COMPATIBLE SOFTWARE if LOCAL APP or PRIVATE WEB APP has access to recipient's PUBLIC KEY</li>
<li>PLAIN TEXT - message that has not been encrypted, and may be readable by anyone handling it</li>
<li>MESSAGE - encrypted text data that may or may not include encrypted attachments. Typically in the format of an email with certain parts of the email encrypted</li>
<li>MESSAGE PASSWORD - one-time password used to encrypt MESSAGE when recipient doesn't have COMPATIBLE SOFTWARE</li>
<li>CONTENT - the actual meaningful information contained inside MESSAGE, accessible to anyone with corresponding PRIVATE KEY or MESSAGE PASSWORD</li>
</ul>
<h2>End-to-end encryption</h2>
<p>Regardless of the method of MESSAGE delivery, CryptUp is an end-to-end encryption software. That means the CONTENT of your MESSAGE is encrypted in LOCAL APP or PRIVATE WEB APP and then sent to the recipient encrypted. How the MESSAGE is handled by the recipient depends on their setup. The vast majority of compatible software will only decrypt your MESSAGEs on the recipient's LOCAL MACHINE. That means neither EMAIL PROVIDER or CRYPTUP can access the CONTENT of the MESSAGE in transit or at rest.</p>
<h2>Email ACCESS TOKEN</h2>
<p>ACCESS TOKEN needed to access user's email is exclusively stored in LOCAL APP on user's LOCAL MACHINE, no exceptions. ACCESS TOKEN is used solely within LOCAL APP for user authentication, sending and receiving of MESSAGES and other related actions that make LOCAL APP work smoothly.</p>
<h2>MESSAGE delivery and storage</h2>
<p>When you message a recipient who has COMPATIBLE SOFTWARE, the encrypted message is transferred from your EMAIL PROVIDER to recipient's EMAIL PROVIDER. This includes any attachments. How the encrypted data is transferred and stored and what happens to the MESSAGE is at the sole discretion of respective EMAIL PROVIDERs. EMAIL PROVIDER will see who you are messaging, how often, the email subject and related meta information just like they do when you send PLAIN TEXT email. The mechanics of sending encrypted email is the same as PLAIN TEXT email, except EMAIL PROVIDER is not able to see the CONTENT of these MESSAGEs.</p>
<p>When any of your recipients do not have COMPATIBLE SOFTWARE, LOCAL APP or PRIVATE WEB APP will require a MESSAGE PASSWORD to be provided by sender. Anyone who has access to the MESSAGE PASSWORD can open such MESSAGE. The encrypted MESSAGE is then sent through EMAIL PROVIDERs the same way as above. In addition, encrypted MESSAGE will be stored on CRYPTUP SERVER. This helps recipients without COMPATIBLE SOFTWARE to open such messages and view their CONTENT through the use of PRIVATE WEB APP. When the recipient doesn't have COMPATIBLE SOFTWARE and the encrypted MESSAGE is relayed through CRYPTUP SERVER, following information is stored along with it: (a) date and time MESSAGE was sent, (b) size of MESSAGE, (c) the encrypted MESSAGE, (d) message expiration time, (e): indication if this is a MESSAGE text or attachment, (f) sender of MESSAGE only if this is an attachment and this particular MESSAGE never expires. MESSAGE that is not an attachment or is set to expire at a future date will not have any sender associated with it on CRYPTUP SERVER. Because MESSAGE PASSWORD can be subject to a BRUTE FORCE ATTACK, it is advisable to use a MESSAGE PASSWORD of sufficient strength for your particular use case.</p>
<h2>Handling MESSAGE PASSWORDs and PASS PHRASEs</h2>
<p>CRYPTUP will never have access to user PRIVATE KEYs, MESSAGE PASSWORDs or PASS PHRASEs. LOCAL APP is intended to never send such information to CRYPTUP SERVER or ATTESTER.</p>
<p>LOCAL APP, PRIVATE WEB APP or any other CRYPTUP software will not distribute PASS PHRASEs or MESSAGE PASSWORDs in any way. Safe storage, backup and distribution of this material is left solely on the user.</p>
<p>If any user intentionally or unintentionally sends a PRIVATE KEY, MESSAGE PASSWORD or PASS PHRASE to CRYPTUP (please do not do that!), CRYPTUP will delete such information immediately upon noticing it, unless the user explicitly indicated that this material is solely for testing purposes. In either case, users should consider such keys not trusted and compromised, and should avoid using them in production scenarios.</p>
<h2>Handling of PRIVATE KEYs</h2>
<p>LOCAL APP will store PRIVATE KEYs in storage accessible only to LOCAL MACHINE such as browser storage, application storage, hard drive or similar, and the security of these PRIVATE KEYs depend on the security of the underlying LOCAL MACHINE that keeps them. For this reason, it is advised to always update to latest operating system, keep up to date with latest security fixes, keep the system virus free using reliable antivirus software, using full-disk encryption or any other practices that make LOCAL MACHINE less vulnerable to attackers. Additionally, CRYPTUP recommends that you select an option to "Always require a pass phrase when opening email" as an additional layer of security in case your LOCAL MACHINE gets compromised in the future through physical or other means.</p>
<p>In addition to storing PRIVATE KEY in LOCAL APP exclusive to LOCAL MACHINE, depending on how was LOCAL APP set up, following will apply:</p>
<ul>
<li><u>Option 1 - manual setup - use (import) my own key</u>: LOCAL APP will keep both the PRIVATE KEY and PASS PHRASE exclusively on LOCAL MACHINE, unless user specifically navigates to backup section of settings where they perform an additional form of PRIVATE KEY backup.</li>
<li><u>Option 2 - manual setup - create a new key</u>: LOCAL APP will provide the user with a comprehensive estimation of the strength of their PASS PHRASE. Once the user chooses a PASS PHRASE of satisfactory strength depending on their use case, LOCAL APP will store the PASS PHRASE and the PRIVATE KEY on LOCAL MACHINE. In addition, as a part of the setup procedure, LOCAL APP will ask user to select an additional method of PRIVATE KEY backup, if needed. User is free to select a backup method or choose not to perform any backup.</li>
<li><u>Option 3 - simple setup - create a new key</u>: LOCAL APP will provide the user with a comprehensive estimation of the strength of their PASS PHRASE. Once the user chooses a PASS PHRASE of satisfactory strength depending on their use case, LOCAL APP will store the PASS PHRASE and the PRIVATE KEY on LOCAL MACHINE. In addition, LOCAL APP will automatically back up the key on user's EMAIL PROVIDER. The backed up key is protected with a PASS PHRASE that will always stay exclusively on LOCAL APP within LOCAL MACHINE. It is strongly recommended to choose a PASS PHRASE that will be evaluated to maximum strength (full strength bar) during LOCAL APP setup, as PASS PHRASEs of such strength take vast amount of resources to crack through BRUTE FORCE ATTACK, making such attacks effectively impossible.</li>
</ul>
<h2>Personal information</h2>
<p>We will not sell or otherwise abuse your personal information.</p>
<p>To be able to fulfill our services, we may need to share user's email address and name with a 3rd party, such as a payment processor for premium accounts.</p>
<p>This may not be necessary for payments made in Bitcoin or Ethereum.</p>
<h2>Missing sections</h2>
<p>This document is factually correct but incomplete. Additional information concerning following topics will be added soon:</p>
<ul>
<li>handling of PUBLIC KEYs, ATTESTER data storage and publication policy</li>
<li>handling of expired MESSAGEs (their content and all associated data gets deleted)</li>
<li>policy of purging data from CRYPTUP SERVER (expired messages purged automatically, everything else per request of sender)</li>
<li>the process and technical details of purging data</li>
<li>software security review and disclosure of security related bugs</li>
</ul>
<h2>Feedback</h2>
<p>This privacy policy is subject to change without prior notice based on feedback from the community. Such changes and prior versions will be visible on project's public repository and also mentioned in project's changelog if/when such changes occur.</p>
<p>Please send me your feedback or requests for clarification at [email protected]</p>
</div>
terms.htm
<div>sample terms page</div>
license.htm
<div>sample license page</div>
sources.htm
<div>sample sources page</div>
If you log out and log in under different account, it will show the old accounts messages mixed with the new
Need to add Crashlytics for release builds.
https://docs.fabric.io/android/fabric/overview.html
When user opens the screen, it will look for backups the same way it does during setup.
If there are no backups found, show this screen:
When I change the radio button to "download", the button text will change to "BACK UP AS A FILE" and the text below button will also change appropriately. When I change the radio back to "EMAIL", it will revert back as on the screen shot.
If there is a backup found:
Email:
This will back up private key in your Gmail Inbox. The security depends on the strength of your pass phrase, which should be very long and hard to guess. Best option for most users.
Download:
This will download your key as a file. Very secure option if your device is encrypted, or if you can keep a file stored securely. If you lose both your laptop and the file, your encrypted email will be unreadable, so you need to keep this file in a safe place. Regular emails will not be affected.
When user clicks on the green backup button, you should replicate the behavior of what the chrome extension does. See CryptUp Chrome extension -> Settings -> Backup.
"flowcrypt-backup" + sanitized_email + ".key"
, where "sanitized_email = account_email_address.replace(/[^a-z0-9]/, '')"
The app will need to use two APIs: api.cryptup.io
(called Api
) and attester.cryptup.io
(called Attester
)
You should probably extend both from a common abstract class, because some of the behavior is the same in both.
Please send a header api-version: 3
with each request to any of the two endpoints. Make the number "3" static parameter in the class, we may want to upgrade to a higher version of the apis sometime in the future.
The v3 apis return a unified error format, like this:
{
"error": {
"code": 500,
"message": "Error message that you can display publicly",
"internal": "message for internal purposes or null"
}
}
* POST /test/welcome # Send a welcome test email
* POST /initial/request # Request initial pubkey attest
* POST /initial/confirm # Confirm initial pubkey validation
* POST /lookup/email # Find a pubkey for email address
* POST /replace/request # Request a pubkey replacement
* POST /replace/confirm # Confirm a pubkey replacement
* POST /link/message # Get URL of message data based on short
* POST /account/subscribe # Subscribe to a premium account
* POST /account/login # Log in or Register a new account
* POST /message/confirm_files # Confirm submission of files to s3
* POST /message/upload # Store a message if it cannot be sent through email provider
* POST /message/token # Get a token that recipients can use to reply on the web
* POST /message/presign_files # Get pre-signed approvals to POST attachments onto s3 bucket
* POST /help/uninstall # User just uninstalled a client app
* POST /help/feedback # Send feedback to devs
Full spec on both is here:
https://github.com/tomholub/cryptup-android-prototype/wiki/attester.cryptup.io
https://github.com/tomholub/cryptup-android-prototype/wiki/api.cryptup.io
You don't really have to test them all because you may not know what exactly is it supposed to do anyway, but it probably makes sense to prepare the models and corresponding methods, and what type of values go into each call, etc.
You can also get inspired here: https://github.com/tomholub/cryptup-android-prototype/blob/master/FlowCrypt/src/main/assets/js/tool.js#L3243 to see examples of something similar in JavaScript.
Things like MESSAGE_TOKEN_ACCOUNT
and all of the original prototype functionality won't be needed anymore.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.