Navigation

HTTP Android Tutorial

This tutorial uses a simple XOR cipher to encrypt or decrypt a string. The application uses the MongoDB Stitch SDK to perform anonymous user authentication and uses the MongoDB Stitch HTTP service to perform the XOR cipher using AWS Lambda.

Considerations

For complete documentation on the MongoDB Stitch Android SDK, see the MongoDB Stitch Android SDK API.

This tutorial assumes prior knowledge and experience in topics including, but not limited to:

  • Android Development, including Android Studio
  • Java and XML programming in context to Android development
  • Amazon Web Services components, including API Gateway and AWS Lambda.

Prerequisites

Android Development

This tutorial requires using Android Studio. The minimum supported Android API level for MongoDB Stitch is 19 (Android 4.4. KitKat).

Authentication

This tutorial provides instructions on using MongoDB Stitch anonymous user authentication. While MongoDB Stitch supports Facebook and Google user authentication, those integrations are out of scope for this tutorial.

To enable Anonymous authentication for your MongoDB Stitch application,

  1. Log in to Atlas and go to Stitch Apps.
  2. Select your MongoDB Stitch application. You will be redirected to the MongoDB Stitch console to the Getting Started page. If you are not on the Getting Started page, click Getting Started in the left-hand navigation bar.
  3. Click Turn On Authentication.
  4. Toggle the Anonymous Authentication switch.

For complete documentation on configuring authentication in MongoDB Stitch, see Authentication.

Amazon Web Services Components

This tutorial assumes a working AWS Lambda function connected to an AWS API Gateway endpoint. At a high level, to integrate a MongoDB Stitch app with AWS Lambda:

  1. Create the Lambda function. See the AWS Lambda documentation. For a copy of the Lambda code used for this example, see MongoCipher.py.
  2. Create an API gateway endpoint and configure it as necessary to support the AWS Lambda function. Ensure you have deployed your AWS API gateway endpoint and that security is configured such that MongoDB Stitch can access the endpoint. See the Amazon API Gateway documentation.
  3. Configure the API gateway endpoint to invoke the Lambda function. See the relevant documentation.

Refer to the tutorial procedure for instructions on configuring the MongoDB Stitch HTTP service and connecting it to the AWS API gateway endpoint and AWS Lambda function.

Procedure

1

Configure HTTP Service in MongoDB Stitch for use with AWS Lambda.

  1. In the MongoDB Stitch console, click Add Service in the left hand navigation panel.

  2. Select HTTP from the selection screen. Enter AWSLambda in the Service Name box at the bottom of the selection screen, then click Add Service.

  3. In the left hand navigation bar under Services, click the new AWSLambda service.

  4. Under Rules, click Add Rules.

    1. From the Actions checklist, select the HTTP methods to enable. For this tutorial, select POST.
    2. In the When text entry, add the host portion of each API endpoint to the %in array.
    3. Click Save.

    For example:

    {
      "%%args.url.host": {
        "%in": [
          "abcd1234.execute-api.aws-region-1.amazonaws.com"
        ]
      }
    }
    

    With this rule, MongoDB Stitch only allows HTTP POST methods executed using the HTTP service whose full host URL matches one of the URLs in the %in array. For example, the provided rule allows HTTP POST requests tothe API Gateway endpoint https://abcd1234.execute-api.aws-region-1.amazonaws.com/prod, but not https://wxyz9876.execute-api.aws-region-1.amazonaws.com/prod.

2

Create a Named Pipeline for Posting Data to the API Gateway Endpoint.

In the MongoDB Stitch console, click Named Pipelines in the left hand navigation panel.

Click New Named Pipeline. Configure the pipeline as follows:

  1. Enter AWSLambdaPipeline for the Name.

  2. Click Add Parameter twice to add two new parameters. Enter message for the first parameter and key for the second parameter. Select Required for each parameter.

  3. MongoDB Stitch creates a simple default built-in stage. To edit, hover your mouse over the stage and click the edit button in the top right hand corner.

  4. For Service, select AWSLambda.

  5. For Action, select post.

  6. In the post argument text entry box, copy in the following:

    {
      "url": "<API Gateway Endpoint>",
      "body": {
        "message": "%%vars.message",
        "key": "%%vars.key"
      }
    }
    

    Replace API Gateway Endpoint with the API Gateway URL connected to the Lambda function.

    Note

    The message and key variables are defined below.

    The stage cannot directly access the parameters passed to the pipeline. Instead, you bind the parameters to stage-defined variables to access them (using the %%vars expansion).

  7. Enable the Bind data to %%vars switch.

  8. Copy in the following to the %%vars text entry:

    {
      "message" : "%%args.message",
      "key" : "%%args.key"
    }
    

    This binds the variable message to the pipeline parameter message (using the %%args) and the variable key to the pipeline parameter key (using the %%args). The variables message and key are accessible in the stage using the expansion %%vars.

