diff options
| author | Rose Hogenson <rosehogenson@posteo.net> | 2025-09-28 09:48:40 -0700 |
|---|---|---|
| committer | Rose Hogenson <rosehogenson@posteo.net> | 2025-09-28 09:48:40 -0700 |
| commit | 04821a6e7752d6852bc1d283a88f05f7abecae87 (patch) | |
| tree | 667266ce89224443831cb21d4d9e94cd6ce48c3d /internal | |
| parent | 768d3629b938196d132b80864204d7b6a9683a9e (diff) | |
| download | roseh.moe-04821a6e7752d6852bc1d283a88f05f7abecae87.tar.zst | |
Add a gob API
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/api/api.go | 64 | ||||
| -rw-r--r-- | internal/api/errorcode_string.go | 28 | ||||
| -rw-r--r-- | internal/cryptoutil/cryptoutil.go | 107 |
3 files changed, 199 insertions, 0 deletions
diff --git a/internal/api/api.go b/internal/api/api.go new file mode 100644 index 0000000..7c8472a --- /dev/null +++ b/internal/api/api.go @@ -0,0 +1,64 @@ +// Package api contains type definitions for the gob API. +package api + +import "encoding/gob" + +type ErrorCode int + +//go:generate go tool stringer -type=ErrorCode +const ( + Ok ErrorCode = iota + Unauthenticated // missing credentials + PermissionDenied // invalid credentials, or login required + BadRequest // incorrect request format (try gob) + NotFound // requested resource was not found + Internal // server encountered an unexpected error +) + +func (c ErrorCode) Error() string { + return c.String() +} + +type Response struct { + Status ErrorCode + Err string + Ok any +} + +type LoginResponse struct { + Token string +} + +type ListNotesRequest struct{} + +type ListNotesResponse struct { + Notes []string +} + +type CreateNoteRequestStream struct { + Chunk []byte +} + +type CreateNoteResponse struct { + Name string +} + +type ReadNoteRequest struct { + Note string +} + +type ReadNoteResponseStream struct { + Chunk []byte +} + +type DeleteNoteRequest struct{} + +type DeleteNoteResponse struct{} + +func init() { + gob.Register(&LoginResponse{}) + gob.Register(&ListNotesResponse{}) + gob.Register(&CreateNoteResponse{}) + gob.Register(&ReadNoteResponseStream{}) + gob.Register(&DeleteNoteResponse{}) +} diff --git a/internal/api/errorcode_string.go b/internal/api/errorcode_string.go new file mode 100644 index 0000000..2b4a294 --- /dev/null +++ b/internal/api/errorcode_string.go @@ -0,0 +1,28 @@ +// Code generated by "stringer -type=ErrorCode"; DO NOT EDIT. + +package api + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[Ok-0] + _ = x[Unauthenticated-1] + _ = x[PermissionDenied-2] + _ = x[BadRequest-3] + _ = x[NotFound-4] + _ = x[Internal-5] +} + +const _ErrorCode_name = "OkUnauthenticatedPermissionDeniedBadRequestNotFoundInternal" + +var _ErrorCode_index = [...]uint8{0, 2, 17, 33, 43, 51, 59} + +func (i ErrorCode) String() string { + if i < 0 || i >= ErrorCode(len(_ErrorCode_index)-1) { + return "ErrorCode(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _ErrorCode_name[_ErrorCode_index[i]:_ErrorCode_index[i+1]] +} diff --git a/internal/cryptoutil/cryptoutil.go b/internal/cryptoutil/cryptoutil.go index 4ed600d..09ed996 100644 --- a/internal/cryptoutil/cryptoutil.go +++ b/internal/cryptoutil/cryptoutil.go @@ -13,6 +13,10 @@ import ( "crypto/subtle" "encoding/hex" "errors" + "fmt" + "hash" + "io" + "slices" ) const ( @@ -169,3 +173,106 @@ func (k EncryptionKey) Decrypt(msg EncryptedMessage) ([]byte, error) { cipher.NewCTR(block, nonce).XORKeyStream(msg, msg) return msg, nil } + +// An EncryptingWriter encrypts the output and writes it to the underlying +// writer. It's very important to call .Flush() to write the MAC after the data +// has been written. +type EncryptingWriter struct { + w io.Writer + stream cipher.Stream + mac hash.Hash + buf []byte +} + +// Writer returns a new writer that encrypts its output. Don't forget to call +// .Flush() to write the MAC. +func (k EncryptionKey) Writer(w io.Writer) (*EncryptingWriter, error) { + aesKey, hmacKey := k[:aesKeySize], k[aesKeySize:] + block, err := aes.NewCipher(aesKey) + if err != nil { + return nil, err + } + mac := hmac.New(sha512.New, hmacKey) + nonce := make([]byte, nonceSize) + rand.Read(nonce) + mac.Write(nonce) + if _, err := w.Write(nonce); err != nil { + return nil, err + } + return &EncryptingWriter{ + w: w, + stream: cipher.NewCTR(block, nonce), + mac: mac, + }, nil +} + +// Write encrypts buf and writes it to the underlying writer. +func (w *EncryptingWriter) Write(buf []byte) (int, error) { + if len(w.buf) < len(buf) { + w.buf = slices.Grow(w.buf, len(buf)-len(w.buf)) + } + w.buf = w.buf[:len(buf)] + // Encrypt-then-MAC + w.stream.XORKeyStream(w.buf, buf) + w.mac.Write(w.buf) + return w.w.Write(w.buf) +} + +// Flush writes the MAC for the encrypted message. Flush must be called after +// all data has been written. +func (w *EncryptingWriter) Flush() error { + _, err := w.w.Write(w.mac.Sum(nil)) + return err +} + +// A DecryptingReader decrypts data from an underlying reader. +type DecryptingReader struct { + r cipher.StreamReader +} + +// Reader returns a new DecryptingReader. The data is processed in two passes, +// first to verify the MAC, then the io.Seeker interface is used to reset the +// reader for decryption. +func (k EncryptionKey) Reader(r io.ReadSeeker) (*DecryptingReader, error) { + aesKey, hmacKey := k[:aesKeySize], k[aesKeySize:] + block, err := aes.NewCipher(aesKey) + if err != nil { + return nil, err + } + totalLen, err := r.Seek(0, io.SeekEnd) + if err != nil { + return nil, err + } + if totalLen < certSize { + return nil, errors.New("file too short") + } + dataLen := totalLen - certSize + if _, err := r.Seek(0, io.SeekStart); err != nil { + return nil, err + } + mac := hmac.New(sha512.New, hmacKey) + if _, err := io.Copy(mac, &io.LimitedReader{R: r, N: dataLen}); err != nil { + return nil, err + } + expectedMAC := mac.Sum(nil) + sig := make([]byte, certSize) + if _, err := io.ReadFull(r, sig); err != nil { + return nil, err + } + if !hmac.Equal(sig, expectedMAC) { + return nil, fmt.Errorf("invalid mac (got %x, want %x)", sig, expectedMAC) + } + if _, err := r.Seek(0, io.SeekStart); err != nil { + return nil, err + } + nonce := make([]byte, nonceSize) + if _, err := io.ReadFull(r, nonce); err != nil { + return nil, err + } + return &DecryptingReader{cipher.StreamReader{S: cipher.NewCTR(block, nonce), R: &io.LimitedReader{R: r, N: dataLen - nonceSize}}}, nil +} + +// Read reads and decrypts data from the underlying reader. +func (r *DecryptingReader) Read(buf []byte) (int, error) { + return r.r.Read(buf) +} |
