From 1afe0716f5be9dd24b14788dcef5ecffa63243b5 Mon Sep 17 00:00:00 2001 From: Ron Rise Date: Sat, 20 Apr 2024 10:33:41 -0400 Subject: [PATCH] inital working commit --- .github/ISSUE_TEMPLATE/bug_report.md | 38 ++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 +++ .gitignore | 4 + LICENSE | 201 ++++++++++++++++++++++ README.md | 76 ++++++++ build.sh | 13 ++ commands/decrypt.go | 53 ++++++ commands/encrypt.go | 68 ++++++++ commands/generate-keypair.go | 71 ++++++++ crypt/file.go | 165 ++++++++++++++++++ crypt/keys.go | 130 ++++++++++++++ go.mod | 28 +++ go.sum | 49 ++++++ main.go | 111 ++++++++++++ printer/log.go | 91 ++++++++++ printer/printer.go | 19 ++ printer/styles.go | 42 +++++ 17 files changed, 1179 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100755 build.sh create mode 100644 commands/decrypt.go create mode 100644 commands/encrypt.go create mode 100644 commands/generate-keypair.go create mode 100644 crypt/file.go create mode 100644 crypt/keys.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 printer/log.go create mode 100644 printer/printer.go create mode 100644 printer/styles.go diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dd84ea7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..585bbe0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea/ +*.pem* +rsa-file-encryption +dist/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f398135 --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +# RSA Public/Private Key File Encryption + +## Build + +Nothing special is needed to build. + +``` +go build --ldflags="-X 'github.com/siteworxpro/rsa-file-encryption/printer.Version=$(git describe --tags --abbrev=0)'" +``` + +## Generating Keys + +Generates a set of RSA key pairs. Default size is 4096 bits. Minimum size is 1024 bits and maximum is 16384 bits + +``` +NAME: +rsa-file-encryption generate-keypair - generate a keypair + +USAGE: +rsa-file-encryption generate-keypair [command options] [arguments...] + +OPTIONS: +--size value, -s value the size of the private key (default: 4096) +--file value, -f value the path to the private key file +--force, -F overwrite the private key file (default: false) +--help, -h show help +``` + +```bash +./rsa-file-encryption generate-keypair -s 4096 -f my-key +``` + +## Encrypting + +Encrypt a file with a public RSA Key + +``` +NAME: + rsa-file-encryption encrypt - encrypt a file + +USAGE: + rsa-file-encryption encrypt [command options] [arguments...] + +OPTIONS: + --file value, -f value file to encrypt + --public-key value, -p value public key path + --force, -F overwrite the encrypted file (default: false) + --help, -h show help +``` + +```bash +./rsa-file-encryption encrypt --file file_to_encrypt.txt --public-key my-key.pub +``` + +## Decrypting + +Decrypt a file with a private RSA key + +``` +NAME: + rsa-file-encryption decrypt - decrypt a file + +USAGE: + rsa-file-encryption decrypt [command options] [arguments...] + +OPTIONS: + --file value, -f value file to decrypt + --private-key value, -p value private key path + --out value, -o value output file name + --force, -F overwrite the encrypted file (default: false) + --help, -h show help +``` + +```bash +./rsa-file-encryption decrypt --file file_to_encrypt.txt.enc --out file_decrypted --private-key my-key +``` \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..8f9b309 --- /dev/null +++ b/build.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +go install + +for distro in $(go tool dist list) +do + arrIN=(${distro//\// }) + + if [[ ${arrIN[0]} == 'linux' || ${arrIN[0]} == 'darwin' || ${arrIN[0]} == 'freebsd' || ${arrIN[0]} == 'windows' ]]; then + echo "Building $distro..." + GOOS=${arrIN[0]} GOARCH=${arrIN[1]} go build --ldflags="-X 'github.com/siteworxpro/rsa-file-encryption/printer.Version=$(git describe --tags --abbrev=0)'" -o dist/rsa-file-encryption_${arrIN[0]}_${arrIN[1]} + fi +done \ No newline at end of file diff --git a/commands/decrypt.go b/commands/decrypt.go new file mode 100644 index 0000000..0a25c62 --- /dev/null +++ b/commands/decrypt.go @@ -0,0 +1,53 @@ +package commands + +import ( + "fmt" + "github.com/siteworxpro/rsa-file-encryption/crypt" + "github.com/siteworxpro/rsa-file-encryption/printer" + "os" +) + +func Decrypt(privateKeyPath string, filePath string, outFile string, force bool) error { + + if _, err := os.Stat(privateKeyPath); err != nil { + return err + } + + if _, err := os.Stat(filePath); err != nil { + return err + } + + if _, err := os.Stat(outFile); err == nil && !force { + return fmt.Errorf("decrypted file already exists (--force, -F) to overwrite") + } + + p := printer.NewPrinter() + encryptedFile := crypt.EncryptedFile{} + + p.LogInfo("Reading Private Key...") + err := encryptedFile.OsReadPrivateKey(privateKeyPath) + if err != nil { + return err + } + + p.LogInfo("Reading and decrypting file...") + c := make(chan bool) + go p.LogSpinner("Decrypting...", c) + + err = encryptedFile.OsReadCipherTextFile(filePath) + if err != nil { + return err + } + + c <- true + + p.LogInfo("Writing un-encrypted file...") + err = encryptedFile.WriteDecryptedFileToDisk(outFile) + if err != nil { + return err + } + + p.LogSuccess("Done!") + + return nil +} diff --git a/commands/encrypt.go b/commands/encrypt.go new file mode 100644 index 0000000..9e8c8c2 --- /dev/null +++ b/commands/encrypt.go @@ -0,0 +1,68 @@ +package commands + +import ( + "fmt" + "github.com/siteworxpro/rsa-file-encryption/crypt" + "github.com/siteworxpro/rsa-file-encryption/printer" + "os" +) + +func Encrypt(publicKeyPath string, filePath string, force bool) error { + + if _, err := os.Stat(publicKeyPath); err != nil { + return err + } + + if _, err := os.Stat(filePath); err != nil { + return err + } + + if _, err := os.Stat(filePath + ".enc"); err == nil && !force { + return fmt.Errorf("encrypted file already exists (--force, -F) to overwrite") + } + + p := printer.NewPrinter() + encryptedFile := crypt.EncryptedFile{} + + p.LogInfo("Reading public key...") + err := encryptedFile.OsReadPublicKey(publicKeyPath) + if err != nil { + return err + } + + size := encryptedFile.PublicKey.Size() + if size < 256 { + return fmt.Errorf("key to weak. use stronger key > 2048 bits") + } + + p.LogInfo("Reading file to encrypt...") + err = encryptedFile.OsReadPlainTextFile(filePath) + if err != nil { + return err + } + + c := make(chan bool) + go p.LogSpinner("Encrypting...", c) + + err = encryptedFile.GenerateSymmetricKey() + if err != nil { + return err + } + + err = encryptedFile.EncryptFile() + if err != nil { + return err + } + + c <- true + + p.LogInfo("Encrypted file successfully") + p.LogInfo("Writing file...") + err = encryptedFile.WriteEncryptFileToDisk(filePath) + if err != nil { + return err + } + + p.LogSuccess("Done!") + return nil +} diff --git a/commands/generate-keypair.go b/commands/generate-keypair.go new file mode 100644 index 0000000..6ec846a --- /dev/null +++ b/commands/generate-keypair.go @@ -0,0 +1,71 @@ +package commands + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "github.com/siteworxpro/rsa-file-encryption/printer" + "os" +) + +func GenerateKeypair(bitSize uint, path string, overwrite bool) error { + if bitSize == 0 { + bitSize = 4096 + } + + if bitSize < 2048 { + return fmt.Errorf("key to weak. size must be greater than 2048") + } + + if bitSize > 16384 { + return fmt.Errorf("key to large. size must be less than 16384") + } + + if _, err := os.Stat(path); err == nil && !overwrite { + return fmt.Errorf("key file already exists - use another filename or -force (-F) to overwrite") + } + + p := printer.NewPrinter() + c := make(chan bool) + + go p.LogSpinner("Generating RSA key...", c) + key, err := rsa.GenerateKey(rand.Reader, int(bitSize)) + c <- true + + if err != nil { + return err + } + + pub := key.Public() + + keyPEM := pem.EncodeToMemory( + &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + }, + ) + + pubPEM := pem.EncodeToMemory( + &pem.Block{ + Type: "RSA PUBLIC KEY", + Bytes: x509.MarshalPKCS1PublicKey(pub.(*rsa.PublicKey)), + }, + ) + + p.LogInfo("Writing private key...") + err = os.WriteFile(path, keyPEM, 0600) + if err != nil { + return err + } + + p.LogInfo("Writing public key...") + err = os.WriteFile(path+".pub", pubPEM, 0644) + if err != nil { + return err + } + + p.LogSuccess("Done!") + return nil +} diff --git a/crypt/file.go b/crypt/file.go new file mode 100644 index 0000000..68636bb --- /dev/null +++ b/crypt/file.go @@ -0,0 +1,165 @@ +package crypt + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/rsa" + "crypto/subtle" + "fmt" + "os" +) + +type EncryptedFile struct { + ciphertext []byte + plainText []byte + nonce []byte + privatePem []byte + PublicPem []byte + privateKey *rsa.PrivateKey + PublicKey *rsa.PublicKey + symmetricKey []byte + symmetricKeyEnc []byte +} + +func (f *EncryptedFile) packFile() []byte { + file := append(f.nonce, f.ciphertext...) + return append(file, f.symmetricKeyEnc...) +} + +func (f *EncryptedFile) EncryptFile() error { + c, err := aes.NewCipher(f.symmetricKey) + if err != nil { + return err + } + + f.nonce = make([]byte, aes.BlockSize) + _, err = rand.Read(f.nonce) + if err != nil { + return err + } + + cbc := cipher.NewCBCEncrypter(c, f.nonce) + ciphertext := make([]byte, len(f.plainText)) + ciphertext = pad(ciphertext, aes.BlockSize) + plaintextP := pad(f.plainText, aes.BlockSize) + + cbc.CryptBlocks(ciphertext, plaintextP) + f.ciphertext = ciphertext + + return nil +} + +func (f *EncryptedFile) OsReadPlainTextFile(path string) error { + plaintext, err := os.ReadFile(path) + if err != nil { + return err + } + f.plainText = plaintext + + return nil +} + +func (f *EncryptedFile) WriteEncryptFileToDisk(filePath string) error { + packed := f.packFile() + + err := os.WriteFile(filePath+".enc", packed, 0600) + if err != nil { + return err + } + + return nil +} + +func (f *EncryptedFile) WriteDecryptedFileToDisk(filePath string) error { + err := os.WriteFile(filePath, f.plainText, 0600) + if err != nil { + return err + } + + return nil +} + +func (f *EncryptedFile) unpackFileAndDecrypt(packedFile []byte) error { + keyLen := f.privateKey.Size() + + lenWithoutKey := len(packedFile) - keyLen + + packedFile, f.symmetricKeyEnc = packedFile[0:lenWithoutKey], packedFile[lenWithoutKey:] + + err := f.decryptSymmetricKey() + if err != nil { + return err + } + + a, err := aes.NewCipher(f.symmetricKey) + if err != nil { + return err + } + f.nonce, f.ciphertext = packedFile[0:aes.BlockSize], packedFile[aes.BlockSize:] + + cbc := cipher.NewCBCDecrypter(a, f.nonce) + + plainText := make([]byte, len(f.ciphertext)) + + cbc.CryptBlocks(plainText, f.ciphertext) + + f.plainText, err = unPad(plainText) + if err != nil { + return err + } + + return nil +} + +func (f *EncryptedFile) OsReadCipherTextFile(path string) error { + packedFile, err := os.ReadFile(path) + if err != nil { + return err + } + + err = f.unpackFileAndDecrypt(packedFile) + if err != nil { + return err + } + + return nil +} + +func pad(buf []byte, size int) []byte { + if size < 1 || size > 255 { + panic(fmt.Sprintf("pkcs7pad: inappropriate block size %d", size)) + } + i := size - (len(buf) % size) + return append(buf, bytes.Repeat([]byte{byte(i)}, i)...) +} + +func unPad(buf []byte) ([]byte, error) { + if len(buf) == 0 { + return nil, fmt.Errorf("pkcs7pad: bad padding") + } + + padLen := buf[len(buf)-1] + toCheck := 255 + good := 1 + if toCheck > len(buf) { + toCheck = len(buf) + } + for i := 0; i < toCheck; i++ { + b := buf[len(buf)-1-i] + + outOfRange := subtle.ConstantTimeLessOrEq(int(padLen), i) + equal := subtle.ConstantTimeByteEq(padLen, b) + good &= subtle.ConstantTimeSelect(outOfRange, 1, equal) + } + + good &= subtle.ConstantTimeLessOrEq(1, int(padLen)) + good &= subtle.ConstantTimeLessOrEq(int(padLen), len(buf)) + + if good != 1 { + return nil, fmt.Errorf("pkcs7pad: bad padding") + } + + return buf[:len(buf)-int(padLen)], nil +} diff --git a/crypt/keys.go b/crypt/keys.go new file mode 100644 index 0000000..c424c4c --- /dev/null +++ b/crypt/keys.go @@ -0,0 +1,130 @@ +package crypt + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/sha512" + "crypto/x509" + "encoding/pem" + "os" +) + +func (f *EncryptedFile) encryptSymmetricKey() error { + hash := sha512.New() + ciphertext, err := rsa.EncryptOAEP(hash, rand.Reader, f.PublicKey, f.symmetricKey, nil) + if err != nil { + return err + } + + f.symmetricKeyEnc = ciphertext + + return nil +} + +func (f *EncryptedFile) decryptSymmetricKey() error { + hash := sha512.New() + plaintext, err := rsa.DecryptOAEP(hash, rand.Reader, f.privateKey, f.symmetricKeyEnc, nil) + if err != nil { + return err + } + + f.symmetricKey = plaintext + + return nil +} + +func (f *EncryptedFile) OsReadPublicKey(path string) error { + pemKey, err := os.ReadFile(path) + if err != nil { + return err + } + + f.PublicPem = pemKey + err = f.ParsePublicPem() + if err != nil { + return err + } + + return nil +} + +func (f *EncryptedFile) OsReadPrivateKey(path string) error { + pemKey, err := os.ReadFile(path) + if err != nil { + return err + } + + f.privatePem = pemKey + + err = f.ParsePrivatePem() + if err != nil { + return err + } + + return nil +} + +func (f *EncryptedFile) GenerateSymmetricKey() error { + symKey := make([]byte, 32) + _, err := rand.Read(symKey) + if err != nil { + return err + } + + f.symmetricKey = symKey + + err = f.encryptSymmetricKey() + if err != nil { + return err + } + + return nil +} + +func (f *EncryptedFile) ParsePublicPem() error { + pemKeyBin, _ := pem.Decode(f.PublicPem) + + if bytes.Contains(f.PublicPem, []byte("-----BEGIN PUBLIC KEY-----")) { + key, err := x509.ParsePKIXPublicKey(pemKeyBin.Bytes) + if err != nil { + return err + } + + f.PublicKey = key.(*rsa.PublicKey) + return nil + } + + pubKey, err := x509.ParsePKCS1PublicKey(pemKeyBin.Bytes) + + if err != nil { + return err + } + + f.PublicKey = pubKey + + return nil +} + +func (f *EncryptedFile) ParsePrivatePem() error { + pemKeyBin, _ := pem.Decode(f.privatePem) + + if bytes.Contains(f.privatePem, []byte("-----BEGIN PRIVATE KEY-----")) { + key, err := x509.ParsePKCS8PrivateKey(pemKeyBin.Bytes) + if err != nil { + return err + } + + f.privateKey = key.(*rsa.PrivateKey) + return nil + } + + privKey, err := x509.ParsePKCS1PrivateKey(pemKeyBin.Bytes) + if err != nil { + return err + } + + f.privateKey = privKey + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4560680 --- /dev/null +++ b/go.mod @@ -0,0 +1,28 @@ +module github.com/siteworxpro/rsa-file-encryption + +go 1.22.1 + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/bubbles v0.18.0 // indirect + github.com/charmbracelet/bubbletea v0.25.0 // indirect + github.com/charmbracelet/lipgloss v0.10.0 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/urfave/cli/v2 v2.27.1 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.12.0 // indirect + golang.org/x/term v0.6.0 // indirect + golang.org/x/text v0.3.8 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6f562c7 --- /dev/null +++ b/go.sum @@ -0,0 +1,49 @@ +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= +github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= +github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= +github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= +github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= +github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= +github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= diff --git a/main.go b/main.go new file mode 100644 index 0000000..21532e0 --- /dev/null +++ b/main.go @@ -0,0 +1,111 @@ +package main + +import ( + "github.com/siteworxpro/rsa-file-encryption/commands" + "github.com/siteworxpro/rsa-file-encryption/printer" + "github.com/urfave/cli/v2" + "os" +) + +func main() { + p := printer.NewPrinter() + p.PrintTitle() + + app := &cli.App{ + Name: "rsa-file-encryption", + Usage: "a file encryption tool using rsa key pairs to encrypt files using AES-256-GCM", + Commands: []*cli.Command{ + { + Name: "encrypt", + Aliases: []string{"e", "en"}, + Usage: "encrypt a file", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "file", + Aliases: []string{"f"}, + Usage: "file to encrypt", + Required: true, + }, + &cli.StringFlag{ + Name: "public-key", + Aliases: []string{"p"}, + Usage: "public key path", + Required: true, + }, + &cli.BoolFlag{ + Name: "force", + Aliases: []string{"F"}, + Usage: "overwrite the encrypted file", + }, + }, + Action: func(c *cli.Context) error { + return commands.Encrypt(c.String("public-key"), c.String("file"), c.Bool("force")) + }, + }, + { + Name: "decrypt", + Aliases: []string{"d", "de"}, + Usage: "decrypt a file", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "file", + Aliases: []string{"f"}, + Usage: "file to decrypt", + Required: true, + }, + &cli.StringFlag{ + Name: "private-key", + Aliases: []string{"p"}, + Usage: "private key path", + Required: true, + }, + &cli.StringFlag{ + Name: "out", + Aliases: []string{"o"}, + Usage: "output file name", + Required: true, + }, + &cli.BoolFlag{ + Name: "force", + Aliases: []string{"F"}, + Usage: "overwrite the encrypted file", + }, + }, + Action: func(c *cli.Context) error { + return commands.Decrypt(c.String("private-key"), c.String("file"), c.String("out"), c.Bool("force")) + }, + }, + { + Name: "generate-keypair", + Aliases: []string{"g", "gk"}, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "size", + Aliases: []string{"s"}, + Usage: "the size of the private key", + DefaultText: "4096", + }, + &cli.StringFlag{ + Name: "file", + Aliases: []string{"f"}, + Usage: "the path to the private key file", + Required: true, + }, + &cli.BoolFlag{ + Name: "force", + Aliases: []string{"F"}, + Usage: "overwrite the private key file", + }, + }, + Usage: "generate a keypair", + Action: func(c *cli.Context) error { + return commands.GenerateKeypair(c.Uint("size"), c.String("file"), c.Bool("force")) + }, + }, + }, + } + + if err := app.Run(os.Args); err != nil { + p.LogError(err.Error()) + } +} diff --git a/printer/log.go b/printer/log.go new file mode 100644 index 0000000..28dfe4b --- /dev/null +++ b/printer/log.go @@ -0,0 +1,91 @@ +package printer + +import ( + "fmt" + "github.com/charmbracelet/bubbles/spinner" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "time" +) + +func (p *Printer) LogSuccess(message string) { + fmt.Println(p.getSuccess().Render("✅ " + message)) +} + +func (p *Printer) LogInfo(message string) { + fmt.Println(p.getInfo().Render("â„šī¸ " + message)) +} + +func (p *Printer) LogError(message string) { + fmt.Println(p.getError().Render("❌ " + message)) +} + +type model struct { + spinner spinner.Model + quitting bool + err error + message string +} + +func (*Printer) LogSpinner(message string, done chan bool) { + p := tea.NewProgram(initialModel(message)) + + go p.Run() + + for { + select { + case <-done: + p.Kill() + } + + time.Sleep(100 * time.Millisecond) + } +} + +func initialModel(message string) model { + s := spinner.New() + s.Spinner = spinner.MiniDot + s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")).PaddingTop(1).PaddingLeft(2) + + return model{spinner: s, message: message} +} + +func (m model) Init() tea.Cmd { + return m.spinner.Tick +} + +type errMsg error + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c": + m.quitting = true + return m, tea.Quit + default: + return m, nil + } + + case errMsg: + m.err = msg + return m, nil + + default: + var cmd tea.Cmd + m.spinner, cmd = m.spinner.Update(msg) + return m, cmd + } +} + +func (m model) View() string { + if m.err != nil { + return m.err.Error() + } + str := fmt.Sprintf(" %s %s\n\n", m.spinner.View(), m.message) + if m.quitting { + return str + "\n" + } + return str +} diff --git a/printer/printer.go b/printer/printer.go new file mode 100644 index 0000000..f918591 --- /dev/null +++ b/printer/printer.go @@ -0,0 +1,19 @@ +package printer + +import ( + "fmt" +) + +var Version = "0.0.0" + +type Printer struct { +} + +func NewPrinter() *Printer { + return &Printer{} +} + +func (p *Printer) PrintTitle() { + fmt.Println(p.getBright().Render(fmt.Sprintf("RSA File Encryption Tool %s", Version))) + fmt.Println() +} diff --git a/printer/styles.go b/printer/styles.go new file mode 100644 index 0000000..ef1541d --- /dev/null +++ b/printer/styles.go @@ -0,0 +1,42 @@ +package printer + +import ( + "github.com/charmbracelet/lipgloss" +) + +func (*Printer) getBright() lipgloss.Style { + return lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#EDEEEDFF")). + Background(lipgloss.Color("#424E46FF")). + MarginTop(1). + PaddingLeft(3). + PaddingRight(3) +} + +func (*Printer) getSuccess() lipgloss.Style { + return lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#00FF00")). + MarginTop(1). + MarginBottom(2). + PaddingLeft(2). + Width(120) +} + +func (*Printer) getError() lipgloss.Style { + return lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#FF0000")). + PaddingLeft(2). + MarginTop(1). + MarginBottom(2). + Width(120) +} + +func (*Printer) getInfo() lipgloss.Style { + return lipgloss.NewStyle(). + Foreground(lipgloss.Color("#4E82B7FF")). + PaddingLeft(2). + Width(120) +}