Skip to content

Commit

Permalink
Merge pull request #272 from WardPearce/feature/improved-access-codes
Browse files Browse the repository at this point in the history
Added access code with kdf
  • Loading branch information
WardPearce authored Sep 10, 2023
2 parents e271769 + 9c88c65 commit 9fa5a6d
Show file tree
Hide file tree
Showing 17 changed files with 206 additions and 41 deletions.
15 changes: 13 additions & 2 deletions backend/paaster/app/controllers/paste.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
from app.env import SETTINGS
from app.helpers.paste import Paste
from app.helpers.s3 import format_file_path, s3_create_client
from app.models.paste import PasteCreatedModel, PasteModel, UpdatePasteModel
from app.models.paste import (
PasteAccessCodeKdf,
PasteCreatedModel,
PasteModel,
UpdatePasteModel,
)
from app.state import State
from litestar import Request, Router, delete, get, post
from litestar.exceptions import HTTPException
Expand Down Expand Up @@ -127,6 +132,12 @@ async def get_paste(
return await Paste(state, paste_id).get(access_code=access_code)


@get("/{paste_id:str}/kdf")
async def get_paste_kdf(state: State, paste_id: str) -> PasteAccessCodeKdf:
return await Paste(state, paste_id).access_code_kdf()


router = Router(
path="/paste", route_handlers=[create_paste, get_paste, delete_paste, update_paste]
path="/paste",
route_handlers=[create_paste, get_paste, delete_paste, update_paste, get_paste_kdf],
)
53 changes: 48 additions & 5 deletions backend/paaster/app/helpers/paste.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from collections.abc import Mapping
from datetime import datetime, timedelta
from secrets import token_urlsafe
from typing import Any, Optional

import bcrypt
from app.env import SETTINGS
from app.helpers.s3 import format_file_path, s3_create_client
from app.models.paste import PasteModel, UpdatePasteModel
from app.models.paste import PasteAccessCodeKdf, PasteModel, UpdatePasteModel
from app.state import State
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
Expand All @@ -24,11 +25,29 @@ async def delete(self, owner_secret: str) -> None:
await self.__delete()

async def update(self, update: UpdatePasteModel, owner_secret: str) -> None:
await self.__validate_owner(owner_secret)
paste = await self.__validate_owner(owner_secret)

to_set = update.dict(exclude_unset=True)
if "access_code" in to_set:
to_set["access_code"] = PASSWORD_HASHER.hash(to_set["access_code"])
to_set["access_code"] = {
**to_set["access_code"],
"code": PASSWORD_HASHER.hash(to_set["access_code"]["code"]),
}
to_set["download_id"] = token_urlsafe(32)

old_key = format_file_path(paste["download_id"])

async with s3_create_client() as client:
await client.copy_object(
Bucket=SETTINGS.s3.bucket,
CopySource={
"Bucket": SETTINGS.s3.bucket,
"Key": old_key,
},
Key=format_file_path(to_set["download_id"]),
)

await client.delete_object(Bucket=SETTINGS.s3.bucket, Key=old_key)

await self.__state.mongo.paste.update_one(
{"_id": self.paste_id},
Expand All @@ -46,13 +65,15 @@ async def __delete(self) -> None:
Key=self.file_key(paste.get("download_id", None)),
)

async def __validate_owner(self, owner_secret: str) -> None:
async def __validate_owner(self, owner_secret: str) -> Mapping[str, Any]:
paste = await self._get_raw()
# Not Argon2, because is always a 256 bit random string,
# Bcrypt used to protect against timing attacks.
if not bcrypt.checkpw(owner_secret.encode(), paste["owner_secret"]):
raise NotAuthorizedException()

return paste

async def _get_raw(self) -> Mapping[str, Any]:
paste = await self.__state.mongo.paste.find_one({"_id": self.paste_id})
if not paste:
Expand All @@ -66,15 +87,37 @@ def file_key(self, download_id: Optional[str] = None) -> str:
def download_url(self, download_id: Optional[str] = None) -> str:
return f"{SETTINGS.s3.download_url}/{self.file_key(download_id)}"

async def access_code_kdf(self) -> PasteAccessCodeKdf:
paste = await self._get_raw()
if (
"access_code" not in paste
or paste["access_code"] is None
or isinstance(paste["access_code"], str)
):
raise NotFoundException()

return PasteAccessCodeKdf(
salt=paste["access_code"]["salt"],
ops_limit=paste["access_code"]["ops_limit"],
mem_limit=paste["access_code"]["mem_limit"],
)

async def get(self, access_code: Optional[str] = None) -> PasteModel:
paste = await self._get_raw()

if "access_code" in paste and paste["access_code"] is not None:
if not access_code:
raise NotAuthorizedException()

# Check if uses legacy access code
server_access_code = (
paste["access_code"]
if isinstance(paste["access_code"], str)
else paste["access_code"]["code"]
)

try:
PASSWORD_HASHER.verify(paste["access_code"], access_code)
PASSWORD_HASHER.verify(server_access_code, access_code)
except VerifyMismatchError:
raise NotAuthorizedException()

Expand Down
14 changes: 12 additions & 2 deletions backend/paaster/app/models/paste.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from datetime import datetime
from typing import Optional
from typing import Optional, Union

from env import SETTINGS
from pydantic import BaseModel, Field
Expand All @@ -22,9 +22,19 @@ class PasteLanguage(BaseModel):
iv: str = Field(..., max_length=SETTINGS.max_iv_size)


class PasteAccessCodeKdf(BaseModel):
salt: str = Field(max_length=32)
ops_limit: int
mem_limit: int


class PasteAccessCode(PasteAccessCodeKdf):
code: str = Field(min_length=1, max_length=256)


class UpdatePasteModel(BaseModel):
expires_in_hours: Optional[float] = Field(None, ge=-1.0, le=99999.0)
access_code: Optional[str] = Field(None, min_length=1, max_length=256)
access_code: Union[Optional[PasteAccessCode], Optional[str]] = None
language: Optional[PasteLanguage] = None


Expand Down
9 changes: 8 additions & 1 deletion backend/paaster/tests/test_paste.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,14 @@ def test_access_code_protect_paste(
) -> None:
response = client.post(
f"/controller/paste/{create_paste.id}/{create_paste.owner_secret}",
json={"access_code": "some_epic_password"},
json={
"access_code": {
"salt": "123",
"code": "some_epic_password",
"ops_limit": 0,
"mem_limit": 0,
}
},
)
assert response.status_code == 201

Expand Down
27 changes: 18 additions & 9 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@
"@sveltejs/vite-plugin-svelte": "^2.4.5",
"@tadashi/svelte-loading": "^3.0.0",
"@tsconfig/svelte": "^5.0.2",
"@types/libsodium-wrappers": "^0.7.11",
"@types/libsodium-wrappers-sumo": "^0.7.6",
"@types/mousetrap": "^1.6.11",
"dayjs": "^1.11.9",
"filedrop-svelte": "^0.1.2",
"fuse.js": "^6.6.2",
"idb-keyval": "^6.2.1",
"libsodium-wrappers": "^0.7.11",
"libsodium-wrappers-sumo": "^0.7.11",
"mousetrap": "^1.6.5",
"niceware": "^4.0.0",
"openapi-typescript-codegen": "^0.25.0",
Expand Down
39 changes: 29 additions & 10 deletions frontend/src/components/ProvideAccessCode.svelte
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<script lang="ts">
import sodium from "libsodium-wrappers";
import sodium from "libsodium-wrappers-sumo";
import { _ } from "svelte-i18n";
import { closeModal } from "svelte-modals";
import { paasterClient } from "../lib/client";
export let isOpen: boolean;
export let b64EncodedRawKey: string;
export let loadPasteFunc: Function;
export let pasteId: string;
let accessCode = ["", "", "", ""];
Expand All @@ -21,16 +23,33 @@
}
async function attemptAccessCode() {
await loadPasteFunc(
sodium.to_base64(
sodium.crypto_generichash(
64,
accessCode.join("-").toLowerCase(),
b64EncodedRawKey
),
const codeString = accessCode.join("-").toLowerCase();
let attemptedCode: string;
try {
const kdf =
await paasterClient.default.controllerPastePasteIdKdfGetPasteKdf(
pasteId
);
attemptedCode = sodium.to_base64(
sodium.crypto_pwhash(
32,
codeString,
sodium.from_base64(kdf.salt),
kdf.ops_limit,
kdf.mem_limit,
sodium.crypto_pwhash_ALG_DEFAULT
)
);
} catch (error) {
attemptedCode = sodium.to_base64(
sodium.crypto_generichash(64, codeString, b64EncodedRawKey),
sodium.base64_variants.URLSAFE_NO_PADDING
)
);
);
}
await loadPasteFunc(attemptedCode);
closeModal();
}
</script>
Expand Down
25 changes: 20 additions & 5 deletions frontend/src/components/SetAccessCode.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import sodium from "libsodium-wrappers";
import sodium from "libsodium-wrappers-sumo";
import toast from "svelte-french-toast";
import { _ } from "svelte-i18n";
import { closeModal } from "svelte-modals";
Expand All @@ -19,15 +19,30 @@
accessCode = generatePassphrase(8);
codeString = accessCode.join("-").toLowerCase();
const salt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
const opsLimit = sodium.crypto_pwhash_OPSLIMIT_MODERATE;
const memLimit = sodium.crypto_pwhash_MEMLIMIT_MODERATE;
const derivedCode = sodium.crypto_pwhash(
32,
codeString,
salt,
opsLimit,
memLimit,
sodium.crypto_pwhash_ALG_DEFAULT
);
await toast.promise(
paasterClient.default.controllerPastePasteIdOwnerSecretUpdatePaste(
pasteId,
ownerSecret,
{
access_code: sodium.to_base64(
sodium.crypto_generichash(64, codeString, b64EncodedRawKey),
sodium.base64_variants.URLSAFE_NO_PADDING
),
access_code: {
code: sodium.to_base64(derivedCode),
mem_limit: memLimit,
ops_limit: opsLimit,
salt: sodium.to_base64(salt),
},
}
),
{
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lib/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export { CancelablePromise, CancelError } from './core/CancelablePromise';
export { OpenAPI } from './core/OpenAPI';
export type { OpenAPIConfig } from './core/OpenAPI';

export type { PasteAccessCode } from './models/PasteAccessCode';
export type { PasteAccessCodeKdf } from './models/PasteAccessCodeKdf';
export type { PasteCreatedModel } from './models/PasteCreatedModel';
export type { PasteLanguage } from './models/PasteLanguage';
export type { PasteModel } from './models/PasteModel';
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/lib/client/models/PasteAccessCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */

export type PasteAccessCode = {
salt: string;
ops_limit: number;
mem_limit: number;
code: string;
};

11 changes: 11 additions & 0 deletions frontend/src/lib/client/models/PasteAccessCodeKdf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */

export type PasteAccessCodeKdf = {
salt: string;
ops_limit: number;
mem_limit: number;
};

3 changes: 2 additions & 1 deletion frontend/src/lib/client/models/PasteCreatedModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
/* tslint:disable */
/* eslint-disable */

import type { PasteAccessCode } from './PasteAccessCode';
import type { PasteLanguage } from './PasteLanguage';

export type PasteCreatedModel = {
expires_in_hours?: (null | number);
access_code?: (null | string);
access_code?: (null | PasteAccessCode | string);
language?: (null | PasteLanguage);
id: string;
iv: string;
Expand Down
Loading

1 comment on commit 9fa5a6d

@vercel
Copy link

@vercel vercel bot commented on 9fa5a6d Sep 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.