diff --git a/cmd/nonshy/main.go b/cmd/nonshy/main.go index fd6d50c..21f99b1 100644 --- a/cmd/nonshy/main.go +++ b/cmd/nonshy/main.go @@ -204,8 +204,6 @@ func main() { }, }, Action: func(c *cli.Context) error { - initdb(c) - err := coldstorage.FileFromColdStorage( c.String("key"), c.String("aes"), @@ -213,7 +211,7 @@ func main() { c.String("output"), ) if err != nil { - log.Error("Error decrypting from cold storage: %s") + log.Error("Error decrypting from cold storage: %s", err) return err } diff --git a/docs/Cold Storage.md b/docs/Cold Storage.md new file mode 100644 index 0000000..e59e561 --- /dev/null +++ b/docs/Cold Storage.md @@ -0,0 +1,70 @@ +# Cold Storage + +One of the security features of the website is **cold storage** which implements a "one way" encryption process for archiving sensitive files on the site. + +The first use case is to archive secondary photo IDs: if a user was requested to provide a scan of their government issued photo ID for approval, the site can archive the original copy to cold storage when approved in case of any future inquiry. + +The cold storage feature works by encrypting the file using an RSA public key, and relies on the matching private key to be **removed** from the web server and kept offline; so in case of a hack or data breach, the key that can decrypt the cold storage files will **NOT** be kept on the same web server. + +This document explains how this feature works and how to configure it. + +## Initialization + +When the server starts up and there is not a cold storage RSA key configured, the feature will be initialized by generating new RSA encryption keys: + +* The directory `./coldstorage/keys` is created and the RSA keys will be written in files named **private.pem** and **public.pem**. +* The RSA public key is _also_ written into the **settings.json** file for the server, at the Encryption / ColdStorageRSAPublicKey property. + +You should **move the keys OFF of your web server machine** and keep them safe for your bookkeeping. Notably, the `private.pem` key is the sensitive file that should be removed. + +The app does not need either of these keys to remain on the server: the settings.json has a copy of the RSA public key which the app uses to create cold storage encrypted files. + +### Admin Dashboard Warning + +As a safety precaution: if the private.pem key remains on disk, a warning is shown at the top of the Admin Dashboard page of the website to remind you that the key should be removed and stored safely offline. + +## Recovering from Cold Storage + +Should you need to recover an encrypted file from cold storage, the `nonshy coldstorage decrypt` command built into the Go server binary has the function to decrypt the files. + +Every item that is moved into cold storage generates two files: an encrypted AES key file (`.aes`) and the encrypted data file itself (with a `.enc` extension). For example, a "photo.jpg" might go into cold storage as two files: "photo.jpg.aes" and "photo.jpg.enc" + +You will need the following three files to decrypt from cold storage: + +1. The RSA private key file (private.pem) +2. The encrypted AES key file (.aes extension) +3. The encrypted cold storage data file (.enc extension) + +The command to decrypt them is thus like: + +```bash +# command example +nonshy coldstorage decrypt \ + --key private.pem \ + --aes photo.jpg.aes \ + --input photo.jpg.enc \ + --output photo.jpg + +# short command line flags work too +nonshy coldstorage decrypt -k private.pem \ + -a photo.jpg.aes -i photo.jpg.enc \ + -o photo.jpg +``` + +The `--output` file is where the decrypted file will be written to. + +## Encryption Algorithms + +When a file is moved into cold storage: + +1. A fresh new AES symmetric key is generated from scratch. +2. The AES key is encrypted using the **RSA public key** and written to the ".aes" file in the coldstorage/ folder. +3. The original file is encrypted using that AES symmetric key and written to the ".enc" file in the coldstorage/ folder. + +At the end of the encrypt function: the web server no longer has the AES key and is _unable_ to decrypt it because the private key is not available (as it should be kept offline for security). + +Decrypting a file out of cold storage is done like so: + +1. The encrypted AES key is unlocked using the **RSA private key**. +2. The encrypted cold storage file (.enc) is decrypted with that AES key. +3. The cleartext data is written to the output file. \ No newline at end of file diff --git a/pkg/config/variable.go b/pkg/config/variable.go index 803ee4b..576c8cd 100644 --- a/pkg/config/variable.go +++ b/pkg/config/variable.go @@ -98,7 +98,7 @@ func LoadSettings() { writeSettings = true } - // Initialize the cold storage ECDSA keys. + // Initialize the cold storage RSA keys. if len(Current.Encryption.ColdStorageRSAPublicKey) == 0 { x509publicKey, err := coldstorage.Initialize() if err != nil { diff --git a/pkg/controller/photo/certification.go b/pkg/controller/photo/certification.go index 3724590..ea9dad4 100644 --- a/pkg/controller/photo/certification.go +++ b/pkg/controller/photo/certification.go @@ -472,10 +472,10 @@ func AdminCertification() http.HandlerFunc { if cert.SecondaryFilename != "" { // Move the original photo into cold storage. coldStorageFilename := fmt.Sprintf( - "photoID-%d-%s-%s.jpg", + "photoID-%d-%s-%d.jpg", user.ID, user.Username, - time.Now().Format(time.RFC3339Nano), + time.Now().Unix(), ) if err := coldstorage.FileToColdStorage( photo.DiskPath(cert.SecondaryFilename), diff --git a/pkg/encryption/keygen/aes.go b/pkg/encryption/keygen/aes.go new file mode 100644 index 0000000..b8aff1c --- /dev/null +++ b/pkg/encryption/keygen/aes.go @@ -0,0 +1,72 @@ +// Package keygen provides the AES key initializer function. +package keygen + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "errors" + "fmt" + "io" +) + +// NewAESKey returns a 32-byte (AES 256 bit) encryption key. +func NewAESKey() ([]byte, error) { + var result = make([]byte, 32) + _, err := rand.Read(result) + return result, err +} + +// EncryptWithAESKey a byte stream using a given AES key. +func EncryptWithAESKey(input []byte, key []byte) ([]byte, error) { + // Generate a new AES cipher. + c, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + // gcm or Galois/Counter Mode + gcm, err := cipher.NewGCM(c) + if err != nil { + return nil, err + } + + // Create a new byte array the size of the GCM nonce + // which must be passed to Seal. + nonce := make([]byte, gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, fmt.Errorf("populating the nonce: %s", err) + } + + // Encrypt the text using the Seal function. + // Seal encrypts and authenticates plaintext, authenticates the + // additional data and appends the result to dst, returning the + // updated slice. The nonce must be NonceSize() bytes long and + // unique for all time, for a given key. + result := gcm.Seal(nonce, nonce, input, nil) + return result, nil +} + +func DecryptWithAESKey(data []byte, key []byte) ([]byte, error) { + c, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(c) + if err != nil { + return nil, err + } + + nonceSize := gcm.NonceSize() + if len(data) < nonceSize { + return nil, errors.New("ciphertext data less than nonceSize") + } + + nonce, ciphertext := data[:nonceSize], data[nonceSize:] + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, err + } + return plaintext, nil +} diff --git a/pkg/encryption/keygen/aes_test.go b/pkg/encryption/keygen/aes_test.go new file mode 100644 index 0000000..51861ea --- /dev/null +++ b/pkg/encryption/keygen/aes_test.go @@ -0,0 +1,72 @@ +package keygen_test + +import ( + "testing" + + "code.nonshy.com/nonshy/website/pkg/encryption/keygen" +) + +func TestAES(t *testing.T) { + type testCase struct { + AESKey []byte // AES key, nil = generate a new one + Input []byte // input text to encrypt + Encrypted []byte // already encrypted text + Expect []byte // expected output on decrypt + } + + var tests = []testCase{ + { + Input: []byte("hello world"), + Expect: []byte("hello world"), + }, + { + AESKey: []byte{170, 94, 243, 132, 85, 247, 149, 238, 245, 39, 140, 125, 226, 178, 134, 161, 17, 151, 139, 248, 16, 94, 165, 8, 102, 238, 214, 183, 86, 138, 219, 52}, + Encrypted: []byte{146, 217, 250, 254, 70, 201, 27, 221, 92, 145, 77, 213, 211, 197, 63, 189, 220, 188, 78, 8, 217, 108, 136, 89, 156, 23, 179, 54, 209, 54, 244, 170, 182, 150, 242, 52, 112, 191, 216, 46}, + Expect: []byte("goodbye mars"), + }, + } + + for i, test := range tests { + if len(test.AESKey) == 0 { + key, err := keygen.NewAESKey() + if err != nil { + t.Errorf("Test #%d: failed to generate new AES key: %s", i, err) + continue + } + test.AESKey = key + } + + if len(test.Encrypted) == 0 { + enc, err := keygen.EncryptWithAESKey(test.Input, test.AESKey) + if err != nil { + t.Errorf("Test #%d: failed to encrypt input: %s", i, err) + continue + } + test.Encrypted = enc + } + + // t.Errorf("Key: %+v\nEnc: %+v", test.AESKey, test.Encrypted) + + dec, err := keygen.DecryptWithAESKey(test.Encrypted, test.AESKey) + if err != nil { + t.Errorf("Test #%d: failed to decrypt: %s", i, err) + continue + } + + // compare the results + var ok = true + if len(dec) != len(test.Expect) { + ok = false + } else { + for j := range dec { + if test.Expect[j] != dec[j] { + ok = false + } + } + } + if !ok { + t.Errorf("Test #%d: got unexpected result from decrypt. Expected %s, got %s", i, test.Expect, dec) + continue + } + } +} diff --git a/pkg/encryption/keygen/keygen.go b/pkg/encryption/keygen/rsa.go similarity index 61% rename from pkg/encryption/keygen/keygen.go rename to pkg/encryption/keygen/rsa.go index ad001ce..7934b76 100644 --- a/pkg/encryption/keygen/keygen.go +++ b/pkg/encryption/keygen/rsa.go @@ -3,81 +3,15 @@ package keygen import ( "crypto" - "crypto/aes" - "crypto/cipher" "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/pem" - "errors" - "fmt" - "io" "os" "code.nonshy.com/nonshy/website/pkg/log" ) -// NewAESKey returns a 32-byte (AES 256 bit) encryption key. -func NewAESKey() ([]byte, error) { - var result = make([]byte, 32) - _, err := rand.Read(result) - return result, err -} - -// EncryptWithAESKey a byte stream using a given AES key. -func EncryptWithAESKey(input []byte, key []byte) ([]byte, error) { - // Generate a new AES cipher. - c, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - - // gcm or Galois/Counter Mode - gcm, err := cipher.NewGCM(c) - if err != nil { - return nil, err - } - - // Create a new byte array the size of the GCM nonce - // which must be passed to Seal. - nonce := make([]byte, gcm.NonceSize()) - if _, err := io.ReadFull(rand.Reader, nonce); err != nil { - return nil, fmt.Errorf("populating the nonce: %s", err) - } - - // Encrypt the text using the Seal function. - // Seal encrypts and authenticates plaintext, authenticates the - // additional data and appends the result to dst, returning the - // updated slice. The nonce must be NonceSize() bytes long and - // unique for all time, for a given key. - result := gcm.Seal(nonce, nonce, input, nil) - return result, nil -} - -func DecryptWithAESKey(data []byte, key []byte) ([]byte, error) { - c, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - - gcm, err := cipher.NewGCM(c) - if err != nil { - return nil, err - } - - nonceSize := gcm.NonceSize() - if len(data) < nonceSize { - return nil, errors.New("ciphertext data less than nonceSize") - } - - nonce, ciphertext := data[:nonceSize], data[nonceSize:] - plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) - if err != nil { - return nil, err - } - return plaintext, nil -} - // NewRSAKeys will generate an RSA 2048-bit key pair. func NewRSAKeys() (*rsa.PrivateKey, error) { privateKey, err := rsa.GenerateKey(rand.Reader, 2048)