Add new and better CLI v2

This commit is contained in:
Evan Su 2024-04-25 10:11:57 -04:00 committed by GitHub
parent c235f28fd2
commit d70ee6effc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 576 additions and 0 deletions

12
cli/v2/go.mod Normal file
View file

@ -0,0 +1,12 @@
module github.com/HACKERALERT/Picocrypt/cli/v2/picocrypt
go 1.22.2
require (
github.com/HACKERALERT/infectious v0.0.0-20240424200929-b9ce72346a19
github.com/HACKERALERT/serpent v0.0.0-20210716182301-293b29869c66
golang.org/x/crypto v0.22.0
golang.org/x/term v0.19.0
)
require golang.org/x/sys v0.19.1-0.20240416221847-9a28524796a5 // indirect

10
cli/v2/go.sum Normal file
View file

@ -0,0 +1,10 @@
github.com/HACKERALERT/infectious v0.0.0-20240424200929-b9ce72346a19 h1:C5t561XXXRJvdiluejbka36n+YaOB4XJuQIo+25hL1k=
github.com/HACKERALERT/infectious v0.0.0-20240424200929-b9ce72346a19/go.mod h1:bTnpEk9zNS1sVKg5TRvLkuSEGVqH0+LRfcMurPtcJvY=
github.com/HACKERALERT/serpent v0.0.0-20210716182301-293b29869c66 h1:YDpFq+y6mRcu97rn/rhYg8u8FdeO0wzTuLgM2gVkA+c=
github.com/HACKERALERT/serpent v0.0.0-20210716182301-293b29869c66/go.mod h1:d/+9q3sIxtIyOgHNgFGr3yGBKKVn5h3vL4hV1qlmoLs=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/sys v0.19.1-0.20240416221847-9a28524796a5 h1:0exPaeAtAlmNHCcRJc+hETS3/TcMV+yjoHhlp4+Ff3E=
golang.org/x/sys v0.19.1-0.20240416221847-9a28524796a5/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=

554
cli/v2/main.go Normal file
View file

