Git Product home page Git Product logo

flowcrypt-android's People

Contributors

buthed avatar denbond7 avatar dependabot-preview[bot] avatar dependabot[bot] avatar ioanmo226 avatar ivanpizhenko avatar martgil avatar seisvelas avatar tomholub avatar vahram-papazyan avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

flowcrypt-android's Issues

Use EmailSyncService in the settings backup activity.

After complete #31, need to migrate to use EmailSyncService in the settings backup activity.
The EmailSyncService will be used for:

  1. Load private keys from the IMAP server;
  2. Send to the user a message with the backup private key (backup email).

newly sent messages should be automatically market as read

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.

expose javascript functions in Java

https://github.com/tomholub/cryptup-android-prototype/wiki/JavaScript-functions-and-specs

  • str_is_email_valid
  • str_parse_email,
  • str_normalize_spaces,
  • str_number_format,
  • str_random,
  • str_html_attribute_encode,
  • str_html_attribute_decode,
  • str_html_escape,
  • str_html_unescape,
  • str_base64url_encode,
  • str_base64url_decode,
  • str_from_uint8,
  • str_to_uint8,
  • str_from_equal_sign_notation_as_utf,
  • str_uint8_as_utf,
  • str_to_hex,
  • str_extract_cryptup_attachments,
  • str_extract_cryptup_reply_token,
  • str_strip_cryptup_reply_token,
  • str_int_to_hex,
  • time_to_utc_timestamp
  • env_url_params,
  • env_url_create,
  • file_pgp_name_patterns,
  • mime_resembles_message,
  • mime_format_content_to_display, // todo - should be refactored into two
  • mime_decode,
  • mime_encode,
  • mime_process
  • mime_parse_message_with_detached_signature,
  • crypto_armor_strip,
  • crypto_armor_clip,
  • crypto_armor_headers,
  • crypto_armor_replace_blocks,
  • crypto_armor_normalize,
  • crypto_hash_sha1,
  • crypto_hash_double_sha1_upper,
  • crypto_hash_sha256,
  • crypto_hash_challenge_answer,
  • crypto_key_read,
  • crypto_key_decrypt,
  • crypto_key_expired_for_encryption,
  • crypto_key_normalize,
  • crypto_key_fingerprint,
  • crypto_key_longid,
  • crypto_key_test,
  • crypto_key_usable,
  • crypto_message_sign,
  • crypto_message_verify,
  • crypto_message_verify_detached,
  • crypto_message_decrypt,
  • crypto_message_encrypt,
  • api_gmail_query_backups
  • api_auth_parse_id_token

other useful functions:

  • mnemonic

optimize decrypt speed

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:

  • 2 seconds loading the message (So long, because we want to get a message from the server without attachments. In this case, we need to make additional requests.)
  • 3.5 seconds decrypting + 1.5 seconds parsing from the raw MIME message to MimeMessage

This is normal?

settings: legal

On the top, there are standard material design tabs. The whole space below is a HTML container. Please create 4 assets:

  • privacy.htm
  • terms.htm
  • license.htm
  • sources.htm

And load them based on the tab. The first tab that opens automatically is privacy.

image

Somehow, the text didn't get show there.. here is screenshot with the privacy policy in it:

image

Asset/page contents:

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>

decrypting messages

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

private key saving, structure and security

Here's an outline of what we should do:

  1. Some information about private keys will have to go into a database
  2. Create a class to encrypt secrets using the KeyStore, call it something like KeyStoreEncryption
  3. look up private keys

1: Database

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.

2: KeyStoreEncryption

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.

3: Look up private keys

This will make it possible to connect this to SecurityStorageConnector and look up private keys and pass phrases based on longid as originally intended

process mime message blocks with mime_process

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:

  1. a few lines of plain text (intro)
  2. an encrypted message
  3. a few lines of plain text (email footer, contact, etc)
  4. a public key

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.

sending encrypted emails

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

create models for common APIs

The app will need to use two APIs: api.cryptup.io (called Api) and attester.cryptup.io (called Attester)

Common stuff

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"
  }
}

Useful Attester calls

* 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

Useful Api calls

 * 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

Details

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.

remove prototype code

Things like MESSAGE_TOKEN_ACCOUNT and all of the original prototype functionality won't be needed anymore.

restore account: find key using IMAP + test passphrase + save

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);
}

what to do when backup is not found

Don't show any error, just instead of "restore from backup" screen, show a "setup screen" with three buttons:

  • [create a new key] - will not work yet
  • [import my key] - will lead to a screen where I can select a file from my device, add a pass phrase and click [use this key] button. It will test it, save it and proceed the same way like when importing from backup
  • [use another account] - gray button

test & fix setting up account from imported key file

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)

Clear inbox cache on logout

If you log out and log in under different account, it will show the old accounts messages mixed with the new

start using a secure random number generator

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);
      }
    },
  },

replying to messages

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.

Contacts: public key database structure

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 API
  • new PgpContact(js, email, name, pubkey, client, attested); - use after you have searched that person

The constructor will fill the remaining values.

Field details:

  • email: email address
  • name: name parsed from email headers, if available, or null
  • pubkey: 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: null
  • api_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:

  • If a public key is found and saved in the database, NEVER try to look it up again on the API.
  • Always use the pubkey from the db if there is one.
  • Never replace a public key that was already saved in the database unless the user does it manually in settings.

This concept is called TOFU (trust on first use) and it's a crucial rule to follow to keep the system secure.

feedback screen

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
}

settings: contacts

Only list contacts where has_pgp == true

Clicking the delete button will remove them from the db. This is useful if the contact now has a new public key attested: next time the user writes them, it will pull a new public key.

image

archive message + delete message

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.

  • If success, the screen should switch to previous folder in inbox and show a success message
  • if error, the screen should not change and show an error message

User info from GoogleSignInResult

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();

settings: backup

When user opens the screen, it will look for backups the same way it does during setup.

Screens

If there are no backups found, show this screen:
image

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:

  • show "Found 1 backup. Your keys are backed up correctly."
  • underneath show green floating button "BACKUP OPTIONS"
  • clicking on the button will show a similar screen to the screenshot above, just without the texts above the radio buttons.

Texts

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.

Behavior

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.

settings screen

On the bottom of the sidebar menu, there should be a Settings line. It will take us to this screen:

image

Where each line is a button that will take us to appropriate settings screen

speed - imap use optimization

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:

  • folders should be cached. When I open it first time, it should load the list of folders and save them somewhere local. Next time I open it, it should:
  • render the folders from the cache
  • pull messages from active folder (typically inbox)
  • when idle, re-fetch the list of 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?

  • we should be keeping and re-using a single connection throughout the app, because IMAP login is an expensive operation
  • because of this, we should be able to detect a broken/failed connection and automatically reconnect it as needed before making further requests
  • in the future, we may even want to create a special connection to download attachments and do similar work. That way, the app doesn't get stuck/blocked while an attachment is being downloaded.

inbox improvements

  • when I click "[Gmail]", it will show an error message. That folder should probably be hidden.
  • emails should be ordered from newest on the top top older ones on the bottom
  • scrolling lower should load more messages

download, parse and view message (no decrypting yet)

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"

image
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

settings: keys

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)

image

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.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.