Compare commits

..

18 Commits

Author SHA1 Message Date
5dff12ea70 use hash instead of path for key 2021-05-16 11:16:25 +02:00
e332dc63e8 on push 2021-05-14 15:09:55 +02:00
a18e9bcc88 Merge pull request #7 from cupcakearmy/testing
use docker-compose
2021-05-14 14:48:56 +02:00
4b43edf54a use docker-compose 2021-05-14 14:48:34 +02:00
e3aa2dd5ff Update test.yml 2021-05-10 10:13:05 +02:00
98a03c25e6 Merge pull request #6 from cupcakearmy/testing
Testing
2021-05-10 10:11:59 +02:00
7f618e7e45 ci 2021-05-10 10:11:31 +02:00
84a7be4549 config 2021-05-10 10:04:19 +02:00
b2bad5f64c cypress runner 2021-05-10 09:58:21 +02:00
41f55c0920 test ids 2021-05-10 09:58:13 +02:00
edbf8a8ecf changelog 2021-05-08 21:47:13 +02:00
4852804581 time bug 2021-05-08 21:47:08 +02:00
22b1c35b3e loading state 2021-05-08 21:46:52 +02:00
d1e9ffd89b up the iterations 2021-05-08 21:46:43 +02:00
9c675ba48c notes about availability 2021-05-08 21:46:33 +02:00
ef3d3d5bde changelog 2021-05-08 10:34:18 +02:00
7e835af3f2 changelog 2021-05-08 10:17:20 +02:00
f153102978 bug 2021-05-08 10:16:05 +02:00
15 changed files with 4741 additions and 34 deletions

View File

@@ -1 +1,2 @@
target target
node_modules

18
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
name: test
on:
workflow_dispatch:
push:
jobs:
text:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install
run: |
docker-compose build
npm ci
- name: Test
run: npm run test:run

View File

@@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.1.0] - 2021-05-16
### Security
- Using hash `#` instead of path
## [1.0.11] - 2021-05-08
### Added
- loading text
- description for created notes about availability
### Changed
- iterations from 100 to 100k
### Fixed
- time based view bug
## [1.0.10] - 2021-05-08
### Fixed
- API endpoint was not reachable
## [1.0.9] - 2021-05-07 ## [1.0.9] - 2021-05-07
## Changed ## Changed

View File

@@ -23,4 +23,6 @@ COPY --from=CLIENT /tmp/build ./client/build
ENV MEMCACHE=memcached:11211 ENV MEMCACHE=memcached:11211
EXPOSE 5000
ENTRYPOINT [ "/app/cryptgeon" ] ENTRYPOINT [ "/app/cryptgeon" ]

View File