3
4

Add necessary import statements.

The following import statements cover the bare minimum requirements for using MongoDB Stitch for anonymous authentication and interacting with a MongoDB server via the MongoDB Stitch MongoDB service:

import com.google.android.gms.tasks.Continuation;
import com.mongodb.stitch.android.auth.UserProfile;
import com.mongodb.stitch.android.auth.anonymous.AnonymousAuthProvider;
import com.mongodb.stitch.android.services.mongodb.MongoClient;
5

Create and Instantiate StitchClient.

The MongoDB Stitch SDK provides the following class for interfacing with the MongoDB Stitch SDK and the MongoDB Stitch HTTP service:

StitchClient(Context context, String clientAppID)

This object handles interactions between the application and the MongoDB Stitch service, MongoDB Stitch authentication, and MongoDB Stitch pipelines. Takes the current context and a string representing the MongoDB Stitch application ID as parameters.

To locate your application ID, click Clients in the navigation panel and retrieve the value for App ID.

Create and instantiate the StitchClient class in your application. The following example instantiates the class in the onCreate() method of MainActivity.java. It also creates private variables for use with this procedure.

// Remember to replace the APP_ID with your Stitch Application ID

private static final String APP_ID = "STITCH-APP-ID"; //The Stitch Application ID
private static final String TAG = "STITCH-SDK";
private static final String MONGODB_SERVICE_NAME = "MONGODB-SERVICE-NAME";

private StitchClient _client;
private MongoClient _mongoClient;

private String currentRestaurantName;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

Replace the STITCH-APP-ID with your MongoDB Stitch application ID and replace STITCH-HTTP-SERVICE with AWSLambda. To retrieve your MongoDB Stitch application ID, from the MongoDB Stitch UI click Clients. Copy the value of App ID and replace STITCH-APP-ID.

6

Retrieve Authentication Providers from MongoDB Stitch and Perform Anonymous User Authentication.

The MongoDB Stitch SDK provides the following objects and related methods for retrieving the authentication providers configured for the MongoDB Stitch application, as well as performing authentication using a given authentication provider.

StitchClient.getAuthProviders()
Returns a Task<AvailableAuthProviders> object. The AvailableAuthProviders class provides methods for checking which authentication providers are configured for the MongoDB Stitch application.
AvailableAuthProviders.hasAnonymous()
Returns a boolean. If true, the MongoDB Stitch application has anonymous authentication configured.
StitchClient.logInWithProvider(AuthProvider authProvider)
Returns a Task<Auth> object. Takes an AuthProvider object, such as the AnonymousAuthProvider() class.

Use the StitchClient.getAuthProviders() method to retrieve the configured authentication providers for the specified MongoDB Stitch application. Use the StitchClient.logInWithProvider() to log in with the specified authentication provider.

The following example retrieves the authentication providers for the MongoDB Stitch application and, if anonymous authentication is configured, performs anonymous authentication.

_client.getAuthProviders().addOnCompleteListener(new OnCompleteListener<AvailableAuthProviders>() {
    @Override
    public void onComplete(@NonNull final Task<AvailableAuthProviders> task) {
        if (!task.isSuccessful()){
            Log.e(TAG, "Could not retrieve authentication providers");
        } else {
            Log.i(TAG, "Retrieved authentication providers");

            if (task.getResult().hasAnonymous()){
                _client.logInWithProvider(new AnonymousAuthProvider()).continueWith(new Continuation<Auth, Object>() {
                    @Override
                    public Object then(@NonNull final Task<Auth> task) throws Exception {
                        if (task.isSuccessful()) {
                            Log.i(TAG,"User Authenticated as " + _client.getAuth().getUserId());
                        } else {
                            Log.e(TAG, "Error logging in anonymously", task.getException());
                        }
                        return null;
                    }
                });
            }
        }
    }
});
7

Invoke the AWS Lambda function.

The MongoDB Stitch SDK provides the following method for executing a pipeline:

StitchClient.executePipeline(PipelineStage stages)

Takes a StitchClient.PipelineStage object that describes the pipeline to execute.

Returns a Task object containing the result of the pipeline.

