Compare commits

...

4 Commits

Author SHA1 Message Date
Andrea Spacca
a7b600c562 refinement 2022-03-02 22:49:20 +09:00
Andrea Spacca
715f2b4cd9 Merge branch 'main' into gpg-encryption-support 2022-03-02 22:34:40 +09:00
Andrea Spacca
fbf9a4facc gpg encryption support 2018-10-06 19:24:40 +02:00
Andrea Spacca
0c844c1f11 gpg encryption support 2018-10-06 19:04:46 +02:00
6 changed files with 185 additions and 27 deletions

View File

@@ -6,6 +6,7 @@ bin
*.pyc *.pyc
*.egg-info *.egg-info
.vagrant .vagrant
.git
.tmp .tmp
bower_components bower_components
node_modules node_modules

View File

@@ -18,3 +18,7 @@ issues:
max-same-issues: 0 max-same-issues: 0
new: false new: false
exclude-use-default: false exclude-use-default: false
exclude-rules:
- linters:
- staticcheck
text: "SA1019:"

View File

@@ -47,6 +47,16 @@ $ curl --upload-file ./hello.txt https://transfer.sh/hello.txt -H "Max-Downloads
$ curl --upload-file ./hello.txt https://transfer.sh/hello.txt -H "Max-Days: 1" # Set the number of days before deletion $ curl --upload-file ./hello.txt https://transfer.sh/hello.txt -H "Max-Days: 1" # Set the number of days before deletion
``` ```
### X-Encrypt-Password
```bash
$ curl --upload-file ./hello.txt https://transfer.sh/hello.txt -H "X-Encrypt-Password: test" # Encrypt the content sever side with AES265 using "test" as password
```
### X-Decrypt-Password
```bash
$ curl https://transfer.sh/BAYh0/hello.txt -H "X-Decrypt-Password: test" # Decrypt the content sever side with AES265 using "test" as password
```
## Response Headers ## Response Headers
### X-Url-Delete ### X-Url-Delete

View File

