Passworded pastes are here.

This commit is contained in:
2018-05-18 22:28:04 +05:00
parent b8e9895617
commit 94a0a435b6
15 changed files with 373 additions and 19 deletions

View File

@@ -27,7 +27,6 @@ package pastes
import (
// stdlib
"bytes"
//"html"
"net/http"
"regexp"
"strconv"
@@ -97,6 +96,27 @@ func pasteGET(ec echo.Context) error {
}
}
if paste.Private && paste.Password != "" {
// Check if cookie for this paste is defined. This means that user
// previously successfully entered a password.
cookie, err := ec.Cookie("PASTE-" + strconv.Itoa(pasteID))
if err != nil {
// No cookie, redirect to auth page.
c.Logger.Info().Msg("Tried to access passworded paste without autorization, redirecting to auth page...")
return ec.Redirect(http.StatusMovedPermanently, "/paste/"+pasteIDRaw+"/"+ec.Param("timestamp")+"/verify")
}
// Generate cookie value to check.
cookieValue := paste.GenerateCryptedCookieValue()
if cookieValue != cookie.Value {
c.Logger.Info().Msg("Invalid cookie, redirecting to auth page...")
return ec.Redirect(http.StatusMovedPermanently, "/paste/"+pasteIDRaw+"/"+ec.Param("timestamp")+"/verify")
}
// If all okay - do nothing :)
}
pasteHTML, err2 := static.ReadFile("paste.html")
if err2 != nil {
return ec.String(http.StatusNotFound, "parse.html wasn't found!")
@@ -106,6 +126,7 @@ func pasteGET(ec echo.Context) error {
pasteHTMLAsString := strings.Replace(string(pasteHTML), "{pasteTitle}", paste.Title, 1)
pasteHTMLAsString = strings.Replace(pasteHTMLAsString, "{pasteID}", strconv.Itoa(paste.ID), -1)
pasteHTMLAsString = strings.Replace(pasteHTMLAsString, "{pasteDate}", paste.CreatedAt.Format("2006-01-02 @ 15:04:05"), 1)
pasteHTMLAsString = strings.Replace(pasteHTMLAsString, "{pasteExpiration}", paste.GetExpirationTime().Format("2006-01-02 @ 15:04:05"), 1)
pasteHTMLAsString = strings.Replace(pasteHTMLAsString, "{pasteLanguage}", paste.Language, 1)
if paste.Private {
@@ -151,6 +172,99 @@ func pasteGET(ec echo.Context) error {
return ec.HTML(http.StatusOK, string(pasteHTMLAsString))
}
// GET for "/paste/PASTE_ID/TIMESTAMP/verify" - a password verify page.
func pastePasswordedVerifyGet(ec echo.Context) error {
verifyHTMLRaw, err := static.ReadFile("passworded_paste_verify.html")
if err != nil {
return ec.String(http.StatusNotFound, "passworded_paste_verify.html wasn't found!")
}
errhtml, err := static.ReadFile("error.html")
if err != nil {
return ec.String(http.StatusNotFound, "error.html wasn't found!")
}
pasteIDRaw := ec.Param("id")
timestampRaw := ec.Param("timestamp")
// We already get numbers from string, so we will not check strconv.Atoi()
// error.
pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0])
// Get paste.
paste, err1 := GetByID(pasteID)
if err1 != nil {
c.Logger.Error().Msgf("Failed to get paste #%d: %s", pasteID, err1.Error())
errhtmlAsString := strings.Replace(string(errhtml), "{error}", "Paste #"+strconv.Itoa(pasteID)+" not found", 1)
return ec.HTML(http.StatusBadRequest, errhtmlAsString)
}
// Check for auth cookie. If present - redirect to paste.
cookie, err := ec.Cookie("PASTE-" + strconv.Itoa(pasteID))
if err == nil {
// No cookie, redirect to auth page.
c.Logger.Debug().Msg("Paste cookie found, checking it...")
// Generate cookie value to check.
cookieValue := paste.GenerateCryptedCookieValue()
if cookieValue == cookie.Value {
c.Logger.Info().Msg("Valid cookie, redirecting to paste page...")
return ec.Redirect(http.StatusMovedPermanently, "/paste/"+pasteIDRaw+"/"+ec.Param("timestamp"))
}
c.Logger.Debug().Msg("Invalid cookie, showing auth page")
}
verifyHTML := strings.Replace(string(verifyHTMLRaw), "{pasteID}", strconv.Itoa(pasteID), -1)
verifyHTML = strings.Replace(verifyHTML, "{pasteTimestamp}", timestampRaw, 1)
return ec.HTML(http.StatusOK, verifyHTML)
}
// POST for "/paste/PASTE_ID/TIMESTAMP/verify" - a password verify page.
func pastePasswordedVerifyPost(ec echo.Context) error {
errhtml, err := static.ReadFile("error.html")
if err != nil {
return ec.String(http.StatusNotFound, "error.html wasn't found!")
}
pasteIDRaw := ec.Param("id")
timestampRaw := ec.Param("timestamp")
// We already get numbers from string, so we will not check strconv.Atoi()
// error.
pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0])
c.Logger.Debug().Msgf("Requesting paste #%+v", pasteID)
// Get paste.
paste, err1 := GetByID(pasteID)
if err1 != nil {
c.Logger.Error().Msgf("Failed to get paste #%d: %s", pasteID, err1.Error())
errhtmlAsString := strings.Replace(string(errhtml), "{error}", "Paste #"+strconv.Itoa(pasteID)+" not found", 1)
return ec.HTML(http.StatusBadRequest, errhtmlAsString)
}
params, err2 := ec.FormParams()
if err2 != nil {
c.Logger.Debug().Msg("No form parameters passed")
return ec.HTML(http.StatusBadRequest, string(errhtml))
}
if paste.VerifyPassword(params["paste-password"][0]) {
// Set cookie that this paste's password is verified and paste
// can be viewed.
cookie := new(http.Cookie)
cookie.Name = "PASTE-" + strconv.Itoa(pasteID)
cookie.Value = paste.GenerateCryptedCookieValue()
cookie.Expires = time.Now().Add(24 * time.Hour)
ec.SetCookie(cookie)
return ec.Redirect(http.StatusFound, "/paste/"+strconv.Itoa(pasteID)+"/"+timestampRaw)
}
errhtmlAsString := strings.Replace(string(errhtml), "{error}", "Invalid password. Please, try again.", 1)
return ec.HTML(http.StatusBadRequest, string(errhtmlAsString))
}
// POST for "/paste/" which will create new paste and redirect to
// "/pastes/CREATED_PASTE_ID".
func pastePOST(ec echo.Context) error {
@@ -227,10 +341,15 @@ func pastePOST(ec echo.Context) error {
// Private paste?
paste.Private = false
privateCheckbox, privateCheckboxFound := params["paste-private"]
if privateCheckboxFound && privateCheckbox[0] == "on" {
pastePassword, pastePasswordFound := params["paste-password"]
if privateCheckboxFound && privateCheckbox[0] == "on" || pastePasswordFound && len(pastePassword[0]) != 0 {
paste.Private = true
}
if len(pastePassword) != 0 {
paste.CreatePassword(pastePassword[0])
}
id, err2 := Save(paste)
if err2 != nil {
c.Logger.Debug().Msgf("Failed to save paste: %s", err2.Error())
@@ -284,6 +403,13 @@ func pasteRawGET(ec echo.Context) error {
}
}
// ToDo: figure out how to handle passworded pastes here.
// Return error for now.
if paste.Password != "" {
c.Logger.Error().Msgf("Cannot render paste #%d as raw: passworded paste. Patches welcome!", pasteID)
return ec.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found")
}
return ec.String(http.StatusOK, paste.Data)
}

View File

@@ -50,6 +50,9 @@ func New(cc *context.Context) {
c.Echo.GET("/paste/:id/:timestamp", pasteGET)
// Show RAW representation of private paste.
c.Echo.GET("/paste/:id/:timestamp/raw", pasteRawGET)
// Verify access to passworded paste.
c.Echo.GET("/paste/:id/:timestamp/verify", pastePasswordedVerifyGet)
c.Echo.POST("/paste/:id/:timestamp/verify", pastePasswordedVerifyPost)
// Pastes list.
c.Echo.GET("/pastes/", pastesGET)

View File

@@ -26,9 +26,13 @@ package pastes
import (
// stdlib
"crypto/sha256"
"fmt"
"math/rand"
"time"
// other
//"github.com/alecthomas/chroma"
"golang.org/x/crypto/scrypt"
)
const (
@@ -36,6 +40,8 @@ const (
PASTE_KEEP_FOR_HOURS = 2
PASTE_KEEP_FOR_DAYS = 3
PASTE_KEEP_FOR_MONTHS = 4
charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
)
var (
@@ -61,6 +67,35 @@ type Paste struct {
PasswordSalt string `db:"password_salt"`
}
// CreatePassword creates password for current paste.
func (p *Paste) CreatePassword(password string) error {
// Create salt - random string.
seededRand := rand.New(rand.NewSource(time.Now().UnixNano()))
saltBytes := make([]byte, 64)
for i := range saltBytes {
saltBytes[i] = charset[seededRand.Intn(len(charset))]
}
saltHashBytes := sha256.Sum256(saltBytes)
p.PasswordSalt = fmt.Sprintf("%x", saltHashBytes)
// Create crypted password and hash it.
passwordCrypted, err := scrypt.Key([]byte(password), []byte(p.PasswordSalt), 131072, 8, 1, 64)
if err != nil {
return err
}
passwordHashBytes := sha256.Sum256(passwordCrypted)
p.Password = fmt.Sprintf("%x", passwordHashBytes)
return nil
}
// GenerateCryptedCookieValue generates crypted cookie value for paste.
func (p *Paste) GenerateCryptedCookieValue() string {
cookieValueCrypted, _ := scrypt.Key([]byte(p.Password), []byte(p.PasswordSalt), 131072, 8, 1, 64)
return fmt.Sprintf("%x", sha256.Sum256(cookieValueCrypted))
}
func (p *Paste) GetExpirationTime() time.Time {
var expirationTime time.Time
switch p.KeepForUnitType {
@@ -88,3 +123,20 @@ func (p *Paste) IsExpired() bool {
return false
}
// VerifyPassword verifies that provided password is valid.
func (p *Paste) VerifyPassword(password string) bool {
// Create crypted password and hash it.
passwordCrypted, err := scrypt.Key([]byte(password), []byte(p.PasswordSalt), 131072, 8, 1, 64)
if err != nil {
return false
}
passwordHashBytes := sha256.Sum256(passwordCrypted)
providedPassword := fmt.Sprintf("%x", passwordHashBytes)
if providedPassword == p.Password {
return true
}
return false
}

View File

@@ -101,7 +101,7 @@ func GetPastesPages() int {
// Save saves paste to database and returns it's ID.
func Save(p *Paste) (int64, error) {
dbConn := c.Database.GetDatabaseConnection()
result, err := dbConn.NamedExec("INSERT INTO `pastes` (title, data, created_at, keep_for, keep_for_unit_type, language, private) VALUES (:title, :data, :created_at, :keep_for, :keep_for_unit_type, :language, :private)", p)
result, err := dbConn.NamedExec("INSERT INTO `pastes` (title, data, created_at, keep_for, keep_for_unit_type, language, private, password, password_salt) VALUES (:title, :data, :created_at, :keep_for, :keep_for_unit_type, :language, :private, :password, :password_salt)", p)
if err != nil {
return 0, err
}