diff --git a/package-lock.json b/package-lock.json
index 1aed84d..76c7e2a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26,6 +26,7 @@
         "classnames": "^2.3.1",
         "detectincognitojs": "^1.1.2",
         "fast-memoize": "^2.5.2",
+        "file-saver": "^2.0.5",
         "fun-animal-names": "^0.1.1",
         "idb-chunk-store": "^1.0.1",
         "localforage": "^1.10.0",
@@ -33,6 +34,7 @@
         "querystring": "^0.2.1",
         "react": "^18.2.0",
         "react-dom": "^18.2.0",
+        "react-file-reader-input": "^2.0.0",
         "react-git-info": "^2.0.1",
         "react-markdown": "^8.0.3",
         "react-qrcode-logo": "^2.8.0",
@@ -56,6 +58,8 @@
       },
       "devDependencies": {
         "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
+        "@types/file-saver": "^2.0.7",
+        "@types/react-file-reader-input": "^2.0.4",
         "@types/react-syntax-highlighter": "^15.5.5",
         "@types/streamsaver": "^2.0.1",
         "@types/uuid": "^8.3.4",
@@ -7451,6 +7455,12 @@
         "@types/send": "*"
       }
     },
+    "node_modules/@types/file-saver": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz",
+      "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
+      "dev": true
+    },
     "node_modules/@types/graceful-fs": {
       "version": "4.1.8",
       "license": "MIT",
@@ -7637,6 +7647,15 @@
         "@types/react": "*"
       }
     },
+    "node_modules/@types/react-file-reader-input": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/@types/react-file-reader-input/-/react-file-reader-input-2.0.4.tgz",
+      "integrity": "sha512-Jqrfn+w42j8t8Q3npMXXKPdk+reIM0UHLKVc3ykrA7q7bN3Z62SGhsClZX0+Edlqm66lcKwmDQl+WMm+Xor7Xg==",
+      "dev": true,
+      "dependencies": {
+        "@types/react": "*"
+      }
+    },
     "node_modules/@types/react-syntax-highlighter": {
       "version": "15.5.9",
       "dev": true,
@@ -13101,6 +13120,11 @@
         "url": "https://opencollective.com/webpack"
       }
     },
+    "node_modules/file-saver": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
+      "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
+    },
     "node_modules/filelist": {
       "version": "1.0.4",
       "license": "Apache-2.0",
@@ -22791,6 +22815,15 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/react-file-reader-input": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/react-file-reader-input/-/react-file-reader-input-2.0.0.tgz",
+      "integrity": "sha512-1XgkCpwMnNQsuOIy938UCntz8Xzwt9ECwHaH3cCfIQK1SPpH+y7gCYtqEcb6Rm0hAUq7Lp9+Ljoti9zGMswYrQ==",
+      "peerDependencies": {
+        "react": "^15.0.0 || ^16.0.0",
+        "react-dom": "^15.0.0 || ^16.0.0"
+      }
+    },
     "node_modules/react-git-info": {
       "version": "2.0.1",
       "license": "MIT",
@@ -31878,6 +31911,12 @@
         "@types/send": "*"
       }
     },
+    "@types/file-saver": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz",
+      "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
+      "dev": true
+    },
     "@types/graceful-fs": {
       "version": "4.1.8",
       "requires": {
@@ -32028,6 +32067,15 @@
         "@types/react": "*"
       }
     },
+    "@types/react-file-reader-input": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/@types/react-file-reader-input/-/react-file-reader-input-2.0.4.tgz",
+      "integrity": "sha512-Jqrfn+w42j8t8Q3npMXXKPdk+reIM0UHLKVc3ykrA7q7bN3Z62SGhsClZX0+Edlqm66lcKwmDQl+WMm+Xor7Xg==",
+      "dev": true,
+      "requires": {
+        "@types/react": "*"
+      }
+    },
     "@types/react-syntax-highlighter": {
       "version": "15.5.9",
       "dev": true,
@@ -35366,6 +35414,11 @@
         }
       }
     },