@@ -13,7 +13,7 @@ type CallOptions = {
method: string method: string
body?: any body?: any
} }
const base = dev ? 'http://localhost:5000' : undefined const base = dev ? 'http://localhost:5000/api/' : '/api/'
async function call(options: CallOptions) { async function call(options: CallOptions) {
return fetch(base + options.url, { return fetch(base + options.url, {
method: options.method, method: options.method,
@@ -27,7 +27,7 @@ async function call(options: CallOptions) {
export async function create(note: Note) { export async function create(note: Note) {
const data = await call({ const data = await call({
url: '/api/notes', url: 'notes',
method: 'post', method: 'post',
body: note, body: note,
}) })
@@ -36,7 +36,7 @@ export async function create(note: Note) {
export async function get(id: string) { export async function get(id: string) {
const data = await call({ const data = await call({
url: `/api/notes/${id}`, url: `notes/${id}`,
method: 'delete', method: 'delete',
}) })
return data as NotePublic return data as NotePublic
@@ -44,7 +44,7 @@ export async function get(id: string) {
export async function info(id: string) { export async function info(id: string) {
const data = await call({ const data = await call({
url: `/api/notes/${id}`, url: `notes/${id}`,
method: 'get', method: 'get',
}) })
return data as NoteInfo return data as NoteInfo

View File

@@ -36,7 +36,7 @@ export function getKeyFromString(password: string) {
} }
export async function getDerivedForKey(key: CryptoKey, salt: ArrayBuffer) { export async function getDerivedForKey(key: CryptoKey, salt: ArrayBuffer) {
const iterations = 1_000 const iterations = 100_000
return window.crypto.subtle.deriveKey( return window.crypto.subtle.deriveKey(
{ {
name: 'PBKDF2', name: 'PBKDF2',

View File

@@ -50,7 +50,8 @@
password: password, password: password,
id: response.id, id: response.id,
} }
} catch { } catch (e) {
console.error(e)
error = 'could not create note.' error = 'could not create note.'
} finally { } finally {
loading = false loading = false
@@ -66,26 +67,49 @@
<TextInput <TextInput
type="text" type="text"
readonly readonly
value="{window.location.origin}/note/{result.id}/{result.password}" label="share link"
value="{window.location.origin}/note/{result.id}#{result.password}"
copy copy
data-testid="note-share-link"
/> />
<br /> <br />
<Button on:click={reset}>new</Button> <p>
<b>availability:</b>
<br />
the note is not guaranteed to be stored as everything is kept in ram, if it fills up the oldest notes
will be removed.
<br />
(you probably will be fine, just be warned.)
</p>
<br />
<Button on:click={reset}>new note</Button>
{:else} {:else}
<form on:submit|preventDefault={submit}> <form on:submit|preventDefault={submit}>
<fieldset disabled={loading}> <fieldset disabled={loading}>
<TextArea label="note" bind:value={note.contents} placeholder="..." /> <TextArea
label="note"
bind:value={note.contents}
placeholder="..."
data-testid="input-note"
/>
<div class="bottom"> <div class="bottom">
<Switch label="advanced" bind:value={advanced} /> <Switch label="advanced" bind:value={advanced} />
<Button type="submit">create</Button> <Button type="submit" data-testid="button-create">create</Button>
</div> </div>
{#if error} {#if error}
<div class="error-text">{error}</div> <div class="error-text">{error}</div>
{/if} {/if}
<p><br />{message}</p> <p>
<br />
{#if loading}
loading...
{:else}
{message}
{/if}
</p>
<div class="advanced" class:hidden={!advanced}> <div class="advanced" class:hidden={!advanced}>
<br /> <br />

View File

@@ -7,18 +7,17 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'
import copy from 'copy-to-clipboard'
import type { NotePublic } from '$lib/api' import type { NotePublic } from '$lib/api'
import { info, get } from '$lib/api' import { info, get } from '$lib/api'
import { decrypt, getKeyFromString } from '$lib/crypto' import { decrypt, getKeyFromString } from '$lib/crypto'
import Button from '$lib/ui/Button.svelte' import Button from '$lib/ui/Button.svelte'
import TextInput from '$lib/ui/TextInput.svelte'
import copy from 'copy-to-clipboard'
import { onMount } from 'svelte'
export let id: string export let id: string
export let password: string
let password: string
let note: NotePublic | null = null let note: NotePublic | null = null
let exists = false let exists = false
@@ -29,6 +28,8 @@
try { try {
loading = true loading = true
error = null error = null
password = window.location.hash.slice(1)
console.log(password)
await info(id) await info(id)
exists = true exists = true
} catch { } catch {
@@ -41,40 +42,50 @@
async function show() { async function show() {
try { try {
error = false error = false
loading = true
const data = note || (await get(id)) // Don't get the content twice on wrong password. const data = note || (await get(id)) // Don't get the content twice on wrong password.
const key = await getKeyFromString(password) const key = await getKeyFromString(password)
data.contents = await decrypt(data.contents, key) data.contents = await decrypt(data.contents, key)
note = data note = data
} catch { } catch {
error = true error = true
} finally {
loading = false
} }
} }
</script> </script>
{#if !loading} {#if !loading}
{#if !exists} {#if !exists}
<p class="error-text">note was not found or was already deleted.</p> <p class="error-text" data-testid="note-not-found">
note was not found or was already deleted.
</p>
{:else if note && !error} {:else if note && !error}
<p class="error-text">you will not get the chance to see the note again.</p> <p class="error-text">you will not get the chance to see the note again.</p>
<div class="note"> <div class="note" data-testid="note-result">
{note.contents} {note.contents}
</div> </div>
<br /> <br />
<Button on:click={() => copy(note.contents)}>copy to clipboard</Button> <Button on:click={() => copy(note.contents)}>copy to clipboard</Button>
{:else} {:else}
<form on:submit|preventDefault={show}> <form on:submit|preventDefault={show}>
<p>click below to show and delete the note if the counter has reached it's limit</p> <fieldset>
<Button type="submit">show note</Button> <p>click below to show and delete the note if the counter has reached it's limit</p>
{#if error} <Button type="submit" data-testid="button-show">show note</Button>
<br /> {#if error}
<p class="error-text">
wrong password. could not decipher. probably a broken link. note was destroyed.
<br /> <br />
</p> <p class="error-text">
{/if} wrong password. could not decipher. probably a broken link. note was destroyed.
<br />
</p>
{/if}
</fieldset>
</form> </form>
{/if} {/if}
{/if} {/if}
{#if loading}
<p>loading...</p>
{/if}
<style> <style>
.note { .note {

5
cypress.json Normal file
View File

@@ -0,0 +1,5 @@
{
"fixturesFolder": false,
"pluginsFile": false,
"supportFile": false
}

2
cypress/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
screenshots
videos

View File

@@ -0,0 +1,41 @@
function createNote(options = {}) {
Object.assign(options, {
text: `Revaluation battle selfish derive suicide revaluation society love superiority salvation spirit virtues revaluation. Aversion sexuality play burying mountains intentions battle reason strong burying war insofar inexpedient war. Fearful intentions selfish madness suicide.`,
})
cy.visit('http://localhost:5000')
const text = options.text
cy.get('[data-testid=input-note]').type(text)
cy.get('[data-testid=button-create]').click()
cy.wait(500)
return cy
.get('[data-testid=note-share-link]')
.invoke('val')
.then((link) => {
return [link, text]
})
}
describe('Basics', () => {
it('Share note', () => {
createNote().then(([link, text]) => {
cy.visit(link)
cy.get('[data-testid=button-show]').click()
cy.wait(250)
cy.get('[data-testid=note-result]').should('have.text', text)
})
})
it('Check destroyed', () => {
createNote().then(([link, text]) => {
// Check the first time
cy.visit(link)
cy.get('[data-testid=button-show]').click()
cy.wait(250)
cy.get('[data-testid=note-result]').should('have.text', text)
// Should not exists anymore
cy.visit(link)
cy.get('[data-testid=note-not-found]').should('exist')
})
})
})

View File

@@ -15,4 +15,4 @@ services:
depends_on: depends_on:
- memcached - memcached
ports: ports:
- 80:5000 - 5000:5000

4572
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,9 +3,14 @@
"dev:docker": "docker-compose up memcached", "dev:docker": "docker-compose up memcached",
"dev:backend": "cargo watch -x 'run --bin cryptgeon'", "dev:backend": "cargo watch -x 'run --bin cryptgeon'",
"dev:front": "npm --prefix client run dev", "dev:front": "npm --prefix client run dev",
"dev": "run-p dev:*" "dev": "run-p dev:*",
"test:server": "docker-compose up --build",
"test:cypress": "cypress run --headless",
"test:run": "start-server-and-test test:server http://localhost:5000 test:cypress"
}, },
"devDependencies": { "devDependencies": {
"npm-run-all": "^4.1.5" "cypress": "^7.2.0",
"npm-run-all": "^4.1.5",
"start-server-and-test": "^1.12.1"
} }
} }

View File

@@ -56,7 +56,8 @@ async fn create(note: web::Json<Note>) -> impl Responder {
if e > 360 { if e > 360 {
return bad_req; return bad_req;
} }
n.expiration = Some(now() + (e * 60)) let expiration = now() + (e * 60);
n.expiration = Some(expiration);
} }
_ => {} _ => {}
} }
@@ -89,8 +90,8 @@ async fn delete(path: web::Path<NotePath>) -> impl Responder {
} }
match changed.expiration { match changed.expiration {
Some(e) => { Some(e) => {
if e > now() { store::del(&p.id.clone());
store::del(&p.id.clone()); if e < now() {
return HttpResponse::BadRequest().finish(); return HttpResponse::BadRequest().finish();
} }
} }