diff options
Diffstat (limited to 'db/verification.go')
-rw-r--r-- | db/verification.go | 805 |
1 files changed, 805 insertions, 0 deletions
diff --git a/db/verification.go b/db/verification.go new file mode 100644 index 0000000..ea623c7 --- /dev/null +++ b/db/verification.go @@ -0,0 +1,805 @@ +package db + +import ( + "crypto" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "database/sql" + "encoding/base64" + "encoding/pem" + "errors" + "fmt" + "math/rand" + "net/smtp" + "os" + "os/exec" + "time" + + "github.com/FChannel0/FChannel-Server/activitypub" + _ "github.com/lib/pq" + + crand "crypto/rand" + "io/ioutil" + "net/http" + "regexp" + "strings" +) + +type Verify struct { + Type string + Identifier string + Code string + Created string + Board string +} + +type VerifyCooldown struct { + Identifier string + Code string + Time int +} + +type Signature struct { + KeyId string + Headers []string + Signature string + Algorithm string +} + +func DeleteBoardMod(verify Verify) error { + query := `select code from boardaccess where identifier=$1 and board=$1` + + rows, err := db.Query(query, verify.Identifier, verify.Board) + if err != nil { + return err + } + + defer rows.Close() + + var code string + rows.Next() + rows.Scan(&code) + + if code != "" { + query := `delete from crossverification where code=$1` + + if _, err := db.Exec(query, code); err != nil { + return err + } + + query = `delete from boardaccess where identifier=$1 and board=$2` + + if _, err := db.Exec(query, verify.Identifier, verify.Board); err != nil { + return err + } + } + + return nil +} + +func GetBoardMod(identifier string) (Verify, error) { + var nVerify Verify + + query := `select code, board, type, identifier from boardaccess where identifier=$1` + + rows, err := db.Query(query, identifier) + + if err != nil { + return nVerify, err + } + + defer rows.Close() + + rows.Next() + rows.Scan(&nVerify.Code, &nVerify.Board, &nVerify.Type, &nVerify.Identifier) + + return nVerify, nil +} + +func CreateBoardMod(verify Verify) error { + pass := CreateKey(50) + + query := `select code from verification where identifier=$1 and type=$2` + + rows, err := db.Query(query, verify.Board, verify.Type) + if err != nil { + return err + } + + defer rows.Close() + + var code string + + rows.Next() + rows.Scan(&code) + + if code != "" { + + query := `select identifier from boardaccess where identifier=$1 and board=$2` + + rows, err := db.Query(query, verify.Identifier, verify.Board) + if err != nil { + return err + } + + defer rows.Close() + + var ident string + rows.Next() + rows.Scan(&ident) + + if ident != verify.Identifier { + + query := `insert into crossverification (verificationcode, code) values ($1, $2)` + + if _, err := db.Exec(query, code, pass); err != nil { + return err + } + + query = `insert into boardaccess (identifier, code, board, type) values ($1, $2, $3, $4)` + + if _, err = db.Exec(query, verify.Identifier, pass, verify.Board, verify.Type); err != nil { + return err + } + + fmt.Printf("Board access - Board: %s, Identifier: %s, Code: %s\n", verify.Board, verify.Identifier, pass) + } + } + + return nil +} + +func CreateVerification(verify Verify) error { + query := `insert into verification (type, identifier, code, created) values ($1, $2, $3, $4)` + + _, err := db.Exec(query, verify.Type, verify.Identifier, verify.Code, time.Now().UTC().Format(time.RFC3339)) + return err +} + +func GetVerificationByEmail(email string) (Verify, error) { + // TODO: this only needs to select one row. + + var verify Verify + + query := `select type, identifier, code, board from boardaccess where identifier=$1` + + rows, err := db.Query(query, email) + if err != nil { + return verify, err + } + + defer rows.Close() + + for rows.Next() { + if err := rows.Scan(&verify.Type, &verify.Identifier, &verify.Code, &verify.Board); err != nil { + return verify, err + } + } + + return verify, nil +} + +func GetVerificationByCode(code string) (Verify, error) { + // TODO: this only needs to select one row. + + var verify Verify + + query := `select type, identifier, code, board from boardaccess where code=$1` + + rows, err := db.Query(query, code) + if err != nil { + return verify, err + } + + defer rows.Close() + + for rows.Next() { + if err := rows.Scan(&verify.Type, &verify.Identifier, &verify.Code, &verify.Board); err != nil { + return verify, err + } + } + + return verify, nil +} + +func GetVerificationCode(verify Verify) (Verify, error) { + var nVerify Verify + + query := `select type, identifier, code, board from boardaccess where identifier=$1 and board=$2` + + rows, err := db.Query(query, verify.Identifier, verify.Board) + if err != nil { + return verify, err + } + + defer rows.Close() + + for rows.Next() { + if err := rows.Scan(&nVerify.Type, &nVerify.Identifier, &nVerify.Code, &nVerify.Board); err != nil { + return nVerify, err + } + + } + + return nVerify, nil +} + +func VerifyCooldownCurrent(auth string) (VerifyCooldown, error) { + var current VerifyCooldown + + query := `select identifier, code, time from verificationcooldown where code=$1` + + rows, err := db.Query(query, auth) + if err != nil { + query := `select identifier, code, time from verificationcooldown where identifier=$1` + + rows, err := db.Query(query, auth) + + if err != nil { + return current, err + } + + defer rows.Close() + + for rows.Next() { + if err := rows.Scan(¤t.Identifier, ¤t.Code, ¤t.Time); err != nil { + return current, err + } + } + } else { + defer rows.Close() + } + + for rows.Next() { + if err := rows.Scan(¤t.Identifier, ¤t.Code, ¤t.Time); err != nil { + return current, err + } + } + + return current, nil +} + +func VerifyCooldownAdd(verify Verify) error { + query := `insert into verficationcooldown (identifier, code) values ($1, $2)` + + _, err := db.Exec(query, verify.Identifier, verify.Code) + return err +} + +func VerficationCooldown() error { + query := `select identifier, code, time from verificationcooldown` + + rows, err := db.Query(query) + if err != nil { + return err + } + + defer rows.Close() + + for rows.Next() { + var verify VerifyCooldown + + if err := rows.Scan(&verify.Identifier, &verify.Code, &verify.Time); err != nil { + return err + } + + nTime := verify.Time - 1 + + query = `update set time=$1 where identifier=$2` + + if _, err := db.Exec(query, nTime, verify.Identifier); err != nil { + return err + } + + VerficationCooldownRemove() + } + + return nil +} + +func VerficationCooldownRemove() error { + query := `delete from verificationcooldown where time < 1` + + _, err := db.Exec(query) + return err +} + +func SendVerification(verify Verify) error { + fmt.Println("sending email") + + from := SiteEmail + pass := SiteEmailPassword + to := verify.Identifier + body := fmt.Sprintf("You can use either\r\nEmail: %s \r\n Verfication Code: %s\r\n for the board %s", verify.Identifier, verify.Code, verify.Board) + + msg := "From: " + from + "\n" + + "To: " + to + "\n" + + "Subject: Image Board Verification\n\n" + + body + + return smtp.SendMail(SiteEmailServer+":"+SiteEmailPort, + smtp.PlainAuth("", from, pass, SiteEmailServer), + from, []string{to}, []byte(msg)) +} + +func IsEmailSetup() bool { + if SiteEmail == "" { + return false + } else if SiteEmailPassword == "" { + return false + } else if SiteEmailServer == "" { + return false + } else if SiteEmailPort == "" { + return false + } + + return true +} + +func HasAuth(code string, board string) (bool, error) { + verify, err := GetVerificationByCode(code) + if err != nil { + return false, err + } + + if verify.Board == Domain || (HasBoardAccess(db, verify) && verify.Board == board) { + return true, nil + } + + return false, nil +} + +func HasAuthCooldown(auth string) (bool, error) { + current, err := VerifyCooldownCurrent(auth) + if err != nil { + return false, err + } + + if current.Time > 0 { + return true, nil + } + + // fmt.Println("has auth is false") + return false, nil +} + +func GetVerify(access string) (Verify, error) { + verify, err := GetVerificationByCode(access) + if err != nil { + return verify, err + } + + if verify.Identifier == "" { + verify, err = GetVerificationByEmail(access) + } + + return verify, err +} + +func CreateNewCaptcha() error { + id := RandomID(8) + file := "public/" + id + ".png" + + for true { + if _, err := os.Stat("./" + file); err == nil { + id = RandomID(8) + file = "public/" + id + ".png" + } else { + break + } + } + + captcha := Captcha() + + var pattern string + rnd := fmt.Sprintf("%d", rand.Intn(3)) + + srnd := string(rnd) + + switch srnd { + case "0": + pattern = "pattern:verticalbricks" + break + + case "1": + pattern = "pattern:verticalsaw" + break + + case "2": + pattern = "pattern:hs_cross" + break + + } + + cmd := exec.Command("convert", "-size", "200x98", pattern, "-transparent", "white", file) + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return err + } + + cmd = exec.Command("convert", file, "-fill", "blue", "-pointsize", "62", "-annotate", "+0+70", captcha, "-tile", "pattern:left30", "-gravity", "center", "-transparent", "white", file) + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return err + } + + rnd = fmt.Sprintf("%d", rand.Intn(24)-12) + + cmd = exec.Command("convert", file, "-rotate", rnd, "-wave", "5x35", "-distort", "Arc", "20", "-wave", "2x35", "-transparent", "white", file) + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return err + } + + var verification Verify + verification.Type = "captcha" + verification.Code = captcha + verification.Identifier = file + + return CreateVerification(verification) +} + +func CreateBoardAccess(verify Verify) error { + hasAccess, err := HasBoardAccess(verify) + if err != nil { + return err + } + + if !hasAccess { + query := `insert into boardaccess (identifier, board) values($1, $2)` + + _, err := db.Exec(query, verify.Identifier, verify.Board) + return err + } + + return nil +} + +func HasBoardAccess(verify Verify) (bool, error) { + query := `select count(*) from boardaccess where identifier=$1 and board=$2` + + rows, err := db.Query(query, verify.Identifier, verify.Board) + if err != nil { + return false, err + } + + defer rows.Close() + + var count int + + rows.Next() + rows.Scan(&count) + + if count > 0 { + return true, nil + } else { + return false, nil + } +} + +func BoardHasAuthType(board string, auth string) (bool, error) { + authTypes, err := GetActorAuth(board) + if err != nil { + return false, err + } + + for _, e := range authTypes { + if e == auth { + return true, nil + } + } + + return false, nil +} + +func Captcha() string { + rand.Seed(time.Now().UTC().UnixNano()) + domain := "ABEFHKMNPQRSUVWXYZ#$&" + rng := 4 + newID := "" + for i := 0; i < rng; i++ { + newID += string(domain[rand.Intn(len(domain))]) + } + + return newID +} + +func CreatePem(actor Actor) error { + privatekey, err := rsa.GenerateKey(crand.Reader, 2048) + if err != nil { + return err + } + + privateKeyBytes := x509.MarshalPKCS1PrivateKey(privatekey) + + privateKeyBlock := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: privateKeyBytes, + } + + privatePem, err := os.Create("./pem/board/" + actor.Name + "-private.pem") + if err != nil { + return err + } + + if err := pem.Encode(privatePem, privateKeyBlock); err != nil { + return err + } + + publickey := &privatekey.PublicKey + publicKeyBytes, err := x509.MarshalPKIXPublicKey(publickey) + if err != nil { + return err + } + + publicKeyBlock := &pem.Block{ + Type: "PUBLIC KEY", + Bytes: publicKeyBytes, + } + + publicPem, err := os.Create("./pem/board/" + actor.Name + "-public.pem") + if err != nil { + return err + } + + if err := pem.Encode(publicPem, publicKeyBlock); err != nil { + return err + } + + _, err = os.Stat("./pem/board/" + actor.Name + "-public.pem") + if os.IsNotExist(err) { + return err + } else { + return StorePemToDB(actor) + } + + fmt.Println(`Created PEM keypair for the "` + actor.Name + `" board. Please keep in mind that +the PEM key is crucial in identifying yourself as the legitimate owner of the board, +so DO NOT LOSE IT!!! If you lose it, YOU WILL LOSE ACCESS TO YOUR BOARD!`) + + return nil +} + +func CreatePublicKeyFromPrivate(actor *activitypub.Actor, publicKeyPem string) error { + publicFilename, err := GetActorPemFileFromDB(publicKeyPem) + if err != nil { + return err + } + + privateFilename := strings.ReplaceAll(publicFilename, "public.pem", "private.pem") + if _, err := os.Stat(privateFilename); err == nil { + // Not a lost cause + priv, err := ioutil.ReadFile(privateFilename) + if err != nil { + return err + } + + block, _ := pem.Decode([]byte(priv)) + if block == nil || block.Type != "RSA PRIVATE KEY" { + return errors.New("failed to decode PEM block containing public key") + } + + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return err + } + + publicKeyDer, err := x509.MarshalPKIXPublicKey(&key.PublicKey) + if err != nil { + return err + } + + pubKeyBlock := pem.Block{ + Type: "PUBLIC KEY", + Headers: nil, + Bytes: publicKeyDer, + } + + publicFileWriter, err := os.Create(publicFilename) + if err != nil { + return err + } + + if err := pem.Encode(publicFileWriter, &pubKeyBlock); err != nil { + return err + } + } else { + fmt.Println(`\nUnable to locate private key from public key generation. Now, +this means that you are now missing the proof that you are the +owner of the "` + actor.Name + `" board. If you are the developer, +then your job is just as easy as generating a new keypair, but +if this board is live, then you'll also have to convince the other +owners to switch their public keys for you so that they will start +accepting your posts from your board from this site. Good luck ;)`) + return errors.New("unable to locate private key") + } + return nil +} + +func StorePemToDB(actor activitypub.Actor) error { + query := "select publicKeyPem from actor where id=$1" + rows, err := db.Query(query, actor.Id) + if err != nil { + return err + } + + defer rows.Close() + + var result string + rows.Next() + rows.Scan(&result) + + if result != "" { + return errors.New("already storing public key for actor") + } + + publicKeyPem := actor.Id + "#main-key" + query = "update actor set publicKeyPem=$1 where id=$2" + if _, err := db.Exec(query, publicKeyPem, actor.Id); err != nil { + return err + } + + file := "./pem/board/" + actor.Name + "-public.pem" + query = "insert into publicKeyPem (id, owner, file) values($1, $2, $3)" + _, err = db.Exec(query, publicKeyPem, actor.Id, file) + return err +} + +func ActivitySign(actor activitypub.Actor, signature string) (string, error) { + query := `select file from publicKeyPem where id=$1 ` + + rows, err := db.Query(query, actor.PublicKey.Id) + if err != nil { + return "", err + } + + defer rows.Close() + + var file string + rows.Next() + rows.Scan(&file) + + file = strings.ReplaceAll(file, "public.pem", "private.pem") + _, err = os.Stat(file) + if err != nil { + fmt.Println(`\n Unable to locate private key. Now, +this means that you are now missing the proof that you are the +owner of the "` + actor.Name + `" board. If you are the developer, +then your job is just as easy as generating a new keypair, but +if this board is live, then you'll also have to convince the other +owners to switch their public keys for you so that they will start +accepting your posts from your board from this site. Good luck ;)`) + return "", errors.New("unable to locate private key") + } + + publickey, err := ioutil.ReadFile(file) + if err != nil { + return "", err + } + + block, _ := pem.Decode(publickey) + + pub, _ := x509.ParsePKCS1PrivateKey(block.Bytes) + rng := crand.Reader + hashed := sha256.New() + hashed.Write([]byte(signature)) + cipher, _ := rsa.SignPKCS1v15(rng, pub, crypto.SHA256, hashed.Sum(nil)) + + return base64.StdEncoding.EncodeToString(cipher), nil +} + +func ActivityVerify(actor activitypub.Actor, signature string, verify string) error { + sig, _ := base64.StdEncoding.DecodeString(signature) + + if actor.PublicKey.PublicKeyPem == "" { + actor = FingerActor(actor.Id) + } + + block, _ := pem.Decode([]byte(actor.PublicKey.PublicKeyPem)) + pub, _ := x509.ParsePKIXPublicKey(block.Bytes) + + hashed := sha256.New() + hashed.Write([]byte(verify)) + + return rsa.VerifyPKCS1v15(pub.(*rsa.PublicKey), crypto.SHA256, hashed.Sum(nil), sig) +} + +func VerifyHeaderSignature(r *http.Request, actor activitypub.Actor) bool { + s := ParseHeaderSignature(r.Header.Get("Signature")) + + var method string + var path string + var host string + var date string + var digest string + var contentLength string + + var sig string + for i, e := range s.Headers { + var nl string + if i < len(s.Headers)-1 { + nl = "\n" + } + + switch e { + case "(request-target)": + method = strings.ToLower(r.Method) + path = r.URL.Path + sig += "(request-target): " + method + " " + path + "" + nl + break + case "host": + host = r.Host + sig += "host: " + host + "" + nl + break + case "date": + date = r.Header.Get("date") + sig += "date: " + date + "" + nl + break + case "digest": + digest = r.Header.Get("digest") + sig += "digest: " + digest + "" + nl + break + case "content-length": + contentLength = r.Header.Get("content-length") + sig += "content-length: " + contentLength + "" + nl + break + } + } + + if s.KeyId != actor.PublicKey.Id { + return false + } + + t, _ := time.Parse(time.RFC1123, date) + + if time.Now().UTC().Sub(t).Seconds() > 75 { + return false + } + + if ActivityVerify(actor, s.Signature, sig) != nil { + return false + } + + return true +} + +func ParseHeaderSignature(signature string) Signature { + var nsig Signature + + keyId := regexp.MustCompile(`keyId=`) + headers := regexp.MustCompile(`headers=`) + sig := regexp.MustCompile(`signature=`) + algo := regexp.MustCompile(`algorithm=`) + + signature = strings.ReplaceAll(signature, "\"", "") + parts := strings.Split(signature, ",") + + for _, e := range parts { + if keyId.MatchString(e) { + nsig.KeyId = keyId.ReplaceAllString(e, "") + continue + } + + if headers.MatchString(e) { + header := headers.ReplaceAllString(e, "") + nsig.Headers = strings.Split(header, " ") + continue + } + + if sig.MatchString(e) { + nsig.Signature = sig.ReplaceAllString(e, "") + continue + } + + if algo.MatchString(e) { + nsig.Algorithm = algo.ReplaceAllString(e, "") + continue + } + } + + return nsig +} |