+    "file-saver": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
+      "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
+    },
     "filelist": {
       "version": "1.0.4",
       "requires": {
@@ -41065,6 +41118,12 @@
       "version": "6.0.9",
       "dev": true
     },
+    "react-file-reader-input": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/react-file-reader-input/-/react-file-reader-input-2.0.0.tgz",
+      "integrity": "sha512-1XgkCpwMnNQsuOIy938UCntz8Xzwt9ECwHaH3cCfIQK1SPpH+y7gCYtqEcb6Rm0hAUq7Lp9+Ljoti9zGMswYrQ==",
+      "requires": {}
+    },
     "react-git-info": {
       "version": "2.0.1",
       "requires": {
diff --git a/package.json b/package.json
index 5c46657..0e82bf4 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
     "classnames": "^2.3.1",
     "detectincognitojs": "^1.1.2",
     "fast-memoize": "^2.5.2",
+    "file-saver": "^2.0.5",
     "fun-animal-names": "^0.1.1",
     "idb-chunk-store": "^1.0.1",
     "localforage": "^1.10.0",
@@ -29,6 +30,7 @@
     "querystring": "^0.2.1",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
+    "react-file-reader-input": "^2.0.0",
     "react-git-info": "^2.0.1",
     "react-markdown": "^8.0.3",
     "react-qrcode-logo": "^2.8.0",
@@ -89,6 +91,8 @@
   },
   "devDependencies": {
     "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
+    "@types/file-saver": "^2.0.7",
+    "@types/react-file-reader-input": "^2.0.4",
     "@types/react-syntax-highlighter": "^15.5.5",
     "@types/streamsaver": "^2.0.1",
     "@types/uuid": "^8.3.4",
@@ -123,6 +127,10 @@
     "@svgr/plugin-svgo": {
       "nth-check": "2.0.1"
     },
+    "react-file-reader-input": {
+      "react": "$react",
+      "react-dom": "$react-dom"
+    },
     "resolve-url-loader": {
       "postcss": "8.4.31"
     }
diff --git a/src/pages/Settings/Settings.tsx b/src/pages/Settings/Settings.tsx
index a6ff906..cfee265 100644
--- a/src/pages/Settings/Settings.tsx
+++ b/src/pages/Settings/Settings.tsx
@@ -1,4 +1,5 @@
 import { ChangeEvent, useContext, useEffect, useState } from 'react'
+import FileReaderInput, { Result } from 'react-file-reader-input'
 import Box from '@mui/material/Box'
 import Button from '@mui/material/Button'
 import Typography from '@mui/material/Typography'
@@ -7,21 +8,26 @@ import Switch from '@mui/material/Switch'
 import FormGroup from '@mui/material/FormGroup'
 import FormControlLabel from '@mui/material/FormControlLabel'
 import Paper from '@mui/material/Paper'
+import useTheme from '@mui/material/styles/useTheme'
 
+import { settingsService } from 'services/Settings'
 import { NotificationService } from 'services/Notification'
 import { ShellContext } from 'contexts/ShellContext'
 import { StorageContext } from 'contexts/StorageContext'
+import { SettingsContext } from 'contexts/SettingsContext'
 import { PeerNameDisplay } from 'components/PeerNameDisplay'
+import { ConfirmDialog } from 'components/ConfirmDialog'
 
-import { ConfirmDialog } from '../../components/ConfirmDialog'
-import { SettingsContext } from '../../contexts/SettingsContext'
+import { isErrorWithMessage } from '../../utils'
 
 interface SettingsProps {
   userId: string
 }
 
 export const Settings = ({ userId }: SettingsProps) => {
-  const { setTitle } = useContext(ShellContext)
+  const theme = useTheme()
+
+  const { setTitle, showAlert } = useContext(ShellContext)
   const { updateUserSettings, getUserSettings } = useContext(SettingsContext)
   const { getPersistedStorage } = useContext(StorageContext)
   const [
@@ -85,17 +91,41 @@ export const Settings = ({ userId }: SettingsProps) => {
     window.location.reload()
   }
 
+  const handleExportSettingsClick = async () => {
+    try {
+      await settingsService.exportSettings(getUserSettings())
+    } catch (e) {
+      if (isErrorWithMessage(e)) {
+        showAlert(e.message, { severity: 'error' })
+      }
+    }
+  }
+
+  const handleImportSettingsClick = async ([[, file]]: Result[]) => {
+    try {
+      const userSettings = await settingsService.importSettings(file)
+
+      updateUserSettings(userSettings)
+
+      showAlert('Profile successfully imported', { severity: 'success' })
+    } catch (e) {
+      if (isErrorWithMessage(e)) {
+        showAlert(e.message, { severity: 'error' })
+      }
+    }
+  }
+
   const areNotificationsAvailable = NotificationService.permission === 'granted'
 
   return (
     <Box className="max-w-3xl mx-auto p-4">
       <Typography
         variant="h2"
-        sx={theme => ({
+        sx={{
           fontSize: theme.typography.h3.fontSize,
           fontWeight: theme.typography.fontWeightMedium,
           mb: 2,
-        })}
+        }}
       >
         Chat
       </Typography>
@@ -137,44 +167,111 @@ export const Settings = ({ userId }: SettingsProps) => {
             label="Show active typing indicators"
           />
         </FormGroup>
-        <Typography variant="subtitle2" sx={_theme => ({})}>
+        <Typography variant="subtitle2">
           Disabling this will also hide your active typing status from others.
         </Typography>
       </Paper>
       <Divider sx={{ my: 2 }} />
       <Typography
         variant="h2"
-        sx={theme => ({
+        sx={{
           fontSize: theme.typography.h3.fontSize,
           fontWeight: theme.typography.fontWeightMedium,
           mb: 2,
-        })}
+        }}
       >
         Data
       </Typography>
       <Typography
         variant="h2"
-        sx={theme => ({
+        sx={{
           fontSize: theme.typography.h5.fontSize,
           fontWeight: theme.typography.fontWeightMedium,
           mb: 1.5,
-        })}
+        }}
       >
-        Delete all settings data
+        Export profile data
       </Typography>
       <Typography
         variant="body1"
-        sx={_theme => ({
+        sx={{
           mb: 2,
-        })}
+        }}
+      >
+        Export your Chitchatter profile data so that it can be moved to another
+        browser or device.{' '}
+        <strong>Be careful not to share the exported data with anyone</strong>.
+        It contains your unique verification keys.
+      </Typography>
+      <Button
+        variant="outlined"
+        sx={{
+          mb: 2,
+        }}
+        onClick={handleExportSettingsClick}
+      >
+        Export profile data
+      </Button>
+      <Typography
+        variant="h2"
+        sx={{
+          fontSize: theme.typography.h5.fontSize,
+          fontWeight: theme.typography.fontWeightMedium,
+          mb: 1.5,
+        }}
+      >
+        Import profile data
+      </Typography>
+      <Typography
+        variant="body1"
+        sx={{
+          mb: 2,
+        }}
+      >
+        Import your Chitchatter profile that was previously exported from
+        another browser or device.
+      </Typography>
+      <FileReaderInput
+        {...{
+          as: 'text',
+          onChange: (_e, results) => {
+            handleImportSettingsClick(results)
+          },
+        }}
+      >
+        <Button
+          color="warning"
+          variant="outlined"
+          sx={{
+            mb: 2,
+          }}
+        >
+          Import profile data
+        </Button>
+      </FileReaderInput>
+      <Typography
+        variant="h2"
+        sx={{
+          fontSize: theme.typography.h5.fontSize,
+          fontWeight: theme.typography.fontWeightMedium,
+          mb: 1.5,
+        }}
+      >
+        Delete all profile data
+      </Typography>
+      <Typography
+        variant="body1"
+        sx={{
+          mb: 2,
+        }}
       >
         <strong>Be careful with this</strong>. This will cause your user name to
         change from{' '}
         <strong>
           <PeerNameDisplay
-            sx={theme => ({
+            sx={{
               fontWeight: theme.typography.fontWeightMedium,
-            })}
+            }}
           >
             {userId}
           </PeerNameDisplay>
@@ -185,9 +282,9 @@ export const Settings = ({ userId }: SettingsProps) => {
       <Button
         variant="outlined"
         color="error"
-        sx={_theme => ({
+        sx={{
           mb: 2,
-        })}
+        }}
         onClick={handleDeleteSettingsClick}
       >
         Delete all data and restart
@@ -199,9 +296,9 @@ export const Settings = ({ userId }: SettingsProps) => {
       />
       <Typography
         variant="subtitle2"
-        sx={_theme => ({
+        sx={{
           mb: 2,
-        })}
+        }}
       >
         Chitchatter only stores user preferences and never message content of
         any kind. This preference data is only stored locally on your device and
diff --git a/src/services/Serialization/Serialization.ts b/src/services/Serialization/Serialization.ts
index 4d1016a..8d3a25b 100644
--- a/src/services/Serialization/Serialization.ts
+++ b/src/services/Serialization/Serialization.ts
@@ -1,4 +1,4 @@
-import { UserSettings } from 'models/settings'
+import { ColorMode, UserSettings } from 'models/settings'
 import { AllowedKeyType, encryptionService } from 'services/Encryption'
 
 export interface SerializedUserSettings
@@ -7,6 +7,31 @@ export interface SerializedUserSettings
   privateKey: string
 }
 
+export const isSerializedUserSettings = (
+  data: any
+): data is SerializedUserSettings => {
+  return (
+    typeof data === 'object' &&
+    data !== null &&
+    'colorMode' in data &&
+    Object.values(ColorMode).includes(data.colorMode) &&
+    'userId' in data &&
+    typeof data.userId === 'string' &&
+    'customUsername' in data &&
+    typeof data.customUsername === 'string' &&
+    'playSoundOnNewMessage' in data &&
+    typeof data.playSoundOnNewMessage === 'boolean' &&
+    'showNotificationOnNewMessage' in data &&
+    typeof data.showNotificationOnNewMessage === 'boolean' &&
+    'showActiveTypingStatus' in data &&
+    typeof data.showActiveTypingStatus === 'boolean' &&
+    'publicKey' in data &&
+    typeof data.publicKey === 'string' &&
+    'privateKey' in data &&
+    typeof data.privateKey === 'string'
+  )
+}
+
 export class SerializationService {
   serializeUserSettings = async (
     userSettings: UserSettings
diff --git a/src/services/Settings/Settings.ts b/src/services/Settings/Settings.ts
new file mode 100644
index 0000000..62da5dc
--- /dev/null
+++ b/src/services/Settings/Settings.ts
@@ -0,0 +1,80 @@
+import { saveAs } from 'file-saver'
+
+import { UserSettings } from 'models/settings'
+import { encryptionService } from 'services/Encryption'
+import {
+  isSerializedUserSettings,
+  serializationService,
+} from 'services/Serialization/Serialization'
+
+class InvalidFileError extends Error {
+  message = 'InvalidFileError: File could not be imported'
+}
+
+const encryptionTestTarget = 'chitchatter'
+
+export class SettingsService {
+  exportSettings = async (userSettings: UserSettings) => {
+    const serializedUserSettings =
+      await serializationService.serializeUserSettings(userSettings)
+
+    const blob = new Blob([JSON.stringify(serializedUserSettings)], {
+      type: 'application/json;charset=utf-8',
+    })
+
+    saveAs(blob, `chitchatter-profile-${userSettings.userId}.json`)
+  }
+
+  importSettings = async (file: File) => {
+    const fileReader = new FileReader()
+
+    const promise = new Promise<UserSettings>((resolve, reject) => {
+      fileReader.addEventListener('loadend', async evt => {
+        try {
+          const fileReaderResult = evt.target?.result
+
+          if (typeof fileReaderResult !== 'string') {
+            throw new Error()
+          }
+
+          const parsedFileResult = JSON.parse(fileReaderResult)
+
+          if (!isSerializedUserSettings(parsedFileResult)) {
+            throw new Error()
+          }
+
+          const deserializedUserSettings =
+            await serializationService.deserializeUserSettings(parsedFileResult)
+
+          const encryptedString = await encryptionService.encryptString(
+            deserializedUserSettings.publicKey,
+            encryptionTestTarget
+          )
+
+          const decryptedString = await encryptionService.decryptString(
+            deserializedUserSettings.privateKey,
+            encryptedString
+          )
+
+          // NOTE: This determines whether the public and private keys match
+          // and are compatible with Chitchatter.
+          if (decryptedString !== encryptionTestTarget) {
+            throw new Error()
+          }
+
+          resolve(deserializedUserSettings)
+        } catch (e) {
+          const err = new InvalidFileError()
+          console.error(err)
+          reject(err)
+        }
+      })
+
+      fileReader.readAsText(file.slice())
+    })
+
+    return promise
+  }
+}
+
+export const settingsService = new SettingsService()
diff --git a/src/services/Settings/index.ts b/src/services/Settings/index.ts
new file mode 100644
index 0000000..6db39fa
--- /dev/null
+++ b/src/services/Settings/index.ts
@@ -0,0 +1 @@
+export * from './Settings'
diff --git a/src/utils.ts b/src/utils.ts
index 9c92d2f..57114dc 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -14,3 +14,12 @@ export const isRecord = (variable: any): variable is Record<string, any> => {
 export const isError = (e: any): e is Error => {
   return e instanceof Error
 }
+
+export const isErrorWithMessage = (e: unknown): e is { message: string } => {
+  return (
+    typeof e === 'object' &&
+    e !== null &&
+    'message' in e &&
+    typeof e.message === 'string'
+  )
+}