151 lines
4.6 KiB
Go
151 lines
4.6 KiB
Go
|
package coldstorage
|
||
|
|
||
|
import (
|
||
|
"crypto/rand"
|
||
|
"crypto/rsa"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"os"
|
||
|
"path"
|
||
|
"path/filepath"
|
||
|
|
||
|
"code.nonshy.com/nonshy/website/pkg/encryption/keygen"
|
||
|
"code.nonshy.com/nonshy/website/pkg/log"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
ColdStorageDirectory = "./coldstorage"
|
||
|
ColdStorageKeysDirectory = path.Join(ColdStorageDirectory, "keys")
|
||
|
ColdStoragePrivateKeyFile = path.Join(ColdStorageKeysDirectory, "private.pem")
|
||
|
ColdStoragePublicKeyFile = path.Join(ColdStorageKeysDirectory, "public.pem")
|
||
|
)
|
||
|
|
||
|
// Initialize generates the RSA key pairs for the first time and creates
|
||
|
// the cold storage directories. It writes the keys to disk and returns the x509 encoded
|
||
|
// public key which goes in the settings.json (the keys on disk are for your bookkeeping).
|
||
|
func Initialize() ([]byte, error) {
|
||
|
log.Warn("NOTICE: rolling a random RSA key pair for cold storage")
|
||
|
rsaKey, err := keygen.NewRSAKeys()
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("generate RSA key: %s", err)
|
||
|
}
|
||
|
|
||
|
// Encode to x509
|
||
|
x509, err := keygen.SerializePublicKey(rsaKey.Public())
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("encode RSA public key to x509: %s", err)
|
||
|
}
|
||
|
|
||
|
// Write the public/private key files to disk.
|
||
|
if _, err := os.Stat(ColdStorageKeysDirectory); os.IsNotExist(err) {
|
||
|
log.Info("Notice: creating cold storage directory")
|
||
|
if err := os.MkdirAll(ColdStorageKeysDirectory, 0755); err != nil {
|
||
|
return nil, fmt.Errorf("create %s: %s", ColdStorageKeysDirectory, err)
|
||
|
}
|
||
|
}
|
||
|
if err := keygen.WriteRSAKeys(
|
||
|
rsaKey,
|
||
|
path.Join(ColdStorageKeysDirectory, "private.pem"),
|
||
|
path.Join(ColdStorageKeysDirectory, "public.pem"),
|
||
|
); err != nil {
|
||
|
return nil, fmt.Errorf("export newly generated public/private key files: %s", err)
|
||
|
}
|
||
|
|
||
|
return x509, nil
|
||
|
}
|
||
|
|
||
|
// Warning returns an error message if the private key is still on disk at its
|
||
|
// original generated location: it should be moved offline for security.
|
||
|
func Warning() error {
|
||
|
if _, err := os.Stat(ColdStoragePrivateKeyFile); os.IsNotExist(err) {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
return errors.New("the private key file at ./coldstorage/keys should be moved off of the server and kept offline for safety")
|
||
|
}
|
||
|
|
||
|
// FileToColdStorage will copy a file, encrypted, into cold storage at the given filename.
|
||
|
func FileToColdStorage(sourceFilePath, outputFileName string, publicKeyPEM []byte) error {
|
||
|
if len(publicKeyPEM) == 0 {
|
||
|
return errors.New("no RSA public key")
|
||
|
}
|
||
|
|
||
|
// Load the public key from PEM encoding.
|
||
|
publicKey, err := keygen.DeserializePublicKey(publicKeyPEM)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("deserializing public key: %s", err)
|
||
|
}
|
||
|
|
||
|
// Generate a unique AES key for encrypting this file in one direction.
|
||
|
aesKey, err := keygen.NewAESKey()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Encrypt the AES key and store it on disk next to the cold storage file.
|
||
|
ciphertext, err := rsa.EncryptPKCS1v15(rand.Reader, publicKey, aesKey)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("encrypt error: %s", err)
|
||
|
}
|
||
|
err = os.WriteFile(
|
||
|
filepath.Join(ColdStorageDirectory, outputFileName+".aes"),
|
||
|
ciphertext,
|
||
|
0600,
|
||
|
)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Read the original plaintext file going into cold storage.
|
||
|
plaintext, err := os.ReadFile(sourceFilePath)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("source file: %s", err)
|
||
|
}
|
||
|
|
||
|
// Encrypt the plaintext with the AES key.
|
||
|
ciphertext, err = keygen.EncryptWithAESKey(plaintext, aesKey)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Write it to disk.
|
||
|
return os.WriteFile(filepath.Join(ColdStorageDirectory, outputFileName+".enc"), ciphertext, 0600)
|
||
|
}
|
||
|
|
||
|
// FileFromColdStorage decrypts a cold storage file and writes it to the output file.
|
||
|
//
|
||
|
// The command `nonshy coldstorage decrypt` uses this function. Requirements:
|
||
|
//
|
||
|
// - privateKeyFile is the RSA private key originally generated for cold storage
|
||
|
// - aesKeyFile is the unique .aes file for the cold storage item
|
||
|
// - ciphertextFile is the encrypted cold storage item
|
||
|
// - outputFile is where you want to save the result to
|
||
|
func FileFromColdStorage(privateKeyFile, aesKeyFile, ciphertextFile, outputFile string) error {
|
||
|
privateKey, err := keygen.PrivateKeyFromFile(privateKeyFile)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("private key file: %s", err)
|
||
|
}
|
||
|
|
||
|
encryptedAESKey, err := os.ReadFile(aesKeyFile)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("reading aes key file: %s", err)
|
||
|
}
|
||
|
|
||
|
aesKey, err := rsa.DecryptPKCS1v15(rand.Reader, privateKey, encryptedAESKey)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("decrypting the aes key file: %s", err)
|
||
|
}
|
||
|
|
||
|
ciphertext, err := os.ReadFile(ciphertextFile)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("reading cold storage file: %s", err)
|
||
|
}
|
||
|
|
||
|
plaintext, err := keygen.DecryptWithAESKey(ciphertext, aesKey)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("decrypting cold storage file: %s", err)
|
||
|
}
|
||
|
|
||
|
return os.WriteFile(outputFile, plaintext, 0644)
|
||
|
}
|