@@ -58,6 +58,12 @@ import (
"github.com/microcosm-cc/bluemonday" "github.com/microcosm-cc/bluemonday"
blackfriday "github.com/russross/blackfriday/v2" blackfriday "github.com/russross/blackfriday/v2"
"github.com/skip2/go-qrcode" "github.com/skip2/go-qrcode"
//lint:ignore SA1019 Ignore the deprecation warnings
"golang.org/x/crypto/openpgp"
//lint:ignore SA1019 Ignore the deprecation warnings
"golang.org/x/crypto/openpgp/armor"
//lint:ignore SA1019 Ignore the deprecation warnings
"golang.org/x/crypto/openpgp/packet"
) )
const getPathPart = "get" const getPathPart = "get"
@@ -65,6 +71,10 @@ const getPathPart = "get"
var ( var (
htmlTemplates = initHTMLTemplates() htmlTemplates = initHTMLTemplates()
textTemplates = initTextTemplates() textTemplates = initTextTemplates()
packetConfig = &packet.Config{
DefaultCipher: packet.CipherAES256,
}
) )
func stripPrefix(path string) string { func stripPrefix(path string) string {
@@ -88,6 +98,96 @@ func initHTMLTemplates() *html_template.Template {
return templates return templates
} }
func transformEncryptionReader(reader io.ReadCloser, password string) (io.Reader, error) {
if len(password) == 0 {
return reader, nil
}
return encrypt(reader, password, packetConfig)
}
func transformDecryptionReader(reader io.ReadCloser, password string) (io.Reader, error) {
if len(password) == 0 {
return reader, nil
}
return decrypt(reader, password, packetConfig)
}
func decrypt(ciphertext io.ReadCloser, password string, packetConfig *packet.Config) (plaintext io.Reader, err error) {
content, err := ioutil.ReadAll(ciphertext)
if err != nil {
return
}
decbuf := bytes.NewBuffer(content)
armorBlock, err := armor.Decode(decbuf)
if err != nil {
return
}
failed := false
prompt := func(keys []openpgp.Key, symmetric bool) ([]byte, error) {
// If the given passphrase isn't correct, the function will be called again, forever.
// This method will fail fast.
// Ref: https://godoc.org/golang.org/x/crypto/openpgp#PromptFunction
if failed {
return nil, errors.New("decryption failed")
}
failed = true
return []byte(password), nil
}
md, err := openpgp.ReadMessage(armorBlock.Body, nil, prompt, packetConfig)
if err != nil {
return
}
plaintext = md.UnverifiedBody
return
}
func encrypt(plaintext io.ReadCloser, password string, packetConfig *packet.Config) (ciphertext io.Reader, err error) {
encbuf := bytes.NewBuffer(nil)
w, err := armor.Encode(encbuf, "PGP MESSAGE", nil)
if err != nil {
safeClose(w)
return
}
pt, err := openpgp.SymmetricallyEncrypt(w, []byte(password), nil, packetConfig)
if err != nil {
safeClose(pt)
safeClose(w)
return
}
content, err := ioutil.ReadAll(plaintext)
if err != nil {
safeClose(pt)
safeClose(w)
return
}
_, err = pt.Write(content)
if err != nil {
safeClose(pt)
safeClose(w)
return
}
// Close writers to force-flush their buffer
safeClose(pt)
safeClose(w)
ciphertext = bytes.NewReader(encbuf.Bytes())
return
}
func healthHandler(w http.ResponseWriter, r *http.Request) { func healthHandler(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("Approaching Neutral Zone, all systems normal and functioning.")) _, _ = w.Write([]byte("Approaching Neutral Zone, all systems normal and functioning."))
} }
@@ -360,7 +460,7 @@ func (s *Server) postHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
metadata := metadataForRequest(contentType, s.randomTokenLength, r) metadata := metadataForRequest(contentType, contentLength, s.randomTokenLength, r)
buffer := &bytes.Buffer{} buffer := &bytes.Buffer{}
if err := json.NewEncoder(buffer).Encode(metadata); err != nil { if err := json.NewEncoder(buffer).Encode(metadata); err != nil {
@@ -377,7 +477,13 @@ func (s *Server) postHandler(w http.ResponseWriter, r *http.Request) {
s.logger.Printf("Uploading %s %s %d %s", token, filename, contentLength, contentType) s.logger.Printf("Uploading %s %s %d %s", token, filename, contentLength, contentType)
if err = s.storage.Put(r.Context(), token, filename, file, contentType, uint64(contentLength)); err != nil { reader, err := transformEncryptionReader(file, r.Header.Get("X-Encrypt-Password"))
if err != nil {
http.Error(w, "Could not crypt file", http.StatusInternalServerError)
return
}
if err = s.storage.Put(r.Context(), token, filename, reader, contentType, uint64(contentLength)); err != nil {
s.logger.Printf("Backend storage error: %s", err.Error()) s.logger.Printf("Backend storage error: %s", err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@@ -415,8 +521,8 @@ func (s *Server) cleanTmpFile(f *os.File) {
type metadata struct { type metadata struct {
// ContentType is the original uploading content type // ContentType is the original uploading content type
ContentType string ContentType string
// Secret as knowledge to delete file // ContentLength is is the original uploading content length
// Secret string ContentLength int64
// Downloads is the actual number of downloads // Downloads is the actual number of downloads
Downloads int Downloads int
// MaxDownloads contains the maximum numbers of downloads // MaxDownloads contains the maximum numbers of downloads
@@ -425,11 +531,16 @@ type metadata struct {
MaxDate time.Time MaxDate time.Time
// DeletionToken contains the token to match against for deletion // DeletionToken contains the token to match against for deletion
DeletionToken string DeletionToken string
// Encrypted contains if the file was encrypted
Encrypted bool
// DecryptedContentType is the original uploading content type
DecryptedContentType string
} }
func metadataForRequest(contentType string, randomTokenLength int, r *http.Request) metadata { func metadataForRequest(contentType string, contentLength int64, randomTokenLength int, r *http.Request) metadata {
metadata := metadata{ metadata := metadata{
ContentType: strings.ToLower(contentType), ContentType: strings.ToLower(contentType),
ContentLength: contentLength,
MaxDate: time.Time{}, MaxDate: time.Time{},
Downloads: 0, Downloads: 0,
MaxDownloads: -1, MaxDownloads: -1,
@@ -448,6 +559,14 @@ func metadataForRequest(contentType string, randomTokenLength int, r *http.Reque
metadata.MaxDate = time.Now().Add(time.Hour * 24 * time.Duration(v)) metadata.MaxDate = time.Now().Add(time.Hour * 24 * time.Duration(v))
} }
if password := r.Header.Get("X-Encrypt-Password"); password != "" {
metadata.Encrypted = true
metadata.ContentType = "text/plain; charset=utf-8"
metadata.DecryptedContentType = contentType
} else {
metadata.Encrypted = false
}
return metadata return metadata
} }
@@ -458,7 +577,7 @@ func (s *Server) putHandler(w http.ResponseWriter, r *http.Request) {
contentLength := r.ContentLength contentLength := r.ContentLength
defer CloseCheck(r.Body.Close) defer safeClose(r.Body)
file, err := ioutil.TempFile(s.tempPath, "transfer-") file, err := ioutil.TempFile(s.tempPath, "transfer-")
defer s.cleanTmpFile(file) defer s.cleanTmpFile(file)
@@ -521,7 +640,7 @@ func (s *Server) putHandler(w http.ResponseWriter, r *http.Request) {
token := token(s.randomTokenLength) token := token(s.randomTokenLength)
metadata := metadataForRequest(contentType, s.randomTokenLength, r) metadata := metadataForRequest(contentType, contentLength, s.randomTokenLength, r)
buffer := &bytes.Buffer{} buffer := &bytes.Buffer{}
if err := json.NewEncoder(buffer).Encode(metadata); err != nil { if err := json.NewEncoder(buffer).Encode(metadata); err != nil {
@@ -540,7 +659,13 @@ func (s *Server) putHandler(w http.ResponseWriter, r *http.Request) {
s.logger.Printf("Uploading %s %s %d %s", token, filename, contentLength, contentType) s.logger.Printf("Uploading %s %s %d %s", token, filename, contentLength, contentType)
if err = s.storage.Put(r.Context(), token, filename, file, contentType, uint64(contentLength)); err != nil { reader, err := transformEncryptionReader(file, r.Header.Get("X-Encrypt-Password"))
if err != nil {
http.Error(w, "Could not crypt file", http.StatusInternalServerError)
return
}
if err = s.storage.Put(r.Context(), token, filename, reader, contentType, uint64(contentLength)); err != nil {
s.logger.Printf("Error putting new file: %s", err.Error()) s.logger.Printf("Error putting new file: %s", err.Error())
http.Error(w, "Could not save file", http.StatusInternalServerError) http.Error(w, "Could not save file", http.StatusInternalServerError)
return return
@@ -651,6 +776,7 @@ func (metadata metadata) remainingLimitHeaderValues() (remainingDownloads, remai
remainingDays = strconv.Itoa(int(timeDifference.Hours()/24) + 1) remainingDays = strconv.Itoa(int(timeDifference.Hours()/24) + 1)
} }
if metadata.MaxDownloads == -1 { if metadata.MaxDownloads == -1 {
remainingDownloads = "n/a" remainingDownloads = "n/a"
} else { } else {
@@ -683,7 +809,7 @@ func (s *Server) checkMetadata(ctx context.Context, token, filename string, incr
var metadata metadata var metadata metadata
r, _, err := s.storage.Get(ctx, token, fmt.Sprintf("%s.metadata", filename)) r, _, err := s.storage.Get(ctx, token, fmt.Sprintf("%s.metadata", filename))
defer CloseCheck(r.Close) defer safeClose(r)
if err != nil { if err != nil {
return metadata, err return metadata, err
@@ -719,7 +845,7 @@ func (s *Server) checkDeletionToken(ctx context.Context, deletionToken, token, f
var metadata metadata var metadata metadata
r, _, err := s.storage.Get(ctx, token, fmt.Sprintf("%s.metadata", filename)) r, _, err := s.storage.Get(ctx, token, fmt.Sprintf("%s.metadata", filename))
defer CloseCheck(r.Close) defer safeClose(r)
if s.storage.IsNotExist(err) { if s.storage.IsNotExist(err) {
return errors.New("metadata doesn't exist") return errors.New("metadata doesn't exist")
@@ -798,7 +924,7 @@ func (s *Server) zipHandler(w http.ResponseWriter, r *http.Request) {
} }
reader, _, err := s.storage.Get(r.Context(), token, filename) reader, _, err := s.storage.Get(r.Context(), token, filename)
defer CloseCheck(reader.Close) defer safeClose(reader)
if err != nil { if err != nil {
if s.storage.IsNotExist(err) { if s.storage.IsNotExist(err) {
@@ -852,10 +978,10 @@ func (s *Server) tarGzHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Connection", "close") w.Header().Set("Connection", "close")
gw := gzip.NewWriter(w) gw := gzip.NewWriter(w)
defer CloseCheck(gw.Close) defer safeClose(gw)
zw := tar.NewWriter(gw) zw := tar.NewWriter(gw)
defer CloseCheck(zw.Close) defer safeClose(zw)
for _, key := range strings.Split(files, ",") { for _, key := range strings.Split(files, ",") {
key = resolveKey(key, s.proxyPath) key = resolveKey(key, s.proxyPath)
@@ -869,7 +995,7 @@ func (s *Server) tarGzHandler(w http.ResponseWriter, r *http.Request) {
} }
reader, contentLength, err := s.storage.Get(r.Context(), token, filename) reader, contentLength, err := s.storage.Get(r.Context(), token, filename)
defer CloseCheck(reader.Close) defer safeClose(reader)
if err != nil { if err != nil {
if s.storage.IsNotExist(err) { if s.storage.IsNotExist(err) {
@@ -914,7 +1040,7 @@ func (s *Server) tarHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Connection", "close") w.Header().Set("Connection", "close")
zw := tar.NewWriter(w) zw := tar.NewWriter(w)
defer CloseCheck(zw.Close) defer safeClose(zw)
for _, key := range strings.Split(files, ",") { for _, key := range strings.Split(files, ",") {
key = resolveKey(key, s.proxyPath) key = resolveKey(key, s.proxyPath)
@@ -928,7 +1054,7 @@ func (s *Server) tarHandler(w http.ResponseWriter, r *http.Request) {
} }
reader, contentLength, err := s.storage.Get(r.Context(), token, filename) reader, contentLength, err := s.storage.Get(r.Context(), token, filename)
defer CloseCheck(reader.Close) defer safeClose(reader)
if err != nil { if err != nil {
if s.storage.IsNotExist(err) { if s.storage.IsNotExist(err) {
@@ -1012,7 +1138,7 @@ func (s *Server) getHandler(w http.ResponseWriter, r *http.Request) {
contentType := metadata.ContentType contentType := metadata.ContentType
reader, contentLength, err := s.storage.Get(r.Context(), token, filename) reader, contentLength, err := s.storage.Get(r.Context(), token, filename)
defer CloseCheck(reader.Close) defer safeClose(reader)
if s.storage.IsNotExist(err) { if s.storage.IsNotExist(err) {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
@@ -1024,7 +1150,6 @@ func (s *Server) getHandler(w http.ResponseWriter, r *http.Request) {
} }
var disposition string var disposition string
if action == "inline" { if action == "inline" {
disposition = "inline" disposition = "inline"
} else { } else {
@@ -1033,9 +1158,7 @@ func (s *Server) getHandler(w http.ResponseWriter, r *http.Request) {
remainingDownloads, remainingDays := metadata.remainingLimitHeaderValues() remainingDownloads, remainingDays := metadata.remainingLimitHeaderValues()
w.Header().Set("Content-Type", contentType) w.Header().Set("Content-Disposition", fmt.Sprintf(`%s; filename="%s"`, disposition, filename))
w.Header().Set("Content-Length", strconv.FormatUint(contentLength, 10))
w.Header().Set("Content-Disposition", fmt.Sprintf("%s; filename=\"%s\"", disposition, filename))
w.Header().Set("Connection", "keep-alive") w.Header().Set("Connection", "keep-alive")
w.Header().Set("X-Remaining-Downloads", remainingDownloads) w.Header().Set("X-Remaining-Downloads", remainingDownloads)
w.Header().Set("X-Remaining-Days", remainingDays) w.Header().Set("X-Remaining-Days", remainingDays)
@@ -1065,7 +1188,22 @@ func (s *Server) getHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
if _, err = io.Copy(w, reader); err != nil { password := r.Header.Get("X-Decrypt-Password");
decryptionReader, err := transformDecryptionReader(reader, password)
if err != nil {
http.Error(w, "Could not decrypt file", http.StatusInternalServerError)
return
}
if metadata.Encrypted && len(password) > 0 {
contentType = metadata.DecryptedContentType
contentLength = uint64(metadata.ContentLength)
}
w.Header().Set("Content-Type", contentType)
w.Header().Set("Content-Length", strconv.FormatUint(contentLength, 10))
if _, err = io.Copy(w, decryptionReader); err != nil {
s.logger.Printf("%s", err.Error()) s.logger.Printf("%s", err.Error())
http.Error(w, "Error occurred copying to output stream", http.StatusInternalServerError) http.Error(w, "Error occurred copying to output stream", http.StatusInternalServerError)
return return

View File

@@ -151,7 +151,7 @@ func (s *LocalStorage) Put(ctx context.Context, token string, filename string, r
} }
f, err = os.OpenFile(filepath.Join(path, filename), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) f, err = os.OpenFile(filepath.Join(path, filename), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
defer CloseCheck(f.Close) defer safeClose(f)
if err != nil { if err != nil {
return err return err
@@ -650,7 +650,7 @@ func getGDriveTokenFromWeb(ctx context.Context, config *oauth2.Config, logger *l
// Retrieves a token from a local file. // Retrieves a token from a local file.
func gDriveTokenFromFile(file string) (*oauth2.Token, error) { func gDriveTokenFromFile(file string) (*oauth2.Token, error) {
f, err := os.Open(file) f, err := os.Open(file)
defer CloseCheck(f.Close) defer safeClose(f)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -663,7 +663,7 @@ func gDriveTokenFromFile(file string) (*oauth2.Token, error) {
func saveGDriveToken(path string, token *oauth2.Token, logger *log.Logger) { func saveGDriveToken(path string, token *oauth2.Token, logger *log.Logger) {
logger.Printf("Saving credential file to: %s\n", path) logger.Printf("Saving credential file to: %s\n", path)
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
defer CloseCheck(f.Close) defer safeClose(f)
if err != nil { if err != nil {
logger.Fatalf("Unable to cache oauth token: %v", err) logger.Fatalf("Unable to cache oauth token: %v", err)
} }

View File

@@ -28,6 +28,7 @@ package server
import ( import (
"fmt" "fmt"
"io"
"math" "math"
"net/http" "net/http"
"net/mail" "net/mail"
@@ -280,8 +281,12 @@ func formatSize(size int64) string {
return fmt.Sprintf("%s %s", strconv.FormatFloat(newVal, 'f', -1, 64), getSuffix) return fmt.Sprintf("%s %s", strconv.FormatFloat(newVal, 'f', -1, 64), getSuffix)
} }
func CloseCheck(f func() error) { func safeClose(c io.Closer) {
if err := f(); err != nil { if c == nil {
return
}
if err := c.Close(); err != nil {
fmt.Println("Received close error:", err) fmt.Println("Received close error:", err)
} }
} }