From f78a02bb8d9b7c5c7e97f498324f402b9001c792 Mon Sep 17 00:00:00 2001
From: Alan Grainger <alan.grainger@gmail.com>
Date: Sun, 3 Nov 2024 19:48:32 +0100
Subject: [PATCH] Expire asset decryption tokens

---
 .github/workflows/ci.yaml |  4 ++--
 app/package.json          |  2 +-
 app/src/immich.ts         | 16 ++++++++++++++--
 app/src/index.ts          | 17 ++++++++++++-----
 4 files changed, 29 insertions(+), 10 deletions(-)

diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index fc9e8e2..ebd62e7 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -53,7 +53,7 @@ jobs:
           build-args: |
             PACKAGE_VERSION=${{ env.PACKAGE_VERSION }}
           tags: |
-            ${{ github.repository }}:latest
             ${{ github.repository }}:${{ env.PACKAGE_VERSION }}
-            ghcr.io/${{ github.repository }}:latest
+            ${{ github.repository }}:latest
             ghcr.io/${{ github.repository }}:${{ env.PACKAGE_VERSION }}
+            ghcr.io/${{ github.repository }}:latest
diff --git a/app/package.json b/app/package.json
index d651891..516d102 100644
--- a/app/package.json
+++ b/app/package.json
@@ -1,6 +1,6 @@
 {
   "name": "immich-public-proxy",
-  "version": "1.3.5",
+  "version": "1.3.6",
   "scripts": {
     "dev": "ts-node src/index.ts",
     "build": "npx tsc",
diff --git a/app/src/immich.ts b/app/src/immich.ts
index 75f794b..ab0fa94 100644
--- a/app/src/immich.ts
+++ b/app/src/immich.ts
@@ -187,7 +187,7 @@ class Immich {
   photoUrl (key: string, id: string, size?: ImageSize, password?: string) {
     const params = { key, size }
     if (password) {
-      Object.assign(params, encrypt(password))
+      Object.assign(params, this.encryptPassword(password))
     }
     return this.buildUrl(`/photo/${key}/${id}`, params)
   }
@@ -196,7 +196,7 @@ class Immich {
    * Return the video data URL for a video
    */
   videoUrl (key: string, id: string, password?: string) {
-    const params = password ? encrypt(password) : {}
+    const params = password ? this.encryptPassword(password) : {}
     return this.buildUrl(`/video/${key}/${id}`, params)
   }
 
@@ -215,6 +215,18 @@ class Immich {
   isKey (key: string) {
     return !!key.match(/^[\w-]+$/)
   }
+
+  /**
+   * When loading assets from a password-protected link, make the decryption key valid for a
+   * short time. If the visitor loads the share link again, it will renew that expiry time.
+   * This prevents people from sharing the image links and bypassing password protection.
+   */
+  encryptPassword (password: string) {
+    return encrypt(JSON.stringify({
+      password,
+      expires: dayjs().add(1, 'hour').format()
+    }))
+  }
 }
 
 const immich = new Immich()
diff --git a/app/src/index.ts b/app/src/index.ts
index 2ee37b8..63aab74 100644
--- a/app/src/index.ts
+++ b/app/src/index.ts
@@ -37,13 +37,20 @@ app.get('/:type(photo|video)/:key/:id', async (req, res) => {
   res.set('Cache-Control', 'public, max-age=' + process.env.CACHE_AGE)
   // Check for valid key and ID
   if (immich.isKey(req.params.key) && immich.isId(req.params.id)) {
-    // Decrypt the password, if one was provided
     let password
+    // Validate the password payload, if one was provided
     if (req.query?.cr && req.query?.iv) {
-      password = decrypt({
-        iv: toString(req.query.iv),
-        cr: toString(req.query.cr)
-      })
+      try {
+        const payload = JSON.parse(decrypt({
+          iv: toString(req.query.iv),
+          cr: toString(req.query.cr)
+        }))
+        if (payload?.expires && dayjs(payload.expires) > dayjs()) {
+          password = payload.password
+        } else {
+          log(`Attempted to load assets from ${req.params.key} with an expired decryption token`)
+        }
+      } catch (e) { }
     }
     // Check if the key is a valid share link
     const sharedLink = (await immich.getShareByKey(req.params.key, password))?.link