Skip to content

Encryption/Decryption support for files #1016

Description

@yittoo

Context

Currently react-native-pdf is not able to decrypt the encrypted sensitive files files. And it is not immediately cleaning up the files even when cache is set to false.

react-native-blob-util has a feature for file transformers, that can be optionally, however react-native-pdf package does not make use of it in current state.

Suggestion

  • Adding boolean prop support that enables usage of ReactNativeBlobUtil.fs.readFileWithTransform
  • Cleaning up the decrypted temporary view file cache upon unmount to disallow restricted access from other applications

Possible approach - patch-package (quick dirty POC state atm - it works but it doesn't fit the code practises 100% the library follows and can be adjusted)

I am willing to create a PR and do the adjustments if the approach is welcome

Patch package

diff --git a/node_modules/react-native-pdf/index.d.ts b/node_modules/react-native-pdf/index.d.ts
index cfcea72..396b7d2 100644
--- a/node_modules/react-native-pdf/index.d.ts
+++ b/node_modules/react-native-pdf/index.d.ts
@@ -56,6 +56,7 @@ export interface PdfProps {
     fitPolicy?: 0 | 1 | 2,
     trustAllCerts?: boolean,
     singlePage?: boolean,
+    encrypted?: boolean,
     onLoadProgress?: (percent: number,) => void,
     onLoadComplete?: (numberOfPages: number, path: string, size: {height: number, width: number}, tableContents?: TableContent[]) => void,
     onPageChanged?: (page: number, numberOfPages: number) => void,
diff --git a/node_modules/react-native-pdf/index.js b/node_modules/react-native-pdf/index.js
index 56df005..208f021 100644
--- a/node_modules/react-native-pdf/index.js
+++ b/node_modules/react-native-pdf/index.js
@@ -57,6 +57,7 @@ export default class Pdf extends Component {
         fitPolicy: PropTypes.number,
         trustAllCerts: PropTypes.bool,
         singlePage: PropTypes.bool,
+        encrypted: PropTypes.bool,
         onLoadComplete: PropTypes.func,
         onPageChanged: PropTypes.func,
         onError: PropTypes.func,
@@ -93,6 +94,7 @@ export default class Pdf extends Component {
         trustAllCerts: true,
         usePDFKit: true,
         singlePage: false,
+        encrypted: false,
         onLoadProgress: (percent) => {
         },
         onLoadComplete: (numberOfPages, path) => {
@@ -119,6 +121,7 @@ export default class Pdf extends Component {
         };

         this.lastRNBFTask = null;
+        this.lastViewFile = null;

     }

@@ -131,10 +134,12 @@ export default class Pdf extends Component {
             // if has download task, then cancel it.
             if (this.lastRNBFTask && this.lastRNBFTask.cancel) {
                 this.lastRNBFTask.cancel(err => {
+                    this._cleanupViewFile();
                     this._loadFromSource(this.props.source);
                 });
                 this.lastRNBFTask = null;
             } else {
+                this._cleanupViewFile();
                 this._loadFromSource(this.props.source);
             }
         }
@@ -153,8 +158,27 @@ export default class Pdf extends Component {
             this.lastRNBFTask = null;
         }

+        this._cleanupViewFile();
+
     }

+    _cleanupViewFile = () => {
+        if (this.lastViewFile) {
+            const viewFile = this.lastViewFile;
+            this.lastViewFile = null;
+            this._unlinkFile(viewFile);
+        }
+    };
+
+    _decryptToViewFile = async (encryptedPath) => {
+        const viewFile = encryptedPath + '.view';
+        this._unlinkFile(viewFile);
+        const base64 = await ReactNativeBlobUtil.fs.readFileWithTransform(encryptedPath, 'base64');
+        await ReactNativeBlobUtil.fs.writeFile(viewFile, base64, 'base64');
+        this.lastViewFile = viewFile;
+        return viewFile;
+    };
+
     _loadFromSource = (newSource) => {

         const source = Image.resolveAssetSource(newSource) || {};
@@ -170,10 +194,17 @@ export default class Pdf extends Component {
         if (source.cache) {
             ReactNativeBlobUtil.fs
                 .stat(cacheFile)
-                .then(stats => {
+                .then(async stats => {
                     if (!Boolean(source.expiration) || (source.expiration * 1000 + stats.lastModified) > (new Date().getTime())) {
-                        if (this._mounted) {
-                            this.setState({path: cacheFile, isDownloaded: true});
+                        try {
+                            const finalPath = this.props.encrypted
+                                ? await this._decryptToViewFile(cacheFile)
+                                : cacheFile;
+                            if (this._mounted) {
+                                this.setState({path: finalPath, isDownloaded: true});
+                            }
+                        } catch (e) {
+                            this._onError(e);
                         }
                     } else {
                         // cache expirated then reload it
@@ -234,10 +265,25 @@ export default class Pdf extends Component {
                         });
                 } else {
                     if (this._mounted) {
-                       this.setState({
-                            path: decodeURIComponent(uri.replace(/file:\/\//i, '')),
-                            isDownloaded: true,
-                        });
+                      const localPath = decodeURIComponent(uri.replace(/file:\/\//i, ''));
+                      if (this.props.encrypted) {
+                          try {
+                              const viewFile = await this._decryptToViewFile(localPath);
+                              if (this._mounted) {
+                                  this.setState({
+                                      path: viewFile,
+                                      isDownloaded: true,
+                                  });
+                              }
+                          } catch (e) {
+                              this._onError(e);
+                          }
+                      } else {
+                          this.setState({
+                              path: localPath,
+                              isDownloaded: true,
+                          });
+                      }
                     }
                 }
             } else {
@@ -265,6 +311,7 @@ export default class Pdf extends Component {
             // response data will be saved to this path if it has access right.
             path: tempCacheFile,
             trusty: this.props.trustAllCerts,
+            transformFile: !!this.props.encrypted,
         })
             .fetch(
                 source.method ? source.method : 'GET',
@@ -288,7 +335,7 @@ export default class Pdf extends Component {

                 this.lastRNBFTask = null;

-                if (res && res.respInfo && res.respInfo.headers && !res.respInfo.headers["Content-Encoding"] && !res.respInfo.headers["Transfer-Encoding"] && res.respInfo.headers["Content-Length"]) {
+                if (!this.props.encrypted && res && res.respInfo && res.respInfo.headers && !res.respInfo.headers["Content-Encoding"] && !res.respInfo.headers["Transfer-Encoding"] && res.respInfo.headers["Content-Length"]) {
                     const expectedContentLength = res.respInfo.headers["Content-Length"];
                     let actualContentLength;

@@ -312,11 +359,19 @@ export default class Pdf extends Component {
                 this._unlinkFile(cacheFile);
                 ReactNativeBlobUtil.fs
                     .cp(tempCacheFile, cacheFile)
-                    .then(() => {
-                        if (this._mounted) {
-                            this.setState({path: cacheFile, isDownloaded: true, progress: 1});
+                    .then(async () => {
+                        try {
+                            const finalPath = this.props.encrypted
+                                ? await this._decryptToViewFile(cacheFile)
+                                : cacheFile;
+                            if (this._mounted) {
+                                this.setState({path: finalPath, isDownloaded: true, progress: 1});
+                            }
+                            this._unlinkFile(tempCacheFile);
+                        } catch (e) {
+                            this._unlinkFile(tempCacheFile);
+                            this._onError(e);
                         }
-                        this._unlinkFile(tempCacheFile);
                     })
                     .catch(async (error) => {
                         throw error;

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions