From efa180fce86e27aeba9d51b76e3e864c57899a99 Mon Sep 17 00:00:00 2001 From: Boris Yankov Date: Wed, 1 Aug 2018 01:58:22 +0300 Subject: [PATCH 1/2] encoding: Add `strToBase64` function and tests The function encodes a JavaScript string to a string in Base64 format. The way to reliably encode strings to Base64 is described in many places, including: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/btoa#Unicode_strings Instead of the `btoa` function we use `base64.encode` from our external base64 library to prevent the error: 'InvalidCharacterError: The string to be encoded contains characters outside of the Latin1 range.' Note: JavaScript strings are UCS-2 or UTF-16 (not UTF-8) http://es5.github.io/x2.html --- src/utils/__tests__/encoding-test.js | 32 +++++++++++++++++++++++++++- src/utils/encoding.js | 5 +++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/utils/__tests__/encoding-test.js b/src/utils/__tests__/encoding-test.js index 2c870c4e566..9d53c8cbeaf 100644 --- a/src/utils/__tests__/encoding-test.js +++ b/src/utils/__tests__/encoding-test.js @@ -5,6 +5,7 @@ import { hexToAscii, asciiToHex, xorHexStrings, + strToBase64, extractApiKey, } from '../encoding'; @@ -61,11 +62,40 @@ describe('asciiToHex', () => { }); }); +describe('strToBase64', () => { + test('can handle an empty string', () => { + const obj = ''; + const expected = ''; + + const result = strToBase64(obj); + + expect(result).toBe(expected); + }); + + test('can encode any string', () => { + const obj = { + key: 'ABCabc123', + empty: null, + array: [1, 2, 3], + }; + const expected = 'W29iamVjdCBPYmplY3Rd'; + + const result = strToBase64(obj); + + expect(result).toBe(expected); + }); + + test('supports unicode characters', () => { + const obj = { key: '😇😈' }; + const expected = 'W29iamVjdCBPYmplY3Rd'; + const result = strToBase64(obj); + expect(result).toBe(expected); + }); +}); describe('extractApiKey', () => { test('correctly extracts an API key that has been XORed with a OTP', () => { const key = 'testing'; const otp = 'A8348A93A83493'; - const encoded = xorHexStrings(asciiToHex(key), otp); expect(extractApiKey(encoded, otp)).toBe(key); }); diff --git a/src/utils/encoding.js b/src/utils/encoding.js index 3e2d9cdf8e2..6908022685a 100644 --- a/src/utils/encoding.js +++ b/src/utils/encoding.js @@ -32,6 +32,11 @@ export const base64ToHex = (bytes: string) => asciiToHex(base64.decode(bytes)); export const hexToBase64 = (hex: string) => base64.encode(hexToAscii(hex)); +// https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding +// `base64.encode` used instead of `btoa` to support Unicode input +export const strToBase64 = (text: string): string => + base64.encode(unescape(encodeURIComponent(text))); + // Extract an API key encoded as a hex string XOR'ed with a one time pad (OTP) // (this is used during the OAuth flow) export const extractApiKey = (encoded: string, otp: string) => From 38aa57a4eb55a121135380ca847475d8db5e0cc9 Mon Sep 17 00:00:00 2001 From: Boris Yankov Date: Wed, 1 Aug 2018 14:54:40 +0300 Subject: [PATCH 2/2] webview: Encode input messages in base64 Fixes #2505 #2538 #2558 When using `postMessage` to communicate with the WebView, depending on the content of the message we send, an exception might be thrown. A minimal but reliable way to reproduce the issue is to send the string `'%22'`. Other character combinations might also cause issues. Messages are sent to the WebView in different ways for iOS/Android. This explains why the issue this is fixing is Android-specific. In iOS, a custom navigation scheme is used to pass the data into the webview (the RCTJSNavigationScheme constant one can grep for) More interesting source code reading on that can be done if one looks at `webViewDidFinishLoad` in `RTCWebView.m`. The Android messaging happens in `receiveCommand` in `ReactWebViewManager.java` It is passed by navigating to a URL of the type `javascript:(.....)` which is a standard way of injecting JavaScript into webpages. The issue comes from the fact that `loadUrl` since Android 4.4 does an URL decode on the string passed to it: https://developer.android.com/reference/android/webkit/WebView#loadUrl(java.lang.String) `evaluateJavascript` does not: https://developer.android.com/reference/android/webkit/WebView.html#evaluateJavascript(java.lang.String,%20android.webkit.ValueCallback%3Cjava.lang.String%3E) --- src/webview/MessageListWeb.js | 3 ++- src/webview/js/generatedEs3.js | 3 ++- src/webview/js/js.js | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/webview/MessageListWeb.js b/src/webview/MessageListWeb.js index dc760b11e1c..ee1d00e2660 100644 --- a/src/webview/MessageListWeb.js +++ b/src/webview/MessageListWeb.js @@ -10,6 +10,7 @@ import getHtml from './html/html'; import renderMessagesAsHtml from './html/renderMessagesAsHtml'; import { getInputMessages } from './webViewHandleUpdates'; import * as webViewEventHandlers from './webViewEventHandlers'; +import { strToBase64 } from '../utils/encoding'; export default class MessageListWeb extends Component { context: Context; @@ -29,7 +30,7 @@ export default class MessageListWeb extends Component { sendMessages = (messages: WebviewInputMessage[]): void => { if (this.webview && messages.length > 0) { - this.webview.postMessage(JSON.stringify(messages), '*'); + this.webview.postMessage(strToBase64(JSON.stringify(messages)), '*'); } }; diff --git a/src/webview/js/generatedEs3.js b/src/webview/js/generatedEs3.js index ae6ab929d3f..450df8a1b65 100644 --- a/src/webview/js/generatedEs3.js +++ b/src/webview/js/generatedEs3.js @@ -241,7 +241,8 @@ var messageHandlers = { document.addEventListener('message', function (e) { scrollEventsDisabled = true; - var messages = JSON.parse(e.data); + var decodedData = decodeURIComponent(escape(window.atob(e.data))); + var messages = JSON.parse(decodedData); messages.forEach(function (msg) { messageHandlers[msg.type](msg); }); diff --git a/src/webview/js/js.js b/src/webview/js/js.js index 6e8e01bb21d..dd1983bb6d9 100644 --- a/src/webview/js/js.js +++ b/src/webview/js/js.js @@ -261,7 +261,8 @@ const messageHandlers = { document.addEventListener('message', e => { scrollEventsDisabled = true; // $FlowFixMe - const messages: WebviewInputMessage[] = JSON.parse(e.data); + const decodedData = decodeURIComponent(escape(window.atob(e.data))); + const messages: WebviewInputMessage[] = JSON.parse(decodedData); messages.forEach((msg: WebviewInputMessage) => { // $FlowFixMe messageHandlers[msg.type](msg);