@ -0,0 +1,554 @@
package main
import (
"archive/zip"
"bytes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/subtle"
"flag"
"fmt"
"hash"
"io"
"math"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/HACKERALERT/infectious"
"github.com/HACKERALERT/serpent"
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/blake2b"
"golang.org/x/crypto/chacha20"
"golang.org/x/crypto/hkdf"
"golang.org/x/crypto/sha3"
"golang.org/x/term"
)
var KiB = 1 << 10
var MiB = 1 << 20
var GiB = 1 << 30
var TiB = 1 << 40
var rs5, _ = infectious.NewFEC(5, 15)
var rs16, _ = infectious.NewFEC(16, 48)
var rs24, _ = infectious.NewFEC(24, 72)
var rs32, _ = infectious.NewFEC(32, 96)
var rs64, _ = infectious.NewFEC(64, 192)
var rs128, _ = infectious.NewFEC(128, 136)
func rsEncode(rs *infectious.FEC, data []byte) []byte {
res := make([]byte, rs.Total())
rs.Encode(data, func(s infectious.Share) {
res[s.Number] = s.Data[0]
})
return res
}
func rsDecode(rs *infectious.FEC, data []byte) ([]byte, error) {
tmp := make([]infectious.Share, rs.Total())
for i := 0; i < rs.Total(); i++ {
tmp[i].Number = i
tmp[i].Data = append(tmp[i].Data, data[i])
}
res, err := rs.Decode(nil, tmp)
if err != nil {
if rs.Total() == 136 {
return data[:128], err
}
return data[:rs.Total()/3], err
}
return res, nil
}
func pad(data []byte) []byte {
padLen := 128 - len(data)%128
padding := bytes.Repeat([]byte{byte(padLen)}, padLen)
return append(data, padding...)
}
func unpad(data []byte) []byte {
padLen := int(data[127])
return data[:128-padLen]
}
func work() int {
flag.Usage = func() {
fmt.Println("Usage: picocrypt [-p]aranoid [-r]eedsolo <item1> [<item2> ...]")
fmt.Println("Items: can be a file (cat.png), folder (./src), or glob (*.txt)")
}
paranoid := flag.Bool("p", false, "")
reedsolo := flag.Bool("r", false, "")
flag.Parse()
mode := ""
if flag.NArg() == 0 {
flag.Usage()
return 0
}
if flag.NArg() == 1 {
if strings.HasSuffix(flag.Arg(0), ".pcv") {
mode = "decrypt"
} else {
mode = "encrypt"
}
} else {
mode = "encrypt"
for _, v := range flag.Args() {
if strings.HasSuffix(v, ".pcv") {
fmt.Println("Multiple items cannot contain volumes.")
return 1
}
}
}
fin_, fout_ := "", ""
if mode == "decrypt" {
fin_ = flag.Arg(0)
fout_ = strings.TrimSuffix(fin_, ".pcv")
} else {
stat, err := os.Stat(flag.Arg(0))
if flag.NArg() == 1 && err == nil && !stat.IsDir() {
fin_ = flag.Arg(0)
fout_ = fin_ + ".pcv"
} else {
items := []string{}
for _, v := range flag.Args() {
if strings.Contains(v, "../") || strings.HasPrefix(v, "/") {
fmt.Println("Cannot encrypt outside of current directory.")
return 1
}
matches, err := filepath.Glob(v)
if err != nil {
fmt.Println("Invalid glob pattern(s).")
return 1
}
items = append(items, matches...)
}
files := []string{}
for _, v := range items {
stat, err := os.Stat(v)
if err != nil {
fmt.Println("Cannot access input(s).")
return 1
}
if !stat.IsDir() {
files = append(files, v)
} else {
filepath.Walk(v, func(path string, _ os.FileInfo, _ error) error {
stat, err := os.Stat(path)
if err == nil && !stat.IsDir() {
files = append(files, path)
}
return nil
})
}
}
if len(files) == 0 {
fmt.Println("Nothing to encrypt.")
return 1
}
dir, err := os.Getwd()
if err != nil {
fmt.Println("Cannot get current working directory.")
return 1
}
file, err := os.CreateTemp("", "")
if err != nil {
fmt.Println("Cannot create temporary file.")
return 1
}
writer := zip.NewWriter(file)
for _, path := range files {
stat, err := os.Stat(path)
if err != nil {
continue
}
header, err := zip.FileInfoHeader(stat)
if err != nil {
continue
}
abs, err := filepath.Abs(path)
if err != nil {
continue
}
abs = filepath.ToSlash(abs)
header.Name = strings.TrimPrefix(abs, filepath.ToSlash(dir))
header.Name = strings.TrimPrefix(header.Name, "/")
header.Method = zip.Deflate
entry, err := writer.CreateHeader(header)
if err != nil {
continue
}
fin_, err := os.Open(path)
if err != nil {
writer.Close()
file.Close()
os.Remove(file.Name())
fmt.Println("Read access to input(s) denied.")
return 1
}
fmt.Println("Compressing:", abs)
_, err = io.Copy(entry, fin_)
fin_.Close()
if err != nil {
writer.Close()
file.Close()
os.Remove(file.Name())
fmt.Println("Insufficient disk space.")
return 1
}
}
writer.Close()
file.Close()
fin_ = file.Name()
fout_ = "encrypted.zip.pcv"
defer os.Remove(file.Name())
}
}
var password []byte
var cpassword []byte
var err error
if mode == "encrypt" {
fmt.Print("Password: ")
password, err = term.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
fmt.Println("\nError reading password.")
return 1
}
fmt.Print("\nConfirm: ")
cpassword, err = term.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
fmt.Println("\nError reading password.")
return 1
}
if !bytes.Equal(password, cpassword) {
fmt.Println("\nPasswords don't match.")
return 1
}
fmt.Println()
} else {
fmt.Print("Password: ")
password, err = term.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
fmt.Println("\nError reading password.")
return 1
}
fmt.Println()
}
var padded bool
var salt []byte // Argon2 salt, 16 bytes
var hkdfSalt []byte // HKDF-SHA3 salt, 32 bytes
var serpentIV []byte // Serpent IV, 16 bytes
var nonce []byte // 24-byte XChaCha20 nonce
var keyHash []byte // SHA3-512 hash of encryption key
var keyHashRef []byte // Same as 'keyHash', but used for comparison
var authTag []byte // 64-byte authentication tag (BLAKE2b or HMAC-SHA3)
fin, err := os.Open(fin_)
if err != nil {
fmt.Println("Error accessing input file.")
return 1
}
_, err = os.Stat(fout_)
if err == nil {
fmt.Println("Output file already exists.")
return 1
}
fout, err := os.Create(fout_)
if err != nil {
fmt.Println("Error creating output file.")
return 1
}
stat, err := os.Stat(fin_)
if err != nil {
fmt.Println("Error accessing input file.")
return 1
}
total := stat.Size()
if mode == "encrypt" {
fmt.Println("Generating volume header...")
errs := make([]error, 11)
salt = make([]byte, 16)
hkdfSalt = make([]byte, 32)
serpentIV = make([]byte, 16)
nonce = make([]byte, 24)
_, errs[0] = fout.Write(rsEncode(rs5, []byte("v1.34")))
commentsLength := []byte(fmt.Sprintf("%05d", 0))
_, errs[1] = fout.Write(rsEncode(rs5, commentsLength))
flags := make([]byte, 5)
if *paranoid {
flags[0] = 1
}
if *reedsolo {
flags[3] = 1
}
if total%int64(MiB) >= int64(MiB)-128 {
flags[4] = 1
}
_, errs[3] = fout.Write(rsEncode(rs5, flags))
rand.Read(salt)
rand.Read(hkdfSalt)
rand.Read(serpentIV)
rand.Read(nonce)
_, errs[4] = fout.Write(rsEncode(rs16, salt))
_, errs[5] = fout.Write(rsEncode(rs32, hkdfSalt))
_, errs[6] = fout.Write(rsEncode(rs16, serpentIV))
_, errs[7] = fout.Write(rsEncode(rs24, nonce))
_, errs[8] = fout.Write(make([]byte, 192))
_, errs[9] = fout.Write(make([]byte, 96))
_, errs[10] = fout.Write(make([]byte, 192))
for _, err := range errs {
if err != nil {
fin.Close()
fout.Close()
os.Remove(fout_)
fmt.Println("Insufficient disk space.")
return 1
}
}
} else {
fmt.Println("Reading volume header...")
errs := make([]error, 10)
version := make([]byte, 15)
fin.Read(version)
_, errs[0] = rsDecode(rs5, version)
tmp := make([]byte, 15)
fin.Read(tmp)
tmp, errs[1] = rsDecode(rs5, tmp)
commentsLength, _ := strconv.Atoi(string(tmp))
fin.Read(make([]byte, commentsLength*3))
total -= int64(commentsLength) * 3
flags := make([]byte, 15)
fin.Read(flags)
flags, errs[2] = rsDecode(rs5, flags)
*paranoid = flags[0] == 1
*reedsolo = flags[3] == 1
padded = flags[4] == 1
if flags[1] == 1 {
fin.Close()
fout.Close()
os.Remove(fout_)
fmt.Println("Keyfiles are not supported.")
return 1
}
salt = make([]byte, 48)
fin.Read(salt)
salt, errs[3] = rsDecode(rs16, salt)
hkdfSalt = make([]byte, 96)
fin.Read(hkdfSalt)
hkdfSalt, errs[4] = rsDecode(rs32, hkdfSalt)
serpentIV = make([]byte, 48)
fin.Read(serpentIV)
serpentIV, errs[5] = rsDecode(rs16, serpentIV)
nonce = make([]byte, 72)
fin.Read(nonce)
nonce, errs[6] = rsDecode(rs24, nonce)
keyHashRef = make([]byte, 192)
fin.Read(keyHashRef)
keyHashRef, errs[7] = rsDecode(rs64, keyHashRef)
keyfileHashRef := make([]byte, 96)
fin.Read(keyfileHashRef)
_, errs[8] = rsDecode(rs32, keyfileHashRef)
authTag = make([]byte, 192)
fin.Read(authTag)
authTag, errs[9] = rsDecode(rs64, authTag)
for _, err := range errs {
if err != nil {
fin.Close()
fout.Close()
os.Remove(fout_)
fmt.Println("The volume header is damaged.")
return 1
}
}
}
var key []byte
if *paranoid {
key = argon2.IDKey(
password,
salt,
8,
1<<20,
8,
32,
)
} else {
key = argon2.IDKey(
password,
salt,
4,
1<<20,
4,
32,
)
}
tmp := sha3.New512()
tmp.Write(key)
keyHash = tmp.Sum(nil)
if mode == "decrypt" {
if subtle.ConstantTimeCompare(keyHash, keyHashRef) != 1 {
fin.Close()
fout.Close()
os.Remove(fout_)
fmt.Println("Incorrect password.")
return 1
}
}
done, counter := 0, 0
chacha, _ := chacha20.NewUnauthenticatedCipher(key, nonce)
var mac hash.Hash
subkey := make([]byte, 32)
hkdf := hkdf.New(sha3.New256, key, hkdfSalt, nil)
hkdf.Read(subkey)
if *paranoid {
mac = hmac.New(sha3.New512, subkey)
} else {
mac, _ = blake2b.New512(subkey)
}
serpentKey := make([]byte, 32)
hkdf.Read(serpentKey)
s, _ := serpent.NewCipher(serpentKey)
serpent := cipher.NewCTR(s, serpentIV)
for {
var src []byte
if mode == "decrypt" && *reedsolo {
src = make([]byte, MiB/128*136)
} else {
src = make([]byte, MiB)
}
size, err := fin.Read(src)
if err != nil {
break
}
src = src[:size]
dst := make([]byte, len(src))
if mode == "encrypt" {
if *paranoid {
serpent.XORKeyStream(dst, src)
copy(src, dst)
}
chacha.XORKeyStream(dst, src)
mac.Write(dst)
if *reedsolo {
copy(src, dst)
dst = nil
if len(src) == MiB {
for i := 0; i < MiB; i += 128 {
dst = append(dst, rsEncode(rs128, src[i:i+128])...)
}
} else {
chunks := math.Floor(float64(len(src)) / 128)
for i := 0; float64(i) < chunks; i++ {
dst = append(dst, rsEncode(rs128, src[i*128:(i+1)*128])...)
}
dst = append(dst, rsEncode(rs128, pad(src[int(chunks*128):]))...)
}
}
} else {
if *reedsolo {
copy(dst, src)
src = nil
if len(dst) == MiB/128*136 {
for i := 0; i < MiB/128*136; i += 136 {
tmp, err := rsDecode(rs128, dst[i:i+136])
if err != nil {
fin.Close()
fout.Close()
os.Remove(fout_)
fmt.Println("The input file is irrecoverably damaged.")
return 1
}
if i == MiB/128*136-136 && done+MiB/128*136 >= int(total) && padded {
tmp = unpad(tmp)
}
src = append(src, tmp...)
}
} else {
chunks := len(dst)/136 - 1
for i := 0; i < chunks; i++ {
tmp, err := rsDecode(rs128, dst[i*136:(i+1)*136])
if err != nil {
fin.Close()
fout.Close()
os.Remove(fout_)
fmt.Println("The input file is irrecoverably damaged.")
return 1
}
src = append(src, tmp...)
}
tmp, err := rsDecode(rs128, dst[int(chunks)*136:])
if err != nil {
fin.Close()
fout.Close()
os.Remove(fout_)
fmt.Println("The input file is irrecoverably damaged.")
return 1
}
src = append(src, unpad(tmp)...)
}
dst = make([]byte, len(src))
}
mac.Write(src)
chacha.XORKeyStream(dst, src)
if *paranoid {
copy(src, dst)
serpent.XORKeyStream(dst, src)
}
}
_, err = fout.Write(dst)
if err != nil {
fin.Close()
fout.Close()
os.Remove(fout_)
fmt.Println("Insufficient disk space.")
return 1
}
if counter >= 60*GiB {
nonce = make([]byte, 24)
hkdf.Read(nonce)
chacha, _ = chacha20.NewUnauthenticatedCipher(key, nonce)
serpentIV = make([]byte, 16)
hkdf.Read(serpentIV)
serpent = cipher.NewCTR(s, serpentIV)
counter = 0
}
}
if mode == "encrypt" {
fout.Seek(int64(309+0*3), 0)
fout.Write(rsEncode(rs64, keyHash))
fout.Write(rsEncode(rs32, make([]byte, 32)))
fout.Write(rsEncode(rs64, mac.Sum(nil)))
} else {
if subtle.ConstantTimeCompare(mac.Sum(nil), authTag) != 1 {
fin.Close()
fout.Close()
os.Remove(fout_)
fmt.Println("The input volume is damaged or modified.")
return 1
}
}
fin.Close()
fout.Close()
fmt.Println("Completed.")
return 0
}
func main() {
os.Exit(work())
}