diff --git a/CONFIG b/CONFIG new file mode 100644 index 0000000..56500f0 --- /dev/null +++ b/CONFIG @@ -0,0 +1,11 @@ +SENDGRID_API_KEY="" +AWS_REGION="us-east-2" +SMTP_SERVER="" +SMTP_PORT="" +SMTP_USERNAME="" +SMTP_PASSWORD="" +EMAIL_TEMPLATE_PATH="template.txt" +EMAIL_CONTENT_TYPE="html" +EMAIL_SUBJECT="A file is shared with you" +FROM_NAME="" +FROM_EMAIL="" diff --git a/README.md b/README.md new file mode 100644 index 0000000..b71053c --- /dev/null +++ b/README.md @@ -0,0 +1,178 @@ +# Introduction + +``` + ,_ + ,' '\,_ Utku Sen's + |_,-'_) wholeaked + /##c '\ ( + ' |' -{. ) "When you have eliminated the impossible, + /\__-' \[] whatever remains, however improbable, + /'-_'\ must be the truth" - Sherlock Holmes + ' \ + ``` + +wholeaked is a file-sharing tool that allows you to find the responsible person in case of a leakage. It's written in Go. + +## How? + +wholeaked gets the file that will be shared and a list of recipients. It creates a unique signature for each recipient and adds it to the file secretly. After then, you can automatically send files to the corresponding recipients by using Sendgrid, AWS SES or SMTP integrations. Instead of sending them by e-mail, you can also share them manually. + +wholeaked works with every file type. However, it has additional features for common file types such as PDF, DOCX, MOV etc. + +### Sharing Process + +``` + +-----------+ + |Top Secret | + |.pdf | + | | + -| | + / | | + / |Hidden | + a@gov / |signature1 | + / +-----------+ + / +-----------+ ++-----------++-----------+ / |Top Secret | +|Top Secret ||Recipient | / |.pdf | +|.pdf ||List | +---------+ / | | +| || | |utkusen/ | / b@gov | | +| ||a@gov |----->|wholeaked| /----------+ | +| ||b@gov | | | \ |Hidden | +| ||c@gov | +---------+ \ |signature2 | +| || | \ +-----------+ ++-----------++-----------+ \ +-----------+ + \ |Top Secret | + \ |.pdf | + c@gov \ | | + \ | | + \ | | + \ |Hidden | + -|signature3 | + +-----------+ +``` + +### Validation Part + +To find who leaked the document, you just need to provide the leaked file to wholeaked and it will reveal the responsible person by comparing the signatures in the database. + +``` ++-----------+ +---------+ +|Top Secret | |Signature| +|.pdf | +---------+|Database | +| | |utkusen/ || | Document leaked by +| |->|wholeaked|| |--------+ +| | | || | b@gov +|Hidden | +---------+| | +|Signature2 | | | ++-----------+ +---------+ + +``` + +## Demonstration Video + +[![Demo Video](https://img.youtube.com/vi/EEDtXp9ngHw/0.jpg)](https://www.youtube.com/watch?v=EEDtXp9ngHw) + +## File Types and Detection Modes + +wholeaked can add the unique signature to different sections of a file. Available detection modes are given below: + +**File Hash:** SHA256 hash of the file. All file types are supported. + +**Binary:** The signature is added to the binary of a file. *Almost* all filetypes are supported. + +**Metadata:** The signature is added to a metadata section of a file. Supported file types: PDF, DOCX, XLSX, PPTX, MOV, JPG, PNG, GIF, EPS, AI, PSD + +**Watermark:** An invisible signature is inserted to the text. Only PDF files are supported. + +# Installation + +## From Binary + +You can download the pre-built binaries from the [releases](https://github.com/utkusen/wholeaked/releases/latest) page and run. For example: + +`tar xzvf wholeaked_0.1.0_Linux_amd64.tar.gz` + +`./wholeaked --help` + +## From Source + +1) Install Go on your system + +2) Run: `go get -u github.com/utkusen/wholeaked` + +## Installing Dependencies + +wholeaked requires `exiftool` for adding signatures to metadata section of files. If you don't want to use this feature, you don't need to install it. + +1) Debian-based Linux: Run `apt install exiftool` +2) macOS: Run `brew install exiftool` +3) Windows: Download exiftool from here https://exiftool.org/ and put the `exiftool.exe` in the same directory with wholeaked. + +wholeaked requires `pdftotext` for verifying watermarks inside PDF files. If you don't want to use this feature, you don't need to install it. + +1) Download "Xpdf command line tools" for Linux, macOS or Windows from here: https://www.xpdfreader.com/download.html +2) Extract the archive and navigate to `bin64` folder. +3) Copy the `pdftotext` (or `pdftotext.exe`) executable to the same folder with wholeaked +4) For Debian Based Linux: Run `apt install libfontconfig` command. + +# Usage + +## Basic Usage + +wholeaked requires a project name `-n`, path of the base file which the signatures will added `-f` and a list of target recipients `-t` + +Example command: `./wholeaked -n test_project -f secret.pdf -t targets.txt` + +The `targets.txt` file should contain name and the e-mail address in the following format: + +``` +Utku Sen,utku@utkusen.com +Bill Gates,bill@microsoft.com +``` + +After execution is completed, the following unique files will be generated: + +``` +test_project/files/Utku_Sen/secret.pdf +test_project/files/Bill_Gates/secret.pdf +``` + +By default, wholeaked adds signatures to all available places that are defined in the "File Types and Detection Modes" section. If you don't want to use a method, you can define it with a `false` flag. For example: + +`./wholeaked -n test_project -f secret.pdf -t targets.txt -binary=false -metadata=false -watermark=false` + +## Sending E-mails + +In order to send e-mails, you need to fill some sections in the `CONFIG` file. + +- If you want to send e-mails via Sendgrid, type your API key to the `SENDGRID_API_KEY` section. + +- If you want to send e-mails via AWS SES integration, you need to install `awscli` on your machine and add the required AWS key to it. wholeaked will read the key by itself. But you need to fill the `AWS_REGION` section in the config file. + +- If you want to send e-mails via a SMTP server, fill the `SMTP_SERVER`, `SMTP_PORT`, `SMTP_USERNAME`, `SMTP_PASSWORD` sections. + +The other necessary fields to fill: + +- `EMAIL_TEMPLATE_PATH` Path of the e-mail's body. You can specify use HTML or text format. +- `EMAIL_CONTENT_TYPE` Can be `html` or `text` +- `EMAIL_SUBJECT` Subject of the e-mail +- `FROM_NAME` From name of the e-mail +- `FROM_EMAIL` From e-mail of the e-mail + +To specify the sending method, you can use `-sendgrid`, `-ses` or `-smtp` flags. For example: + +`./wholeaked -n test_project -f secret.pdf -t targets.txt -sendgrid` + +## Validating a Leaked File + +You can use the `-validate` flag to reveal the owner of a leaked file. wholeaked will compare the signatures detected in the file and the database located in the project folder. Example: + +`./wholeaked -n test_project -f secret.pdf -validate` + +**Important:** You shouldn't delete the `project_folder/db.csv` file if you want to use the file validation feature. If that file is deleted, wholeaked won't be able to compare the signatures. + +# Donation + +Loved the project? You can buy me a coffee + +Buy Me A Coffee \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9fee5f9 --- /dev/null +++ b/go.mod @@ -0,0 +1,28 @@ +module github.com/utkusen/wholeaked + +go 1.17 + +require ( + github.com/aws/aws-sdk-go v1.42.38 + github.com/barasher/go-exiftool v1.7.0 + github.com/fatih/color v1.13.0 + github.com/google/uuid v1.3.0 + github.com/pdfcpu/pdfcpu v0.3.13 + github.com/sendgrid/sendgrid-go v3.10.5+incompatible + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df +) + +require ( + github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650 // indirect + github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/mattn/go-colorable v0.1.9 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/sendgrid/rest v2.6.7+incompatible // indirect + golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb // indirect + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect + golang.org/x/text v0.3.6 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..42016ad --- /dev/null +++ b/go.sum @@ -0,0 +1,62 @@ +github.com/aws/aws-sdk-go v1.42.38 h1:/fNQTB4ZUQOa8+cfX7C7F0zyXRdiN1jGKKXt3+5nmzM= +github.com/aws/aws-sdk-go v1.42.38/go.mod h1:OGr6lGMAKGlG9CVrYnWYDKIyb829c6EVBRjxqjmPepc= +github.com/barasher/go-exiftool v1.7.0 h1:EOGb5D6TpWXmqsnEjJ0ai6+tIW2gZFwIoS9O/33Nixs= +github.com/barasher/go-exiftool v1.7.0/go.mod h1:F9s/a3uHSM8YniVfwF+sbQUtP8Gmh9nyzigNF+8vsWo= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hhrutter/lzw v0.0.0-20190827003112-58b82c5a41cc/go.mod h1:yJBvOcu1wLQ9q9XZmfiPfur+3dQJuIhYQsMGLYcItZk= +github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650 h1:1yY/RQWNSBjJe2GDCIYoLmpWVidrooriUr4QS/zaATQ= +github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650/go.mod h1:yJBvOcu1wLQ9q9XZmfiPfur+3dQJuIhYQsMGLYcItZk= +github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7 h1:o1wMw7uTNyA58IlEdDpxIrtFHTgnvYzA8sCQz8luv94= +github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7/go.mod h1:WkUxfS2JUu3qPo6tRld7ISb8HiC0gVSU91kooBMDVok= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/pdfcpu/pdfcpu v0.3.13 h1:VFon2Yo1PJt+sA57vPAeXWGLSZ7Ux3Jl4h02M0+s3dg= +github.com/pdfcpu/pdfcpu v0.3.13/go.mod h1:UJc5xsXg0fpmjp1zOPdyYcAQArc/Zf3V0nv5URe+9fg= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sendgrid/rest v2.6.7+incompatible h1:VitKiUoCWxqUSezj7gHtG3tAjQPXElDcj6Gxflog6pA= +github.com/sendgrid/rest v2.6.7+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= +github.com/sendgrid/sendgrid-go v3.10.5+incompatible h1:2f/d7odubrZMkwqSupQDU5ad1GkS8syopBapDazh5bM= +github.com/sendgrid/sendgrid-go v3.10.5+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +golang.org/x/image v0.0.0-20190823064033-3a9bac650e44/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk= +golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM= +golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/main.go b/main.go new file mode 100644 index 0000000..18700c1 --- /dev/null +++ b/main.go @@ -0,0 +1,860 @@ +package main + +import ( + "archive/zip" + "bufio" + "bytes" + "crypto/sha256" + "crypto/tls" + "encoding/base64" + "flag" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strconv" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ses" + "github.com/barasher/go-exiftool" + "github.com/fatih/color" + "github.com/google/uuid" + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/sendgrid/sendgrid-go" + "github.com/sendgrid/sendgrid-go/helpers/mail" + "gopkg.in/gomail.v2" +) + +var currentDir, _ = os.Getwd() + +func main() { + fmt.Println(` + ,_ + ,' '\,_ Utku Sen's + |_,-'_) wholeaked + /##c '\ ( + ' |' -{. ) "When you have eliminated the impossible, + /\__-' \[] whatever remains, however improbable, + /'-_'\ must be the truth" - Sherlock Holmes + ' \ + `) + + projectName := flag.String("n", "", "Name of the project") + targetsFile := flag.String("t", "", "Path of the targets file") + baseFile := flag.String("f", "", "Path of the base file") + binaryFlag := flag.Bool("binary", true, "Add a unique signature to the binary") + metadataFlag := flag.Bool("metadata", true, "Add a unique signature to metadata of the file") + watermarkFlag := flag.Bool("watermark", true, "Add a unique signature to the PDF file as a watermark") + sendgridFlag := flag.Bool("sendgrid", false, "Send files with Sendgrid Integration") + sesFlag := flag.Bool("ses", false, "Send files with AWS SES Integration") + smtpFlag := flag.Bool("smtp", false, "Send files with a SMTP server") + validateFlag := flag.Bool("validate", false, "Find who leaked the file") + flag.Parse() + if *projectName == "" { + color.Red("Project name (-n) is required.") + flag.PrintDefaults() + os.Exit(1) + } + if *targetsFile == "" && !*validateFlag { + color.Red("Targets file (-t) is required.") + flag.PrintDefaults() + os.Exit(1) + } + if *baseFile == "" { + color.Red("Base file (-f) is required.") + flag.PrintDefaults() + os.Exit(1) + } + + if !*binaryFlag && !*metadataFlag && !*watermarkFlag && !*validateFlag { + color.Red("No flags are set") + os.Exit(1) + } + startProcess(*baseFile, *targetsFile, *projectName, *binaryFlag, *metadataFlag, *watermarkFlag, *sendgridFlag, *sesFlag, *smtpFlag, *validateFlag) + +} + +func startProcess(baseFile string, targetsFile string, projectName string, binaryFlag bool, metadataFlag bool, watermarkFlag bool, sendgridFlag bool, sesFlag bool, smtpFlag bool, validateFlag bool) { + fmt.Println("Operation started") + projectDir := filepath.Join(currentDir, projectName) + dbPath := filepath.Join(projectDir, "db.csv") + existsFlag := false + if validateFlag { + detectLeak(baseFile, dbPath) + return + } + if _, err := os.Stat(targetsFile); os.IsNotExist(err) { + color.Red("Targets file does not exist.") + os.Exit(1) + } + + if _, err := os.Stat(baseFile); os.IsNotExist(err) { + color.Red("Targets file does not exist.") + os.Exit(1) + } + err := os.Mkdir(projectDir, 0777) + if err != nil { + if !sendgridFlag && !sesFlag && !smtpFlag { + color.Red("Project already exists.") + os.Exit(1) + } else { + fmt.Println("Local files are already created for this project. Do you want to send them? (y/n)") + reader := bufio.NewReader(os.Stdin) + input, err := reader.ReadString('\n') + if err != nil { + fmt.Println("An error occured while reading input. Please try again", err) + os.Exit(1) + } + input = strings.TrimSuffix(input, "\n") + if input != "y" { + os.Exit(1) + } else { + existsFlag = true + } + } + } + if !existsFlag { + generateTargetDB(dbPath, readTargets(targetsFile)) + createLocalFiles(baseFile, projectName, binaryFlag, metadataFlag, watermarkFlag) + color.Magenta("Local files are created") + } + configs := parseConfigFile() + + if sendgridFlag { + fmt.Println("Sending files with Sendgrid") + for _, line := range readTargets(dbPath) { + sendWithSendgrid(strings.Split(line, ",")[0], strings.Split(line, ",")[1], configs["FROM_NAME"], configs["FROM_EMAIL"], configs["EMAIL_SUBJECT"], configs["EMAIL_TEMPLATE_PATH"], configs["EMAIL_CONTENT_TYPE"], strings.Split(line, ",")[4]) + } + } + if sesFlag { + fmt.Println("Sending files with AWS SES") + for _, line := range readTargets(dbPath) { + sendWithSES(strings.Split(line, ",")[0], strings.Split(line, ",")[1], configs["FROM_NAME"], configs["FROM_EMAIL"], configs["EMAIL_SUBJECT"], configs["EMAIL_TEMPLATE_PATH"], configs["EMAIL_CONTENT_TYPE"], strings.Split(line, ",")[4], configs["AWS_REGION"]) + } + } + if smtpFlag { + fmt.Println("Sending files with the SMTP Server") + for _, line := range readTargets(dbPath) { + sendWithSMTP(strings.Split(line, ",")[0], strings.Split(line, ",")[1], configs["FROM_NAME"], configs["FROM_EMAIL"], configs["EMAIL_SUBJECT"], configs["EMAIL_TEMPLATE_PATH"], configs["EMAIL_CONTENT_TYPE"], strings.Split(line, ",")[4]) + } + } + + fmt.Println("Operation Done. Bye") + +} + +func detectWatermarkPDF(file string, signature string) bool { + operatingSystem := runtime.GOOS + var cmd *exec.Cmd + if operatingSystem == "windows" { + cmd = exec.Command(filepath.Join(currentDir, "pdftotext.exe"), file) + } else { + cmd = exec.Command(filepath.Join(currentDir, "pdftotext"), file) + } + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + color.Red("Couldn't run pdftotext binary") + fmt.Println(err) + os.Exit(1) + } + outPath := strings.Replace(file, ".pdf", ".txt", -1) + content, err := ioutil.ReadFile(outPath) + if err != nil { + color.Red("Couldn't read the PDF file") + fmt.Println(err) + os.Exit(1) + } + + text := string(content) + os.Remove(outPath) + return strings.Contains(text, signature) +} + +func addWatermarkPDF(file string, signature string) { + onTop := false + update := false + wm, _ := api.TextWatermark(signature, "sc:.9, rot:0, mo:1, op:0", onTop, update, pdfcpu.POINTS) + api.AddWatermarksFile(file, "", nil, wm, nil) +} + +func detectLeak(file string, dbPath string) { + targets := readTargets(dbPath) + foundFlag := false + for _, target := range targets { + signature := strings.Split(target, ",")[2] + name := strings.Replace(strings.Split(target, ",")[0], " ", "_", -1) + fileHash := strings.Split(target, ",")[3] + binaryFlag, hashFlag, metadataFlag, watermarkFlag := detectSignature(file, signature, fileHash) + if hashFlag { + color.Magenta("File Hash Matched: " + name) + foundFlag = true + } + if binaryFlag { + color.Magenta("Signature Detected in Binary: " + name) + foundFlag = true + } + if metadataFlag { + color.Magenta("Signature Detected in Metadata: " + name) + foundFlag = true + } + if watermarkFlag { + color.Magenta("Watermark Matched: " + name) + foundFlag = true + } + } + if !foundFlag { + fmt.Println("No match found.") + } +} + +func getHash(file string) string { + f, err := os.Open(file) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + defer f.Close() + + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + fmt.Println(err) + os.Exit(1) + } + return fmt.Sprintf("%x", h.Sum(nil)) +} + +func applySignature(file string, signature string, binaryFlag bool, metadataFlag bool, watermarkFlag bool) { + extension := filepath.Ext(file) + if extension == ".pdf" && watermarkFlag { + addWatermarkPDF(file, signature) + } + if metadataFlag { + addMetadataSignature(file, signature) + } + if extension != ".docx" && extension != ".xlsx" && extension != ".pptx" { + if binaryFlag { + appendSignature(file, signature) + } + } + +} + +func appendSignature(file string, signature string) { + signature = " " + signature + f, err := os.OpenFile(file, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + defer f.Close() + _, err = f.WriteString(signature) + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func listFiles(root string) ([]string, error) { + var files []string + + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if !info.IsDir() { + files = append(files, path) + } + return nil + }) + if err != nil { + return nil, err + } + return files, nil + +} + +func addMetadataSignature(file string, signature string) { + var metaSection string + extension := filepath.Ext(file) + if extension == ".docx" || extension == ".xlsx" || extension == ".pptx" { + workingDir, _ := filepath.Abs(filepath.Dir(file)) + Unzip(file, filepath.Join(workingDir, "temp")) + coreFile := filepath.Join(workingDir, "temp", "docProps", "core.xml") + if _, err := os.Stat(coreFile); os.IsNotExist(err) { + color.Red("Document doesn't contain core.xml metadata section. It usually happens if you use Google Docs. Try to use Microsoft Office instead.") + os.Exit(1) + } else { + f, err := os.Open(coreFile) + if err != nil { + color.Red("Error occurred during reading core.xml file") + fmt.Println(err) + os.Exit(1) + } + defer f.Close() + scanner := bufio.NewScanner(f) + var fileContent string + for scanner.Scan() { + fileContent += scanner.Text() + } + re := regexp.MustCompile(`([\w\s]+)`) + match := re.FindStringSubmatch(fileContent) + if match == nil { + color.Red("Document doesn't contain core.xml metadata section. It usually happens if you use Google Docs. Try to use Microsoft Office instead.") + os.Exit(1) + } + newCreator := strings.Replace(fileContent, match[0], ""+signature+"", 1) + _ = ioutil.WriteFile(coreFile, []byte(newCreator), 0644) + os.Remove(file) + officeCompress(filepath.Join(workingDir, "temp"), file) + os.RemoveAll(filepath.Join(workingDir, "temp")) + } + + } else { + switch { + case extension == ".pdf": + metaSection = "Producer" + case extension == ".mov": + metaSection = "Software" + default: + metaSection = "Title" + } + targetDir := filepath.Dir(file) + tempDir := filepath.Join(targetDir, "temp") + os.Mkdir(filepath.Join(tempDir), 0777) + fileName := filepath.Base(file) + tempFile := filepath.Join(tempDir, fileName) + copyFile(file, tempFile) + e, _ := exiftool.NewExiftool() + defer e.Close() + originals := e.ExtractMetadata(tempFile) + originals[0].SetString(metaSection, signature) + e.WriteMetadata(originals) + os.Remove(file) + copyFile(tempFile, file) + os.RemoveAll(tempDir) + + } +} + +func officeCompress(source string, target string) { + files, err := listFiles(source) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + buf := new(bytes.Buffer) + w := zip.NewWriter(buf) + for _, file := range files { + absPath, _ := filepath.Abs(file) + relPath, _ := filepath.Rel(source, absPath) + f, err := w.Create(relPath) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + fh, err := os.Open(absPath) + if err != nil { + fmt.Println(err) + os.Exit(1) + + } + _, err = io.Copy(f, fh) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + fh.Close() + } + err = w.Close() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + err = ioutil.WriteFile(target, buf.Bytes(), 0644) + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func exifRead(file string, field string) string { + et, err := exiftool.NewExiftool() + if err != nil { + fmt.Printf("Error when intializing: %v\n", err) + return "" + } + defer et.Close() + + fileInfos := et.ExtractMetadata(file) + + for _, fileInfo := range fileInfos { + if fileInfo.Err != nil { + fmt.Printf("Error concerning %v: %v\n", fileInfo.File, fileInfo.Err) + continue + } + + for k, v := range fileInfo.Fields { + if k == field { + exifValue := fmt.Sprint(v) + return exifValue + + } + } + } + return os.DevNull +} + +func copyFile(src, dest string) (err error) { + s, err := os.Open(src) + if err != nil { + return err + } + defer s.Close() + + d, err := os.Create(dest) + if err != nil { + return err + } + defer d.Close() + + _, err = io.Copy(d, s) + if err != nil { + return err + } + return nil +} + +func detectSignature(file string, signature string, hashValue string) (bool, bool, bool, bool) { + binaryFlag := false + hashFlag := false + metadataFlag := false + watermarkFlag := false + fileHash := getHash(file) + if fileHash == hashValue { + hashFlag = true + } + var metaSection string + f, err := os.Open(file) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + defer f.Close() + var lines []string + scanner := bufio.NewScanner(f) + const maxCapacity = 5048 * 5048 + buf := make([]byte, maxCapacity) + scanner.Buffer(buf, maxCapacity) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + if err := scanner.Err(); err != nil { + fmt.Println(err) + os.Exit(1) + } + for _, line := range lines { + + if strings.Contains(line, signature) { + + binaryFlag = true + } + } + extension := filepath.Ext(file) + switch { + case extension == ".pdf": + metaSection = "Producer" + watermarkFlag = detectWatermarkPDF(file, signature) + case extension == ".mov": + metaSection = "Software" + case extension == ".docx": + metaSection = "Creator" + case extension == ".xlsx": + metaSection = "Creator" + case extension == ".pptx": + metaSection = "Creator" + default: + metaSection = "Title" + } + exifValue := exifRead(file, metaSection) + metadataFlag = strings.Contains(exifValue, signature) + + return binaryFlag, hashFlag, metadataFlag, watermarkFlag +} + +func readTargets(file string) []string { + f, err := os.Open(file) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + defer f.Close() + var lines []string + scanner := bufio.NewScanner(f) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + if err := scanner.Err(); err != nil { + fmt.Println(err) + os.Exit(1) + } + return lines +} + +func generateSignature() string { + id := uuid.New() + return "75746b7573656e-" + id.String() +} + +func generateTargetDB(file string, targets []string) { + f, err := os.Create(file) + if err != nil { + color.Red("Can't create the database file") + fmt.Println(err) + os.Exit(1) + } + defer f.Close() + for _, target := range targets { + if target != "" { + if !strings.Contains(target, ",") || strings.Count(target, ",") > 1 { + color.Red("Wrong target format: " + target) + color.Red("It should be something like this: Utku Sen,utku@utkusen.com") + os.Exit(1) + } + _, err := f.WriteString(target + "," + generateSignature() + "\n") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + } + } +} + +func createLocalFiles(baseFile string, projectName string, binaryFlag bool, metadataFlag bool, watermarkFlag bool) { + currentDir, _ := os.Getwd() + projectDir := filepath.Join(currentDir, projectName) + fileDir := filepath.Join(projectDir, "files") + targets := readTargets(filepath.Join(projectDir, "db.csv")) + err := os.Mkdir(fileDir, 0777) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + var updatedDB string + var fileHash string + for _, target := range targets { + if target != "" { + signature := strings.Split(target, ",")[2] + name := strings.Replace(strings.Split(target, ",")[0], " ", "_", -1) + privateDir := filepath.Join(fileDir, name) + err := os.Mkdir(privateDir, 0777) + if err != nil { + color.Red("Can't create the folder") + fmt.Println(err) + os.Exit(1) + } + fileLocation := filepath.Join(privateDir, filepath.Base(baseFile)) + _ = CopyFile(baseFile, fileLocation) + applySignature(fileLocation, signature, binaryFlag, metadataFlag, watermarkFlag) + fileHash = getHash(fileLocation) + updatedDB += target + "," + fileHash + "," + fileLocation + "\n" + + } + err = ioutil.WriteFile(filepath.Join(projectDir, "db.csv"), []byte(updatedDB), 0644) + if err != nil { + color.Red("Can't write to the database") + fmt.Println(err) + os.Exit(1) + } + } +} + +func CopyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + _, err = io.Copy(out, in) + cerr := out.Close() + if err != nil { + return err + } + return cerr +} + +func Unzip(src string, destination string) ([]string, error) { + var filenames []string + r, err := zip.OpenReader(src) + if err != nil { + return filenames, err + } + + defer r.Close() + for _, f := range r.File { + fpath := filepath.Join(destination, f.Name) + if !strings.HasPrefix(fpath, filepath.Clean(destination)+string(os.PathSeparator)) { + return filenames, fmt.Errorf("%s is an illegal filepath", fpath) + } + filenames = append(filenames, fpath) + + if f.FileInfo().IsDir() { + os.MkdirAll(fpath, os.ModePerm) + continue + } + + if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { + return filenames, err + } + outFile, err := os.OpenFile(fpath, + os.O_WRONLY|os.O_CREATE|os.O_TRUNC, + f.Mode()) + if err != nil { + return filenames, err + } + + rc, err := f.Open() + if err != nil { + return filenames, err + } + + _, err = io.Copy(outFile, rc) + outFile.Close() + rc.Close() + if err != nil { + return filenames, err + } + } + return filenames, nil +} + +func GetFileContentType(out *os.File) string { + buffer := make([]byte, 512) + _, err := out.Read(buffer) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + contentType := http.DetectContentType(buffer) + + return contentType +} + +func sendWithSendgrid(toName string, toEmail string, fromName string, fromEmail string, subject string, bodyFile string, contentType string, attachment string) { + configFile, err := os.Open("CONFIG") + if err != nil { + color.Red("Can't read the CONFIG file") + fmt.Println(err) + os.Exit(1) + } + defer configFile.Close() + scanner := bufio.NewScanner(configFile) + for scanner.Scan() { + if strings.Contains(scanner.Text(), "SENDGRID_API_KEY") { + apiKey := strings.TrimSpace(strings.Replace(strings.Split(scanner.Text(), "=")[1], "\"", "", -1)) + if apiKey == "" { + fmt.Println("No Sendgrid API Key is set in the CONFIG file") + os.Exit(1) + } else { + body, err := ioutil.ReadFile(bodyFile) + if err != nil { + color.Red("Error occurred while reading the template file") + fmt.Println(err) + os.Exit(1) + } + m := mail.NewV3Mail() + newBody := strings.Replace(string(body), "{{Name}}", toName, -1) + from := mail.NewEmail(fromName, fromEmail) + to := mail.NewEmail(toName, toEmail) + content := mail.NewContent("text/plain", newBody) + if contentType == "text" { + content = mail.NewContent("text/plain", newBody) + } else if contentType == "html" { + content = mail.NewContent("text/html", newBody) + } else { + fmt.Println("Content type is not set correctly") + os.Exit(1) + } + m.SetFrom(from) + m.AddContent(content) + personalization := mail.NewPersonalization() + personalization.AddTos(to) + personalization.Subject = subject + m.AddPersonalizations(personalization) + f, err := os.Open(attachment) + if err != nil { + color.Red("Can't open the attachment file") + fmt.Println(err) + os.Exit(1) + } + defer f.Close() + contentType := GetFileContentType(f) + attachmentFile := mail.NewAttachment() + dat, err := ioutil.ReadFile(attachment) + if err != nil { + fmt.Println(err) + } + encoded := base64.StdEncoding.EncodeToString([]byte(dat)) + attachmentFile.SetContent(encoded) + attachmentFile.SetType(contentType) + fileName := filepath.Base(attachment) + attachmentFile.SetFilename(fileName) + attachmentFile.SetDisposition("attachment") + m.AddAttachment(attachmentFile) + request := sendgrid.GetRequest(apiKey, "/v3/mail/send", "https://api.sendgrid.com") + request.Method = "POST" + request.Body = mail.GetRequestBody(m) + response, err := sendgrid.API(request) + if err != nil { + color.Red("Error occurred while using Sendgrid's API") + fmt.Println(err) + os.Exit(1) + } + if response.StatusCode == 202 { + color.Green("Email sent successfully to : " + toName + " (" + toEmail + ")") + } else { + color.Red("Email couldn't sent to : " + toName + " (" + toEmail + ") Check your Sendgrid Integration:") + fmt.Println(response) + os.Exit(1) + } + + } + + } + } +} + +func sendWithSES(toName string, toEmail string, fromName string, fromEmail string, subject string, bodyFile string, contentType string, attachment string, region string) { + sess, err := session.NewSession(&aws.Config{ + Region: aws.String(region)}, + ) + if err != nil { + color.Red("Error occurred while creating AWS session") + fmt.Println(err) + os.Exit(1) + } + body, err := ioutil.ReadFile(bodyFile) + if err != nil { + color.Red("Error occurred while reading the template file") + fmt.Println(err) + os.Exit(1) + } + newBody := strings.Replace(string(body), "{{Name}}", toName, -1) + msg := gomail.NewMessage() + svc := ses.New(sess) + msg.SetAddressHeader("From", fromEmail, fromName) + type Recipient struct { + toEmails []string + } + recipient := Recipient{ + toEmails: []string{toEmail}, + } + var recipients []*string + for _, r := range recipient.toEmails { + recipient := r + recipients = append(recipients, &recipient) + } + msg.SetHeader("To", recipient.toEmails...) + msg.SetHeader("Subject", subject) + if contentType == "text" { + msg.SetBody("text/plain", newBody) + } else if contentType == "html" { + msg.SetBody("text/html", newBody) + } else { + fmt.Println("Content type is not set correctly") + os.Exit(1) + } + msg.Attach(attachment) + var emailRaw bytes.Buffer + msg.WriteTo(&emailRaw) + message := ses.RawMessage{Data: emailRaw.Bytes()} + input := &ses.SendRawEmailInput{Source: &fromEmail, Destinations: recipients, RawMessage: &message} + _, err = svc.SendRawEmail(input) + if err != nil { + color.Red("Email couldn't sent to : " + toName + " (" + toEmail + ")") + fmt.Println(err) + os.Exit(1) + } + color.Green("Email sent successfully to : " + toName + " (" + toEmail + ")") +} + +func sendWithSMTP(toName string, toEmail string, fromName string, fromEmail string, subject string, bodyFile string, contentType string, attachment string) { + configs := parseConfigFile() + + if configs["SMTP_SERVER"] == "" { + color.Red("No SMTP Server is set in the CONFIG file") + os.Exit(1) + } + + if configs["SMTP_PORT"] == "" { + color.Red("No SMTP Port is set in the CONFIG file") + os.Exit(1) + } + + m := gomail.NewMessage() + m.SetHeader("From", m.FormatAddress(fromEmail, fromName)) + m.SetHeader("To", toEmail) + m.SetHeader("Subject", subject) + body, err := ioutil.ReadFile(bodyFile) + if err != nil { + color.Red("Error occurred while reading the template file") + fmt.Println(err) + os.Exit(1) + } + newBody := strings.Replace(string(body), "{{Name}}", toName, -1) + if contentType == "text" { + m.SetBody("text/plain", newBody) + } else if contentType == "html" { + m.SetBody("text/html", newBody) + } else { + color.Red("Content type is not set correctly") + os.Exit(1) + } + m.Attach(attachment) + intPort, err := strconv.Atoi(configs["SMTP_PORT"]) + if err != nil { + color.Red("Invalid SMTP Port") + os.Exit(1) + } + d := gomail.NewDialer(configs["SMTP_SERVER"], intPort, configs["SMTP_USERNAME"], configs["SMTP_PASSWORD"]) + d.TLSConfig = &tls.Config{InsecureSkipVerify: true} + if err := d.DialAndSend(m); err != nil { + color.Red("Email couldn't sent to : " + toName + " (" + toEmail + ")") + fmt.Println(err) + os.Exit(1) + } else { + color.Green("Email sent successfully to : " + toName + " (" + toEmail + ")") + } + +} + +func parseConfigFile() map[string]string { + f, err := os.Open("CONFIG") + if err != nil { + color.Red("Can't read the CONFIG file") + fmt.Println(err) + os.Exit(1) + } + defer f.Close() + scanner := bufio.NewScanner(f) + var config = make(map[string]string) + for scanner.Scan() { + if strings.Contains(scanner.Text(), "=") { + key := strings.TrimSpace(strings.Split(scanner.Text(), "=")[0]) + value := strings.TrimSpace(strings.Replace(strings.Split(scanner.Text(), "=")[1], "\"", "", -1)) + config[key] = value + } + } + return config +} diff --git a/template.txt b/template.txt new file mode 100644 index 0000000..c2dea1c --- /dev/null +++ b/template.txt @@ -0,0 +1,4 @@ +Dear {{Name}}

+ +A file is shared with you. You can find it in the attachment. +