diff --git a/src/key.js b/src/key.js
index 5e8e92de..0a428a2d 100644
--- a/src/key.js
+++ b/src/key.js
@@ -195,7 +195,7 @@ Key.prototype.getKeyIds = function() {
  */
 Key.prototype.getUserIds = function() {
   return this.users.map(user => {
-    return user.userId ? util.encode_utf8(user.userId.userid) : null;
+    return user.userId ? user.userId.userid : null;
   }).filter(userid => userid !== null);
 };
 
diff --git a/src/packet/literal.js b/src/packet/literal.js
index bd2c3c88..b2586615 100644
--- a/src/packet/literal.js
+++ b/src/packet/literal.js
@@ -64,23 +64,7 @@ Literal.prototype.setText = function(text, format='utf8') {
  */
 Literal.prototype.getText = function(clone=false) {
   if (this.text === null || util.isStream(this.text)) { // Assume that this.text has been read
-    let lastChar = '';
-    const decoder = new TextDecoder('utf-8');
-    // eslint-disable-next-line no-inner-declarations
-    function process(value, lastChunk=false) {
-      // decode UTF8
-      const text = lastChar + decoder.decode(value, { stream: !lastChunk });
-      // normalize EOL to \n
-      const normalized = util.nativeEOL(text);
-      // if last char is \r, store it for the next chunk so we can normalize \r\n
-      if (normalized[normalized.length - 1] === '\r') {
-        lastChar = '\r';
-        return normalized.slice(0, -1);
-      }
-      lastChar = '';
-      return normalized;
-    }
-    this.text = stream.transform(this.getBytes(clone), process, () => process(new Uint8Array(), true));
+    this.text = util.nativeEOL(util.decode_utf8(this.getBytes(clone)));
   }
   return this.text;
 };
@@ -104,10 +88,8 @@ Literal.prototype.setBytes = function(bytes, format) {
  */
 Literal.prototype.getBytes = function(clone=false) {
   if (this.data === null) {
-    // normalize EOL to \r\n
-    const text = util.canonicalizeEOL(this.text);
-    // encode UTF8
-    this.data = util.str_to_Uint8Array(util.encode_utf8(text));
+    // normalize EOL to \r\n and encode UTF8
+    this.data = util.encode_utf8(util.canonicalizeEOL(this.text));
   }
   if (clone) {
     return stream.passiveClone(this.data);
@@ -146,7 +128,7 @@ Literal.prototype.read = async function(bytes) {
     const format = enums.read(enums.literal, await reader.readByte());
 
     const filename_len = await reader.readByte();
-    this.filename = util.decode_utf8(util.Uint8Array_to_str(await reader.readBytes(filename_len)));
+    this.filename = util.decode_utf8(await reader.readBytes(filename_len));
 
     this.date = util.readDate(await reader.readBytes(4));
 
@@ -162,7 +144,7 @@ Literal.prototype.read = async function(bytes) {
  * @returns {Uint8Array | ReadableStream<Uint8Array>} Uint8Array representation of the packet
  */
 Literal.prototype.write = function() {
-  const filename = util.str_to_Uint8Array(util.encode_utf8(this.filename));
+  const filename = util.encode_utf8(this.filename);
   const filename_length = new Uint8Array([filename.length]);
 
   const format = new Uint8Array([enums.write(enums.literal, this.format)]);
diff --git a/src/packet/signature.js b/src/packet/signature.js
index 53c78224..0121cfd0 100644
--- a/src/packet/signature.js
+++ b/src/packet/signature.js
@@ -525,7 +525,7 @@ Signature.prototype.toSign = function (type, data) {
       // normalize EOL to \r\n
       text = util.canonicalizeEOL(text);
       // encode UTF8
-      return util.str_to_Uint8Array(util.encode_utf8(text));
+      return util.encode_utf8(text);
     }
     case t.standalone:
       return new Uint8Array(0);
diff --git a/src/packet/userid.js b/src/packet/userid.js
index 90ce88e6..8d052421 100644
--- a/src/packet/userid.js
+++ b/src/packet/userid.js
@@ -52,7 +52,7 @@ function Userid() {
  * @param {Uint8Array} input payload of a tag 13 packet
  */
 Userid.prototype.read = function (bytes) {
-  this.parse(util.decode_utf8(util.Uint8Array_to_str(bytes)));
+  this.parse(util.decode_utf8(bytes));
 };
 
 /**
@@ -70,7 +70,7 @@ Userid.prototype.parse = function (userid) {
  * @returns {Uint8Array} binary representation
  */
 Userid.prototype.write = function () {
-  return util.str_to_Uint8Array(util.encode_utf8(this.userid));
+  return util.encode_utf8(this.userid);
 };
 
 /**
diff --git a/src/polyfills.js b/src/polyfills.js
index 6b462083..aa869b1b 100644
--- a/src/polyfills.js
+++ b/src/polyfills.js
@@ -47,9 +47,13 @@ if (typeof window !== 'undefined') {
 if (typeof TransformStream === 'undefined') {
   require('@mattiasbuelens/web-streams-polyfill');
 }
-if (typeof TextDecoder === 'undefined') {
-  global.TextDecoder = util.getNodeTextDecoder();
+if (typeof TextEncoder === 'undefined') {
+  const nodeUtil = util.nodeRequire('util') || {};
+  global.TextEncoder = nodeUtil.TextEncoder;
+  global.TextDecoder = nodeUtil.TextDecoder;
 }
-if (typeof TextDecoder === 'undefined') {
-  global.TextDecoder = require('text-encoding-utf-8').TextDecoder;
+if (typeof TextEncoder === 'undefined') {
+  const textEncoding = require('text-encoding-utf-8');
+  global.TextEncoder = textEncoding.TextEncoder;
+  global.TextDecoder = textEncoding.TextDecoder;
 }
diff --git a/src/type/s2k.js b/src/type/s2k.js
index 223f2f41..ce644941 100644
--- a/src/type/s2k.js
+++ b/src/type/s2k.js
@@ -143,7 +143,7 @@ S2K.prototype.write = function () {
  * hashAlgorithm hash length
  */
 S2K.prototype.produce_key = function (passphrase, numBytes) {
-  passphrase = util.str_to_Uint8Array(util.encode_utf8(passphrase));
+  passphrase = util.encode_utf8(passphrase);
 
   function round(prefix, s2k) {
     const algorithm = enums.write(enums.hash, s2k.algorithm);
diff --git a/src/util.js b/src/util.js
index 6b85474c..495a6260 100644
--- a/src/util.js
+++ b/src/util.js
@@ -309,28 +309,31 @@ export default {
   },
 
   /**
-   * Convert a native javascript string to a string of utf8 bytes
-   * @param {String} str The string to convert
-   * @returns {String} A valid squence of utf8 bytes
+   * Convert a native javascript string to a Uint8Array of utf8 bytes
+   * @param {String|ReadableStream} str The string to convert
+   * @returns {Uint8Array|ReadableStream} A valid squence of utf8 bytes
    */
   encode_utf8: function (str) {
-    return stream.transform(str, value => unescape(encodeURIComponent(value)));
+    const encoder = new TextEncoder('utf-8');
+    // eslint-disable-next-line no-inner-declarations
+    function process(value, lastChunk=false) {
+      return encoder.encode(value, { stream: !lastChunk });
+    }
+    return stream.transform(str, process, () => process('', true));
   },
 
   /**
-   * Convert a string of utf8 bytes to a native javascript string
-   * @param {String} utf8 A valid squence of utf8 bytes
-   * @returns {String} A native javascript string
+   * Convert a Uint8Array of utf8 bytes to a native javascript string
+   * @param {Uint8Array|ReadableStream} utf8 A valid squence of utf8 bytes
+   * @returns {String|ReadableStream} A native javascript string
    */
   decode_utf8: function (utf8) {
-    if (typeof utf8 !== 'string') {
-      throw new Error('Parameter "utf8" is not of type string');
-    }
-    try {
-      return decodeURIComponent(escape(utf8));
-    } catch (e) {
-      return utf8;
+    const decoder = new TextDecoder('utf-8');
+    // eslint-disable-next-line no-inner-declarations
+    function process(value, lastChunk=false) {
+      return decoder.decode(value, { stream: !lastChunk });
     }
+    return stream.transform(utf8, process, () => process(new Uint8Array(), true));
   },
 
   /**
@@ -627,10 +630,6 @@ export default {
     return (util.nodeRequire('stream') || {}).Readable;
   },
 
-  getNodeTextDecoder: function() {
-    return (util.nodeRequire('util') || {}).TextDecoder;
-  },
-
   getHardwareConcurrency: function() {
     if (util.detectNode()) {
       const os = util.nodeRequire('os');
@@ -678,14 +677,24 @@ export default {
    * Normalize line endings to \r\n
    */
   canonicalizeEOL: function(text) {
-    return stream.transform(text, value => value.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r\n"));
+    return stream.transform(util.nativeEOL(text), value => value.replace(/\r/g, "\n").replace(/\n/g, "\r\n"));
   },
 
   /**
    * Convert line endings from canonicalized \r\n to native \n
    */
   nativeEOL: function(text) {
-    return text.replace(/\r\n/g, "\n");
+    let lastChar = '';
+    return stream.transform(text, value => {
+      value = lastChar + value;
+      if (value[value.length - 1] === '\r') {
+        lastChar = '\r';
+        value = value.slice(0, -1);
+      } else {
+        lastChar = '';
+      }
+      return value.replace(/\r\n/g, '\n');
+    }, () => lastChar);
   },
 
   /**
diff --git a/test/general/signature.js b/test/general/signature.js
index 8686274b..dee2e04e 100644
--- a/test/general/signature.js
+++ b/test/general/signature.js
@@ -707,7 +707,7 @@ yYDnCgA=
     await privKey.decrypt('hello world');
     return openpgp.sign({ privateKeys:[privKey], message: openpgp.message.fromText(plaintext), detached: true}).then(async function(signed) {
       const signature = await openpgp.signature.readArmored(signed.signature);
-      return openpgp.verify({ publicKeys:[pubKey], message: openpgp.message.fromBinary(openpgp.util.str_to_Uint8Array(openpgp.util.encode_utf8(plaintext))), signature: signature });
+      return openpgp.verify({ publicKeys:[pubKey], message: openpgp.message.fromBinary(openpgp.util.encode_utf8(plaintext)), signature: signature });
     }).then(function(cleartextSig) {
       expect(cleartextSig).to.exist;
       expect(cleartextSig.signatures).to.have.length(1);
@@ -740,7 +740,7 @@ yYDnCgA=
     await Promise.all([privKey.primaryKey.decrypt('hello world'), privKey.subKeys[0].keyPacket.decrypt('hello world')]);
     return openpgp.sign({ privateKeys:[privKey], message: openpgp.message.fromText(plaintext), detached: true}).then(async function(signed) {
       const signature = await openpgp.signature.readArmored(signed.signature);
-      return openpgp.encrypt({ message: openpgp.message.fromBinary(openpgp.util.str_to_Uint8Array(openpgp.util.encode_utf8(plaintext))), publicKeys: [pubKey], signature })
+      return openpgp.encrypt({ message: openpgp.message.fromBinary(openpgp.util.encode_utf8(plaintext)), publicKeys: [pubKey], signature })
     }).then(async ({ data }) => {
       const csMsg = await openpgp.message.readArmored(data);
       return openpgp.decrypt({ message: csMsg, privateKeys: [ privKey ], publicKeys: [ pubKey ] });
diff --git a/test/general/streaming.js b/test/general/streaming.js
index f4b9e4ac..6bc1e104 100644
--- a/test/general/streaming.js
+++ b/test/general/streaming.js
@@ -431,7 +431,7 @@ function tests() {
       });
       expect(util.isStream(decrypted.data)).to.equal(expectedType);
       const reader = openpgp.stream.getReader(decrypted.data);
-      expect((await reader.peekBytes(256)).toString('utf8').substr(0, 64)).to.equal(plaintext[0]);
+      expect((await reader.peekBytes(plaintext[0].length * 4)).toString('utf8').substr(0, plaintext[0].length)).to.equal(plaintext[0]);
       if (i > 10) throw new Error('Data did not arrive early.');
       expect((await reader.readToEnd()).toString('utf8')).to.equal(util.concat(plaintext));
     } finally {
diff --git a/test/general/testInputs.js b/test/general/testInputs.js
index 2659a50a..8a12cee5 100644
--- a/test/general/testInputs.js
+++ b/test/general/testInputs.js
@@ -3,15 +3,16 @@
  * Generates a 64 character long javascript string out of the whole utf-8 range.
  */
 function createSomeMessage(){
-    const length = 50;
     let arr = [];
-    for (let i= 0; i < length; i++){
-        arr.push(String.fromCharCode(
-            Math.floor(Math.random() * 10174) + 1));
+    for (let i = 0; i < 30; i++) {
+        arr.push(Math.floor(Math.random() * 10174) + 1);
     }
-    return '  \t' + arr.join('').replace(/\r/g, '\n') + '  \t\n한국어/조선말';
+    for (let i = 0; i < 10; i++) {
+        arr.push(0x1F600 + Math.floor(Math.random() * (0x1F64F - 0x1F600)) + 1);
+    }
+    return '  \t' + String.fromCodePoint(...arr).replace(/\r/g, '\n') + '  \t\n한국어/조선말';
 }
 
- module.exports = {
-     createSomeMessage: createSomeMessage
- };
+module.exports = {
+    createSomeMessage: createSomeMessage
+};
diff --git a/test/general/util.js b/test/general/util.js
index a611590a..32b672c4 100644
--- a/test/general/util.js
+++ b/test/general/util.js
@@ -153,10 +153,6 @@ describe('Util unit tests', function() {
   });
 
   describe("Misc.", function() {
-    it('util.decode_utf8 throws error if invalid parameter type', function () {
-      const test = openpgp.util.decode_utf8.bind(null, {chameleon: true});
-      expect(test).to.throw(Error, /Parameter "utf8" is not of type string/);
-    });
     it('util.readNumber should not overflow until full range of uint32', function () {
       const ints = [Math.pow(2, 20), Math.pow(2, 25), Math.pow(2, 30), Math.pow(2, 32) - 1];
       for(let i = 0; i < ints.length; i++) {