The application provides two options for invoking the AWS function:

  • Execute a MongoDB Stitch pipeline using the AWS Lambda HTTP service.

                    } else {
                        TextView res = (TextView) findViewById(R.id.resultFound);
    
                        if (task.getResult().size() == 0) {
                            res.setText("No results found");
                            Log.i(TAG, "Query failed to return any results");
                            clearComments();
                            return null;
                        }
                        res.setText("Restaurant found");
    
                        final Document doc = task.getResult().get(0);
    
                        final TextView cuisine = (TextView) findViewById(R.id.cuisine);
                        cuisine.setText(doc.get("cuisine").toString());
    
                        final TextView location = (TextView) findViewById(R.id.location);
                        location.setText(doc.get("location").toString());
    
                        final List<Document> comments = (List<Document>) doc.get("comments");
                        if (comments.size() > 0) {
    
                            // showComments() passes the list of documents to a custom list adapter.
                            // It then passes the list adapter to a list view, where the comments are displayed.
                            showComments(comments);
                        } else {
                            clearComments();
                        }
                    }
                    return null;
                }
            });
        }
    }
    
    private void clearComments() {
    
        final ListView lv = (ListView) findViewById(R.id.commentList);
        lv.setAdapter(null);
    
    }
    
    public void writeComment(final View view) {
    
  • Execute a MongoDB Stitch pipeline that calls the AWSLambda named pipeline.

        // part of the document.
        if (!_client.isAuthenticated()){
            warnAuth();
            return;
        }
    
        if (currentRestaurantName.matches("")){
            warnSearch();
            return;
        }
    
        final EditText input = new EditText(this);
        input.setInputType(InputType.TYPE_CLASS_TEXT);
    
        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("Write Comment");
        builder.setView(input);
        builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
    
                // The query document uses the name of the currently displayed restaurant
                final Document query = new Document("name",currentRestaurantName);
    
                // This is specific to anonymous authentication.
                // For facebook or google, you can check for a username using _client.getAuth().getUser.getData().get("name")
                final Document newComment = new Document("user_id", _client.getAuth().getUserId());
                newComment.put("comment" , input.getText().toString());
    
                // The $push update operator adds the "newComment" document to the "comment" array.
                // If "comment" does not exist, $push creates the array and adds "newComment" to it.
                final Document update = new Document( "$push" , new Document("comments", newComment));
    
                // This code block performs an "updateOne" operation, updating the document associated to the currently selected restaurant and adding a new comment to the "comment" array.
                // On success, it calls "refreshComments()", which refreshes the List View displaying the comments associated to the restaurant.
                _mongoClient.getDatabase("guidebook").getCollection("restaurants").updateOne(query,update).continueWith(new Continuation<Void, Object>() {
                    @Override
                    public Object then(@NonNull final Task<Void> task) {
                        if (task.isSuccessful()) {
                            refreshComments(view);
                        } else {
                            Log.e(TAG,"Error writing comment");
    

Both methods perform essentially the same function. However, by utilizing a named pipeline, you can modify the pipeline without updating the application. Depending on the scope of changes, such as changes to the expected parameters or values, an application update may still be required.

Example Code

This example code assumes a single-activity application. You can clone or fork the full application code at https://github.com/mongodb/stitch-examples/tree/master/HTTP-service/HTTP-service-android

Substitute STITCH-APP-ID for your MongoDB Stitch application ID.

Substitute HTTP-SERVICE-NAME for the name of the MongoDB Stitch HTTP service you want the application to use.

MainActivity.java

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
package com.mongodb.stitch.examples.mongorestaurant;

import android.content.DialogInterface;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.text.InputType;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;

import com.google.android.gms.tasks.Continuation;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;

import com.mongodb.stitch.android.StitchClient;
import com.mongodb.stitch.android.auth.Auth;
import com.mongodb.stitch.android.auth.AvailableAuthProviders;
import com.mongodb.stitch.android.auth.UserProfile;
import com.mongodb.stitch.android.auth.anonymous.AnonymousAuthProvider;
import com.mongodb.stitch.android.services.mongodb.MongoClient;

import org.bson.Document;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {

    // Remember to replace the APP_ID with your Stitch Application ID

    private static final String APP_ID = "STITCH-APP-ID"; //The Stitch Application ID
    private static final String TAG = "STITCH-SDK";
    private static final String MONGODB_SERVICE_NAME = "MONGODB-SERVICE-NAME";

    private StitchClient _client;
    private MongoClient _mongoClient;

    private String currentRestaurantName;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        _client = new StitchClient(this.getBaseContext(), APP_ID);
        _mongoClient = new MongoClient(_client, MONGODB_SERVICE_NAME);

        currentRestaurantName = "";
        doAnonymousAuthentication();
    }

    private void doAnonymousAuthentication() {

        _client.getAuthProviders().addOnCompleteListener(new OnCompleteListener<AvailableAuthProviders>() {
            @Override
            public void onComplete(@NonNull final Task<AvailableAuthProviders> task) {
                if (!task.isSuccessful()){
                    Log.e(TAG, "Could not retrieve authentication providers");
                } else {
                    Log.i(TAG, "Retrieved authentication providers");

                    if (task.getResult().hasAnonymous()){
                        _client.logInWithProvider(new AnonymousAuthProvider()).continueWith(new Continuation<Auth, Object>() {
                            @Override
                            public Object then(@NonNull final Task<Auth> task) throws Exception {
                                if (task.isSuccessful()) {
                                    Log.i(TAG,"User Authenticated as " + _client.getAuth().getUserId());
                                } else {
                                    Log.e(TAG, "Error logging in anonymously", task.getException());
                                }
                                return null;
                            }
                        });
                    }
                }
            }
        });
    }

    public void searchRestaurant(View view){

        if (!_client.isAuthenticated()){
            warnAuth();
        } else {

            final EditText restaurant = (EditText) findViewById(R.id.searchName);
            currentRestaurantName = restaurant.getText().toString();

            if (currentRestaurantName.matches("")){
                new AlertDialog.Builder(this)
                        .setTitle("Invalid restaurant")
                        .setMessage("Please specify a restaurant name. Try searching again.")
                        .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
                            public void onClick(DialogInterface dialog, int which) {
                            }
                        })
                        .setIcon(android.R.drawable.ic_dialog_alert)
                        .show();
                return;
            }

            final Document query = new Document( "name",currentRestaurantName);
            Log.i(TAG, "Restaurant search query:" + query);

            // This code block is a simple find() command on the "restaurants" collection in the "guidebook" database.
            // The application only cares about the first returned result, even if there are multiple matches.

            _mongoClient.getDatabase("guidebook").getCollection("restaurants").find(query).continueWith(new Continuation<List<Document>, Object>() {

                @Override
                public Object then(@NonNull final Task<List<Document>> task) throws Exception {
                    if (!task.isSuccessful()){
                        Log.e(TAG,"Failed to execute query");
                    } else {
                        TextView res = (TextView) findViewById(R.id.resultFound);

                        if (task.getResult().size() == 0) {
                            res.setText("No results found");
                            Log.i(TAG, "Query failed to return any results");
                            clearComments();
                            return null;
                        }
                        res.setText("Restaurant found");

                        final Document doc = task.getResult().get(0);

                        final TextView cuisine = (TextView) findViewById(R.id.cuisine);
                        cuisine.setText(doc.get("cuisine").toString());

                        final TextView location = (TextView) findViewById(R.id.location);
                        location.setText(doc.get("location").toString());

                        final List<Document> comments = (List<Document>) doc.get("comments");
                        if (comments.size() > 0) {

                            // showComments() passes the list of documents to a custom list adapter.
                            // It then passes the list adapter to a list view, where the comments are displayed.
                            showComments(comments);
                        } else {
                            clearComments();
                        }
                    }
                    return null;
                }
            });
        }
    }

    private void clearComments() {

        final ListView lv = (ListView) findViewById(R.id.commentList);
        lv.setAdapter(null);

    }

    public void writeComment(final View view) {

        // Its important to check for authentication, as the authenticated user ID is used as
        // part of the document.
        if (!_client.isAuthenticated()){
            warnAuth();
            return;
        }

        if (currentRestaurantName.matches("")){
            warnSearch();
            return;
        }

        final EditText input = new EditText(this);
        input.setInputType(InputType.TYPE_CLASS_TEXT);

        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("Write Comment");
        builder.setView(input);
        builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {

                // The query document uses the name of the currently displayed restaurant
                final Document query = new Document("name",currentRestaurantName);

                // This is specific to anonymous authentication.
                // For facebook or google, you can check for a username using _client.getAuth().getUser.getData().get("name")
                final Document newComment = new Document("user_id", _client.getAuth().getUserId());
                newComment.put("comment" , input.getText().toString());

                // The $push update operator adds the "newComment" document to the "comment" array.
                // If "comment" does not exist, $push creates the array and adds "newComment" to it.
                final Document update = new Document( "$push" , new Document("comments", newComment));

                // This code block performs an "updateOne" operation, updating the document associated to the currently selected restaurant and adding a new comment to the "comment" array.
                // On success, it calls "refreshComments()", which refreshes the List View displaying the comments associated to the restaurant.
                _mongoClient.getDatabase("guidebook").getCollection("restaurants").updateOne(query,update).continueWith(new Continuation<Void, Object>() {
                    @Override
                    public Object then(@NonNull final Task<Void> task) {
                        if (task.isSuccessful()) {
                            refreshComments(view);
                        } else {
                            Log.e(TAG,"Error writing comment");
                        }
                        return null;
                    }
                });

            }
        });
        builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.cancel();
            }
        });

        builder.show();
    }

    public void refreshComments(View view) {

        if (currentRestaurantName.matches("")){
            warnSearch();
            return;
        }

        final Document query = new Document("name",currentRestaurantName);

        _mongoClient.getDatabase("guidebook").getCollection("restaurants").find(query).continueWith(new Continuation<List<Document>, Object>() {
            @Override
            public Object then(@NonNull final Task<List<Document>> task) {
                if (!task.isSuccessful()){
                    Log.e(TAG, "Error refreshing comments");
                } else {

                    final Document result = task.getResult().get(0);
                    final List<Document> comments = (List<Document>) result.get("comments");

                    if (comments.size() > 0 ){
                        showComments(comments);
                    } else {
                        clearComments();
                    }
                }
                return null;
            }
        });
    }

    private void showComments(List<Document> comments) {

        final CustomListAdapter cla = new CustomListAdapter(this,comments);

        final ListView lv = (ListView) findViewById(R.id.commentList);
        lv.setAdapter(cla);
    }

    private void warnAuth() {
        new AlertDialog.Builder(this)
                .setTitle("Not Authenticated")
                .setMessage("The application automatically performs anonymous authentication. If you continue to see this message, check for network connectivity")
                .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int which) {
                    }
                })
                .setIcon(android.R.drawable.ic_dialog_alert)
                .show();
        return;
    }

    private void warnSearch() {
        new AlertDialog.Builder(this)
                .setTitle("Invalid restaurant")
                .setMessage("You can only read or write comments for a valid restaurant. Try searching again.")
                .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int which) {
                    }
                })
                .setIcon(android.R.drawable.ic_dialog_alert)
                .show();
        return;
    }

}

