summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--internal/cryptoutil/oae2.go73
-rw-r--r--roseh.moe.go182
-rw-r--r--templates/login.html.template1
-rw-r--r--templates/upload.html.template7
-rw-r--r--tools/notes/notes.go36
5 files changed, 238 insertions, 61 deletions
diff --git a/internal/cryptoutil/oae2.go b/internal/cryptoutil/oae2.go
index 8eb18fb..2310039 100644
--- a/internal/cryptoutil/oae2.go
+++ b/internal/cryptoutil/oae2.go
@@ -16,14 +16,11 @@ import (
// Online Authenticated Encryption from https://eprint.iacr.org/2015/189.pdf
const (
- aeadOverhead = 16
- aesKeySize = 32
- noncePrefixSize = 3
- gcmNonceSize = 12
- headerSize = aesKeySize + noncePrefixSize
- cacheSize = 192 * 1024
- encryptedSegmentSize = cacheSize - 1
- segmentSize = encryptedSegmentSize - aeadOverhead
+ aeadOverhead = 16
+ aesKeySize = 32
+ noncePrefixSize = 3
+ gcmNonceSize = 12
+ headerSize = aesKeySize + noncePrefixSize
)
// An EncryptionKey is used for encrypting and decrypting data. A key can be any
@@ -96,30 +93,57 @@ func (o *oae2) decryptBlock(out, block []byte, lastBlock bool) ([]byte, error) {
// to the segment size, so it's important to call Close to flush the
// final segment.
type EncryptingWriter struct {
- w io.Writer
- oae2 oae2
+ w io.Writer
+ oae2 oae2
+ segmentSize int
initialized bool
err error
buf []byte
}
+type opts struct {
+ additionalData []byte
+ segmentSize int
+}
+
+var defaultOpts = opts{segmentSize: 192*1024 - 1 - aeadOverhead}
+
+type Option func(*opts)
+
+func WithAdditionalData(ad []byte) Option {
+ return func(opts *opts) {
+ opts.additionalData = ad
+ }
+}
+
+func WithSegmentSize(segmentSize int) Option {
+ return func(opts *opts) {
+ opts.segmentSize = segmentSize
+ }
+}
+
// NewWriter returns a new EncryptingWriter that writes to w. The additionalData
// will be authenticated with the first segment, but is not written to w. The
// same additional data must be provided when decrypting.
-func (k EncryptionKey) NewWriter(w io.Writer, additionalData []byte) *EncryptingWriter {
+func (k EncryptionKey) NewWriter(w io.Writer, options ...Option) *EncryptingWriter {
+ opts := defaultOpts
+ for _, o := range options {
+ o(&opts)
+ }
return &EncryptingWriter{
w: w,
oae2: oae2{
key: k,
- additionalData: additionalData,
+ additionalData: opts.additionalData,
},
+ segmentSize: opts.segmentSize,
}
}
func (w *EncryptingWriter) initialize() error {
w.initialized = true
- w.buf = make([]byte, 0, encryptedSegmentSize)
+ w.buf = make([]byte, 0, w.segmentSize+aeadOverhead)
header := make([]byte, headerSize)
rand.Read(header)
if w.err = w.oae2.initialize(header); w.err != nil {
@@ -141,7 +165,7 @@ func (w *EncryptingWriter) Write(buf []byte) (int, error) {
r := bytes.NewReader(buf)
nn := 0
for r.Len() > 0 {
- if len(w.buf) == segmentSize {
+ if len(w.buf) == w.segmentSize {
var encrypted []byte
if encrypted, w.err = w.oae2.encryptBlock(w.buf[:0], w.buf, false); w.err != nil {
return nn, w.err
@@ -151,7 +175,7 @@ func (w *EncryptingWriter) Write(buf []byte) (int, error) {
}
w.buf = w.buf[:0]
}
- n, _ := r.Read(w.buf[len(w.buf):segmentSize])
+ n, _ := r.Read(w.buf[len(w.buf):w.segmentSize])
w.buf = w.buf[:len(w.buf)+n]
nn += n
}
@@ -187,8 +211,9 @@ func (w *EncryptingWriter) Close() error {
// A DecryptingReader decrypts data using the STREAM construction.
type DecryptingReader struct {
- r *bufio.Reader
- oae2 oae2
+ r *bufio.Reader
+ oae2 oae2
+ segmentSize int
initialized bool
decryptedBuf bytes.Buffer
@@ -196,13 +221,18 @@ type DecryptingReader struct {
// NewReader returns a new DecryptingWriter that decrypts data from r. The
// additionaData must be the same that was provided when encrypting.
-func (k EncryptionKey) NewReader(r io.Reader, additionalData []byte) *DecryptingReader {
+func (k EncryptionKey) NewReader(r io.Reader, options ...Option) *DecryptingReader {
+ opts := defaultOpts
+ for _, o := range options {
+ o(&opts)
+ }
return &DecryptingReader{
- r: bufio.NewReaderSize(r, encryptedSegmentSize+1),
+ r: bufio.NewReaderSize(r, opts.segmentSize+aeadOverhead+1),
oae2: oae2{
key: k,
- additionalData: additionalData,
+ additionalData: opts.additionalData,
},
+ segmentSize: opts.segmentSize,
}
}
@@ -215,12 +245,13 @@ func (r *DecryptingReader) initialize() error {
return err
}
r.r.Discard(len(header))
- r.decryptedBuf = *bytes.NewBuffer(make([]byte, 0, segmentSize))
+ r.decryptedBuf = *bytes.NewBuffer(make([]byte, 0, r.segmentSize))
r.initialized = true
return nil
}
func (r *DecryptingReader) fillBuf() error {
+ encryptedSegmentSize := r.segmentSize + aeadOverhead
// Peek one extra byte to make sure if this is the last segment
block, readErr := r.r.Peek(encryptedSegmentSize + 1)
if len(block) == 0 {
diff --git a/roseh.moe.go b/roseh.moe.go
index 33122d2..988938d 100644
--- a/roseh.moe.go
+++ b/roseh.moe.go
@@ -173,7 +173,7 @@ func attachCookie(w http.ResponseWriter) error {
http.SetCookie(w, &http.Cookie{
Name: "auth",
Value: token,
- Path: "/notepad",
+ Path: "/",
Expires: time.Now().Add(cookieExpiration),
Secure: true,
HttpOnly: true,
@@ -205,7 +205,7 @@ func cookieAuth(w http.ResponseWriter, r *http.Request) (string, bool) {
http.SetCookie(w, &http.Cookie{
Name: "csrf-token",
Value: csrfToken,
- Path: "/notepad",
+ Path: "/",
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
@@ -233,6 +233,33 @@ func setEncryptionKey(rawKey cryptoutil.RawKey) error {
return nil
}
+type redirectTarget int
+
+const (
+ redirectDefault redirectTarget = iota
+ redirectNotepad
+ redirectUpload
+)
+
+func parseRedirectTarget(s string) redirectTarget {
+ i, err := strconv.Atoi(s)
+ if err != nil {
+ return redirectDefault
+ }
+ return redirectTarget(i)
+}
+
+func (t redirectTarget) path() string {
+ switch t {
+ case redirectNotepad:
+ return "/notepad"
+ case redirectUpload:
+ return "/notes/upload"
+ default:
+ return "/"
+ }
+}
+
var (
//go:embed templates/login.html.template
loginString string
@@ -240,13 +267,14 @@ var (
)
type loginTemplateArgs struct {
- Error bool
+ Error bool
+ Redirect redirectTarget
}
func login(w http.ResponseWriter, r *http.Request) {
rawKey, err := notepadPassword.CheckPassword(r.FormValue("password"))
if err != nil {
- if err := loginTemplate.Execute(w, loginTemplateArgs{Error: true}); err != nil {
+ if err := loginTemplate.Execute(w, loginTemplateArgs{Error: true, Redirect: parseRedirectTarget(r.FormValue("redirect"))}); err != nil {
log.Printf("Warning: login: %s", err)
}
return
@@ -256,7 +284,7 @@ func login(w http.ResponseWriter, r *http.Request) {
return
}
attachCookie(w)
- http.Redirect(w, r, "/notepad", http.StatusSeeOther)
+ http.Redirect(w, r, parseRedirectTarget(r.FormValue("redirect")).path(), http.StatusSeeOther)
}
func readNotepad(key cryptoutil.EncryptionKey) (string, error) {
@@ -265,7 +293,7 @@ func readNotepad(key cryptoutil.EncryptionKey) (string, error) {
return "", err
}
defer f.Close()
- notepad, err := io.ReadAll(key.NewReader(f, []byte("notepad")))
+ notepad, err := io.ReadAll(key.NewReader(f, cryptoutil.WithAdditionalData([]byte("notepad"))))
if err != nil {
return "", err
}
@@ -286,7 +314,7 @@ type notepadTemplateArgs struct {
func notepad(w http.ResponseWriter, r *http.Request) {
csrfToken, ok := cookieAuth(w, r)
if !ok {
- if err := loginTemplate.Execute(w, loginTemplateArgs{}); err != nil {
+ if err := loginTemplate.Execute(w, loginTemplateArgs{Redirect: redirectNotepad}); err != nil {
log.Printf("Warning: login: %s", err)
}
return
@@ -295,7 +323,7 @@ func notepad(w http.ResponseWriter, r *http.Request) {
key := encryptionKey
encryptionKeyMu.Unlock()
if key == nil {
- if err := loginTemplate.Execute(w, loginTemplateArgs{}); err != nil {
+ if err := loginTemplate.Execute(w, loginTemplateArgs{Redirect: redirectNotepad}); err != nil {
log.Printf("Warning: login: %s", err)
}
return
@@ -327,7 +355,7 @@ func saveNote(w http.ResponseWriter, r *http.Request) error {
return err
}
defer f.Close()
- encryptingWriter := key.NewWriter(f, []byte("notepad"))
+ encryptingWriter := key.NewWriter(f, cryptoutil.WithAdditionalData([]byte("notepad")))
if _, err := encryptingWriter.Write([]byte(r.FormValue("content"))); err != nil {
os.Remove(f.Name())
return err
@@ -414,7 +442,7 @@ func dumpFile(w io.Writer, key cryptoutil.EncryptionKey, name string) error {
return err
}
defer f.Close()
- _, err = io.Copy(w, key.NewReader(f, []byte("chunks/"+filepath.Base(name))))
+ _, err = io.Copy(w, key.NewReader(f, cryptoutil.WithAdditionalData([]byte("chunks/"+filepath.Base(name)))))
return err
}
@@ -428,6 +456,27 @@ var (
wordList = strings.Split(strings.TrimSuffix(wordListString, "\n"), "\n")
)
+func newNoteName() (*os.File, error) {
+ for n := 1; ; n++ {
+ buf := make([]byte, 2*n)
+ rand.Read(buf)
+ words := make([]string, n)
+ for i := range words {
+ words[i] = wordList[binary.NativeEndian.Uint16(buf[2*i:])&0x1fff]
+ }
+ name := strings.Join(words, "-")
+ var err error
+ f, err := os.OpenFile(*notepadDir+"/notes/"+name, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
+ if err != nil {
+ if errors.Is(err, os.ErrExist) {
+ continue
+ }
+ return nil, fmt.Errorf("create note: create note file: %s", err)
+ }
+ return f, nil
+ }
+}
+
func createNote(req *api.CreateNoteRequest) (*api.CreateNoteResponse, error) {
type continuationToken struct {
Prefix string
@@ -479,7 +528,7 @@ func createNote(req *api.CreateNoteRequest) (*api.CreateNoteResponse, error) {
return nil, err
}
}
- encryptingWriter := key.NewWriter(f, []byte("chunks/"+name))
+ encryptingWriter := key.NewWriter(f, cryptoutil.WithAdditionalData([]byte("chunks/"+name)))
if _, err := encryptingWriter.Write(req.Chunk); err != nil {
return nil, err
}
@@ -499,27 +548,12 @@ func createNote(req *api.CreateNoteRequest) (*api.CreateNoteResponse, error) {
if err := os.MkdirAll(*notepadDir+"/notes", 0700); err != nil {
return nil, err
}
- var finalNote *os.File
- for n := 1; ; n++ {
- buf := make([]byte, 2*n)
- rand.Read(buf)
- words := make([]string, n)
- for i := range words {
- words[i] = wordList[binary.NativeEndian.Uint16(buf[2*i:])&0x1fff]
- }
- name := strings.Join(words, "-")
- var err error
- finalNote, err = os.OpenFile(*notepadDir+"/notes/"+name, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
- if err != nil {
- if errors.Is(err, os.ErrExist) {
- continue
- }
- return nil, fmt.Errorf("create note: create note file: %s", err)
- }
- defer finalNote.Close()
- break
+ finalNote, err := newNoteName()
+ if err != nil {
+ return nil, err
}
- writer := key.NewWriter(finalNote, []byte("notes/"+filepath.Base(finalNote.Name())))
+ defer finalNote.Close()
+ writer := key.NewWriter(finalNote, cryptoutil.WithAdditionalData([]byte("notes/"+filepath.Base(finalNote.Name()))))
for i := range contToken.I {
if err := dumpFile(writer, key, *notepadDir+"/chunks/"+chunkName(contToken.Prefix, i)); err != nil {
return nil, err
@@ -568,7 +602,7 @@ func readNote(stream *encoder, req *api.ReadNoteRequest) error {
if key == nil {
return fmt.Errorf("%w: need login", api.PermissionDenied)
}
- _, err = io.Copy(&readNoteResponseWriter{stream: stream}, key.NewReader(f, []byte("notes/"+req.Note)))
+ _, err = io.Copy(&readNoteResponseWriter{stream: stream}, key.NewReader(f, cryptoutil.WithAdditionalData([]byte("notes/"+req.Note))))
return err
}
@@ -624,6 +658,87 @@ func gobRespStreamMiddleware[Request any](next func(*encoder, *Request) error) h
}
}
+var (
+ //go:embed templates/upload.html.template
+ uploadTemplateString string
+ uploadTemplate = template.Must(template.Must(outlineTemplate.Clone()).New("body").Parse(uploadTemplateString)).Lookup("outline")
+)
+
+type uploadTemplateArgs struct {
+ CSRFToken string
+}
+
+func uploadPage(w http.ResponseWriter, r *http.Request) {
+ csrfToken, ok := cookieAuth(w, r)
+ if !ok {
+ if err := loginTemplate.Execute(w, loginTemplateArgs{Redirect: redirectUpload}); err != nil {
+ log.Printf("Warning: login: %s", err)
+ }
+ return
+ }
+ encryptionKeyMu.Lock()
+ key := encryptionKey
+ encryptionKeyMu.Unlock()
+ if key == nil {
+ if err := loginTemplate.Execute(w, loginTemplateArgs{Redirect: redirectUpload}); err != nil {
+ log.Printf("Warning: login: %s", err)
+ }
+ return
+ }
+ if err := uploadTemplate.Execute(w, uploadTemplateArgs{CSRFToken: csrfToken}); err != nil {
+ log.Printf("Warning: upload: %s", err)
+ }
+}
+
+func uploadNote(w http.ResponseWriter, r *http.Request) {
+ if csrfToken, err := r.Cookie("csrf-token"); err != nil || subtle.ConstantTimeCompare([]byte(csrfToken.Value), []byte(r.FormValue("csrf-token"))) == 0 {
+ http.Error(w, "bad CSRF token", http.StatusForbidden)
+ return
+ }
+ if _, ok := cookieAuth(w, r); !ok {
+ log.Printf("asdf: bad auth")
+ if err := loginTemplate.Execute(w, loginTemplateArgs{Redirect: redirectUpload}); err != nil {
+ log.Printf("Warning: login: %s", err)
+ }
+ return
+ }
+ encryptionKeyMu.Lock()
+ key := encryptionKey
+ encryptionKeyMu.Unlock()
+ if key == nil {
+ log.Printf("asdf: key nil")
+ if err := loginTemplate.Execute(w, loginTemplateArgs{Redirect: redirectUpload}); err != nil {
+ log.Printf("Warning: login: %s", err)
+ }
+ return
+ }
+ noteFile, err := newNoteName()
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ defer noteFile.Close()
+ contentReader, _, err := r.FormFile("content")
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ writer := key.NewWriter(noteFile, cryptoutil.WithAdditionalData([]byte("notes/"+filepath.Base(noteFile.Name()))))
+ if _, err := io.Copy(writer, contentReader); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if err := writer.Close(); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if err := noteFile.Close(); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ fmt.Fprintf(w, "upload! pog\n%s", filepath.Base(noteFile.Name()))
+}
+
func main() {
flag.Parse()
@@ -636,6 +751,9 @@ func main() {
http.HandleFunc("POST /login", login)
http.HandleFunc("GET /notepad", notepad)
http.HandleFunc("POST /notepad/autosave", autosave)
+ http.HandleFunc("GET /notes/upload", uploadPage)
+ http.HandleFunc("POST /notes/upload", uploadNote)
+ // http.HandleFunc("GET /notes/{note}", readNoteContent)
http.HandleFunc("POST /api/login", apiLogin)
http.HandleFunc("GET /api/list-notes", gobReqRespMiddleware(listNotes))
http.HandleFunc("POST /api/create-note", gobReqRespMiddleware(createNote))
diff --git a/templates/login.html.template b/templates/login.html.template
index 0cf3469..8d1ec9b 100644
--- a/templates/login.html.template
+++ b/templates/login.html.template
@@ -5,6 +5,7 @@
<p class="login-error">Incorrect password</p>
{{end}}
<form class="password-form" action="/login" method="post">
+ <input type="hidden" name="redirect" value="{{.Redirect}}">
<label class="password-label" for="password">Enter password</label>
<input id="password" class="password" type="password" name="password" autofocus>
</form>
diff --git a/templates/upload.html.template b/templates/upload.html.template
new file mode 100644
index 0000000..f920f23
--- /dev/null
+++ b/templates/upload.html.template
@@ -0,0 +1,7 @@
+{{define "head"}}<title>Upload</title>{{end}}
+
+<form method="POST" enctype="multipart/form-data">
+ <input type="hidden" name="csrf-token" value="{{.CSRFToken}}">
+ <input type="file" name="content">
+ <input type="submit" value="Upload">
+</form>
diff --git a/tools/notes/notes.go b/tools/notes/notes.go
index f22b7f9..9f190f6 100644
--- a/tools/notes/notes.go
+++ b/tools/notes/notes.go
@@ -10,6 +10,7 @@ import (
"io"
"net/http"
"os"
+ "time"
"github.com/google/subcommands"
"gitlab.com/rhogenson/roseh.moe/internal/api"
@@ -20,10 +21,12 @@ var serverURL = flag.String("url", "https://roseh.moe", "server url")
const tokenCache = "/dev/shm/roseh.moe-upload-token"
+var errUnavailable = errors.New("unavailable")
+
func readGobResp[Response any](req *http.Request) (*Response, error) {
httpResp, err := http.DefaultClient.Do(req)
if err != nil {
- return nil, fmt.Errorf("read gob response: http: %s", err)
+ return nil, fmt.Errorf("%w: read gob response: http: %s", errUnavailable, err)
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusOK {
@@ -45,9 +48,9 @@ func readGobResp[Response any](req *http.Request) (*Response, error) {
}
func login(ctx context.Context) (*api.LoginResponse, error) {
- fmt.Print("Enter password:")
+ fmt.Fprint(os.Stderr, "Enter password:")
password, err := term.ReadPassword(int(os.Stdin.Fd()))
- fmt.Println()
+ fmt.Fprintln(os.Stderr)
if err != nil {
return nil, err
}
@@ -214,11 +217,28 @@ type createNoteRequestStreamWriter struct {
}
func (w *createNoteRequestStreamWriter) Write(buf []byte) (int, error) {
- resp, err := createNote(w.ctx, &api.CreateNoteRequest{
- ContinuationToken: w.continuationToken,
- Chunk: buf,
- More: true,
- })
+ var (
+ resp *api.CreateNoteResponse
+ err error
+ )
+ delay := time.Second
+ for i := 0; i < 3; i++ {
+ resp, err = createNote(w.ctx, &api.CreateNoteRequest{
+ ContinuationToken: w.continuationToken,
+ Chunk: buf,
+ More: true,
+ })
+ if !errors.Is(err, errUnavailable) {
+ break
+ }
+ fmt.Fprintf(os.Stderr, "Warning: %s (retrying)\n", err)
+ select {
+ case <-time.After(delay):
+ case <-w.ctx.Done():
+ return 0, w.ctx.Err()
+ }
+ delay *= 2
+ }
if err != nil {
return 0, err
}