activity_main.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Switch
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/switchPipeline"
        android:text="Use Named Pipeline To Cipher or Decipher Message"
        android:checked="true"
        />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/encryptDesc"
        />

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Message To Encrypt"
        android:id="@+id/message"
        />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Secret Key"
        android:textIsSelectable="true"
        android:id="@+id/key"
        />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="CIPHER ME!"
        android:onClick="cipher"
        android:id="@+id/cipherButton"
        />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/cipheredMessage"
        android:textIsSelectable="true"
        />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/decryptDesc"
        />

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Message To Decrypt"
        android:id="@+id/decryptMessage"
        />

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Secret Key"
        android:id="@+id/decryptKey"
        />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="DECIPHER ME!"
        android:onClick="decipher"
        android:id="@+id/decipherButton"
        />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/decipheredMessage"
        android:textIsSelectable="true"
        />

</LinearLayout>

build.grade (Module: app)

Important

There may be more than one build.gradle file in your project. You must edit the build.gradle file associated to the application.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
apply plugin: 'com.android.application'

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"
    defaultConfig {
        applicationId "com.mongodb.stitch.examples.mongocipher"
        minSdkVersion 19
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.2.0'
    compile 'com.android.support.constraint:constraint-layout:1.0.1'
    testCompile 'junit:junit:4.12'

    compile 'org.mongodb:stitch:0.1.0-SNAPSHOT'
}

repositories {

    // TODO: Remove once BSON 3.5.0 is released
    maven {
        url "https://oss.sonatype.org/content/repositories/snapshots"
    }
}

MongoCipher.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def lambda_handler_xor_cipher(message, context):
    
    # AWS passes the HTTP request body as a dictionary to the first parameter
    msg = message.get("message")
    key = message.get("key")
    
    # IMPORTANT: 
    #   If the key length is less than the message, 
    #   zip() truncates the message to match the key length.
    
    if isinstance(msg, str):
        return "".join(chr(ord(a) ^ ord(b)) for a, b in zip(msg, key))
    else:
        # msg and key must be an iterable such as list or tuple
        return bytes([a ^ b for a, b in zip(msg, key)])