[auth] Integrated decryption in CLI, removing standalone tool (#691)

## Description

## Tests
This commit is contained in:
Neeraj Gupta 2024-03-06 10:49:51 +05:30 committed by GitHub
parent f02b99b40e
commit 7dc5ccb154
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 3 additions and 706 deletions

View file

@ -1,82 +0,0 @@
package main
import (
"encoding/base64"
"errors"
"fmt"
"golang.org/x/crypto/argon2"
)
// deriveArgonKey generates a 32-bit cryptographic key using the Argon2id algorithm.
// Parameters:
// - password: The plaintext password to be hashed.
// - salt: The salt as a base64 encoded string.
// - memLimit: The memory limit in bytes.
// - opsLimit: The number of iterations.
//
// Returns:
// - A byte slice representing the derived key.
// - An error object, which is nil if no error occurs.
func deriveArgonKey(password, salt string, memLimit, opsLimit int) ([]byte, error) {
if memLimit < 1024 || opsLimit < 1 {
return nil, fmt.Errorf("invalid memory or operation limits")
}
// Decode salt from base64
saltBytes, err := base64.StdEncoding.DecodeString(salt)
if err != nil {
return nil, fmt.Errorf("invalid salt: %v", err)
}
// Generate key using Argon2id
// Note: We're assuming a fixed key length of 32 bytes and changing the threads
key := argon2.IDKey([]byte(password), saltBytes, uint32(opsLimit), uint32(memLimit/1024), 1, 32)
return key, nil
}
// decryptChaCha20poly1305 decrypts the given data using the ChaCha20-Poly1305 algorithm.
// Parameters:
// - data: The encrypted data as a byte slice.
// - key: The key for decryption as a byte slice.
// - nonce: The nonce for decryption as a byte slice.
//
// Returns:
// - A byte slice representing the decrypted data.
// - An error object, which is nil if no error occurs.
// func decryptChaCha20poly13052(data []byte, key []byte, nonce []byte) ([]byte, error) {
// reader := bytes.NewReader(data)
// header := sodium.SecretStreamXCPHeader{Bytes: nonce}
// decoder, err := sodium.MakeSecretStreamXCPDecoder(
// sodium.SecretStreamXCPKey{Bytes: key},
// reader,
// header)
// if err != nil {
// log.Println("Failed to make secret stream decoder", err)
// return nil, err
// }
// // Buffer to store the decrypted data
// decryptedData := make([]byte, len(data))
// n, err := decoder.Read(decryptedData)
// if err != nil && err != io.EOF {
// log.Println("Failed to read from decoder", err)
// return nil, err
// }
// return decryptedData[:n], nil
// }
func decryptChaCha20poly13052(data []byte, key []byte, nonce []byte) ([]byte, error) {
decryptor, err := NewDecryptor(key, nonce)
if err != nil {
return nil, err
}
decoded, tag, err := decryptor.Pull(data)
if tag != TagFinal {
return nil, errors.New("invalid tag")
}
if err != nil {
return nil, err
}
return decoded, nil
}

View file

@ -1,53 +0,0 @@
package main
import (
"encoding/base64"
"testing"
)
const (
password = "test_password"
kdfSalt = "vd0dcYMGNLKn/gpT6uTFTw=="
memLimit = 64 * 1024 * 1024 // 64MB
opsLimit = 2
cipherText = "kBXQ2PuX6y/aje5r22H0AehRPh6sQ0ULoeAO"
cipherNonce = "v7wsI+BFZsRMIjDm3rTxPhmi/CaUdkdJ"
expectedPlainText = "plain_text"
expectedDerivedKey = "vp8d8Nee0BbIML4ab8Cp34uYnyrN77cRwTl920flyT0="
)
func TestDeriveArgonKey(t *testing.T) {
derivedKey, err := deriveArgonKey(password, kdfSalt, memLimit, opsLimit)
if err != nil {
t.Fatalf("Failed to derive key: %v", err)
}
if base64.StdEncoding.EncodeToString(derivedKey) != expectedDerivedKey {
t.Fatalf("Derived key does not match expected key")
}
}
func TestDecryptChaCha20poly1305(t *testing.T) {
derivedKey, err := deriveArgonKey(password, kdfSalt, memLimit, opsLimit)
if err != nil {
t.Fatalf("Failed to derive key: %v", err)
}
decodedCipherText, err := base64.StdEncoding.DecodeString(cipherText)
if err != nil {
t.Fatalf("Failed to decode cipher text: %v", err)
}
decodedCipherNonce, err := base64.StdEncoding.DecodeString(cipherNonce)
if err != nil {
t.Fatalf("Failed to decode cipher nonce: %v", err)
}
decryptedText, err := decryptChaCha20poly13052(decodedCipherText, derivedKey, decodedCipherNonce)
if err != nil {
t.Fatalf("Failed to decrypt: %v", err)
}
if string(decryptedText) != expectedPlainText {
t.Fatalf("Decrypted text : %s does not match the expected text: %s", string(decryptedText), expectedPlainText)
}
}

View file

@ -1,105 +0,0 @@
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
"os"
)
type Export struct {
Version int `json:"version"`
KDFParams KDF `json:"kdfParams"`
EncryptedData string `json:"encryptedData"`
EncryptionNonce string `json:"encryptionNonce"`
}
type KDF struct {
MemLimit int `json:"memLimit"`
OpsLimit int `json:"opsLimit"`
Salt string `json:"salt"`
}
func resolvePath(path string) (string, error) {
if path[:2] != "~/" {
return path, nil
}
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return home + path[1:], nil
}
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("Error:", err)
}
}()
if len(os.Args) != 4 {
fmt.Println("Usage: ./decrypt <export_file> <password> <output_file>")
return
}
exportFile, err := resolvePath(os.Args[1])
if err != nil {
fmt.Println("Error resolving exportFile path:", err)
return
}
password := os.Args[2]
outputFile, err := resolvePath(os.Args[3])
if err != nil {
fmt.Println("Error resolving outputFile path:", err)
return
}
data, err := os.ReadFile(exportFile)
if err != nil {
fmt.Println("Error reading file:", err)
return
}
var export Export
if err := json.Unmarshal(data, &export); err != nil {
fmt.Println("Error parsing JSON:", err)
return
}
if export.Version != 1 {
fmt.Println("Unsupported version")
return
}
encryptedData, err := base64.StdEncoding.DecodeString(export.EncryptedData)
if err != nil {
fmt.Println("Error decoding encrypted data:", err)
return
}
nonce, err := base64.StdEncoding.DecodeString(export.EncryptionNonce)
if err != nil {
fmt.Println("Error decoding nonce:", err)
return
}
key, err := deriveArgonKey(password, export.KDFParams.Salt, export.KDFParams.MemLimit, export.KDFParams.OpsLimit)
if err != nil {
fmt.Println("Error deriving key:", err)
return
}
decryptedData, err := decryptChaCha20poly13052(encryptedData, key, nonce)
if err != nil {
fmt.Println("Error decrypting data:", err)
return
}
if err := os.WriteFile(outputFile, decryptedData, 0644); err != nil {
fmt.Println("Error writing decrypted data to file:", err)
return
}
fmt.Printf("Decrypted data written to %s\n", outputFile)
}

View file

@ -1,7 +0,0 @@
module decrypt
go 1.20
require golang.org/x/crypto v0.11.0
require golang.org/x/sys v0.10.0 // indirect

View file

@ -1,4 +0,0 @@
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View file

@ -1,43 +0,0 @@
#!/bin/bash
# Create a "bin" directory if it doesn't exist
mkdir -p bin
# List of target operating systems
OS_TARGETS=("windows" "linux" "darwin")
# Corresponding architectures for each OS
ARCH_TARGETS=("386 amd64" "386 amd64 arm arm64" "amd64 arm64")
# Loop through each OS target
for index in "${!OS_TARGETS[@]}"
do
OS=${OS_TARGETS[$index]}
for ARCH in ${ARCH_TARGETS[$index]}
do
# Set the GOOS environment variable for the current target OS
export GOOS="$OS"
export GOARCH="$ARCH"
# Set the output binary name to "ente-decrypt" for the current OS and architecture
BINARY_NAME="ente-decrypt-$OS-$ARCH"
# Add .exe extension for Windows
if [ "$OS" == "windows" ]; then
BINARY_NAME="ente-decrypt-$OS-$ARCH.exe"
fi
# Build the binary and place it in the "bin" directory
go build -o "bin/$BINARY_NAME" decrypt.go crypt.go stream.go
# Print a message indicating the build is complete for the current OS and architecture
echo "Built for $OS ($ARCH) as bin/$BINARY_NAME"
done
done
# Clean up any environment variables
unset GOOS
unset GOARCH
# Print a message indicating the build process is complete
echo "Build process completed for all platforms and architectures. Binaries are in the 'bin' directory."

View file

@ -1,409 +0,0 @@
package main
import (
"bytes"
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"golang.org/x/crypto/chacha20"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/poly1305"
)
// public constants
const (
//TagMessage the most common tag, that doesn't add any information about the nature of the message.
TagMessage = 0
// TagPush indicates that the message marks the end of a set of messages,
// but not the end of the stream. For example, a huge JSON string sent as multiple chunks can use this tag to indicate to the application that the string is complete and that it can be decoded. But the stream itself is not closed, and more data may follow.
TagPush = 0x01
// TagRekey "forget" the key used to encrypt this message and the previous ones, and derive a new secret key.
TagRekey = 0x02
// TagFinal indicates that the message marks the end of the stream, and erases the secret key used to encrypt the previous sequence.
TagFinal = TagPush | TagRekey
StreamKeyBytes = chacha20poly1305.KeySize
StreamHeaderBytes = chacha20poly1305.NonceSizeX
// XChaCha20Poly1305IetfABYTES links to crypto_secretstream_xchacha20poly1305_ABYTES
XChaCha20Poly1305IetfABYTES = 16 + 1
)
const cryptoCoreHchacha20InputBytes = 16
/* const crypto_secretstream_xchacha20poly1305_INONCEBYTES = 8 */
const cryptoSecretStreamXchacha20poly1305Counterbytes = 4
var pad0 [16]byte
var invalidKey = errors.New("invalid key")
var invalidInput = errors.New("invalid input")
var cryptoFailure = errors.New("crypto failed")
func memZero(b []byte) {
for i := range b {
b[i] = 0
}
}
func xorBuf(out, in []byte) {
for i := range out {
out[i] ^= in[i]
}
}
func bufInc(n []byte) {
c := 1
for i := range n {
c += int(n[i])
n[i] = byte(c)
c >>= 8
}
}
// crypto_secretstream_xchacha20poly1305_state
type streamState struct {
k [StreamKeyBytes]byte
nonce [chacha20poly1305.NonceSize]byte
pad [8]byte
}
func (s *streamState) reset() {
for i := range s.nonce {
s.nonce[i] = 0
}
s.nonce[0] = 1
}
type Encryptor interface {
Push(m []byte, tag byte) ([]byte, error)
}
type Decryptor interface {
Pull(m []byte) ([]byte, byte, error)
}
type encryptor struct {
streamState
}
type decryptor struct {
streamState
}
func NewStreamKey() []byte {
k := make([]byte, chacha20poly1305.KeySize)
_, _ = rand.Read(k)
return k
}
func NewEncryptor(key []byte) (Encryptor, []byte, error) {
if len(key) != StreamKeyBytes {
return nil, nil, invalidKey
}
header := make([]byte, StreamHeaderBytes)
_, _ = rand.Read(header)
stream := &encryptor{}
k, err := chacha20.HChaCha20(key[:], header[:16])
if err != nil {
//fmt.Printf("error: %v", err)
return nil, nil, err
}
copy(stream.k[:], k)
stream.reset()
for i := range stream.pad {
stream.pad[i] = 0
}
for i, b := range header[cryptoCoreHchacha20InputBytes:] {
stream.nonce[i+cryptoSecretStreamXchacha20poly1305Counterbytes] = b
}
// fmt.Printf("stream: %+v\n", stream.streamState)
return stream, header, nil
}
func (s *encryptor) Push(plain []byte, tag byte) ([]byte, error) {
var err error
//crypto_onetimeauth_poly1305_state poly1305_state;
var poly *poly1305.MAC
//unsigned char block[64U];
var block [64]byte
//unsigned char slen[8U];
var slen [8]byte
//unsigned char *c;
//unsigned char *mac;
//
//if (outlen_p != NULL) {
//*outlen_p = 0U;
//}
mlen := len(plain)
//if (mlen > crypto_secretstream_xchacha20poly1305_MESSAGEBYTES_MAX) {
//sodium_misuse();
//}
out := make([]byte, mlen+XChaCha20Poly1305IetfABYTES)
chacha, err := chacha20.NewUnauthenticatedCipher(s.k[:], s.nonce[:])
if err != nil {
return nil, err
}
//crypto_stream_chacha20_ietf(block, sizeof block, state->nonce, state->k);
chacha.XORKeyStream(block[:], block[:])
//crypto_onetimeauth_poly1305_init(&poly1305_state, block);
var poly_init [32]byte
copy(poly_init[:], block[:])
poly = poly1305.New(&poly_init)
// TODO add support for add data
//sodium_memzero(block, sizeof block);
//crypto_onetimeauth_poly1305_update(&poly1305_state, ad, adlen);
//crypto_onetimeauth_poly1305_update(&poly1305_state, _pad0,
//(0x10 - adlen) & 0xf);
//memset(block, 0, sizeof block);
//block[0] = tag;
memZero(block[:])
block[0] = tag
//
//crypto_stream_chacha20_ietf_xor_ic(block, block, sizeof block, state->nonce, 1U, state->k);
//crypto_onetimeauth_poly1305_update(&poly1305_state, block, sizeof block);
//out[0] = block[0];
chacha.XORKeyStream(block[:], block[:])
_, _ = poly.Write(block[:])
out[0] = block[0]
//
//c = out + (sizeof tag);
c := out[1:]
//crypto_stream_chacha20_ietf_xor_ic(c, m, mlen, state->nonce, 2U, state->k);
//crypto_onetimeauth_poly1305_update(&poly1305_state, c, mlen);
//crypto_onetimeauth_poly1305_update (&poly1305_state, _pad0, (0x10 - (sizeof block) + mlen) & 0xf);
chacha.XORKeyStream(c, plain)
_, _ = poly.Write(c[:mlen])
padlen := (0x10 - len(block) + mlen) & 0xf
_, _ = poly.Write(pad0[:padlen])
//
//STORE64_LE(slen, (uint64_t) adlen);
//crypto_onetimeauth_poly1305_update(&poly1305_state, slen, sizeof slen);
binary.LittleEndian.PutUint64(slen[:], uint64(0))
_, _ = poly.Write(slen[:])
//STORE64_LE(slen, (sizeof block) + mlen);
//crypto_onetimeauth_poly1305_update(&poly1305_state, slen, sizeof slen);
binary.LittleEndian.PutUint64(slen[:], uint64(len(block)+mlen))
_, _ = poly.Write(slen[:])
//
//mac = c + mlen;
//crypto_onetimeauth_poly1305_final(&poly1305_state, mac);
mac := c[mlen:]
copy(mac, poly.Sum(nil))
//sodium_memzero(&poly1305_state, sizeof poly1305_state);
//
//XOR_BUF(STATE_INONCE(state), mac, crypto_secretstream_xchacha20poly1305_INONCEBYTES);
//sodium_increment(STATE_COUNTER(state), crypto_secretstream_xchacha20poly1305_COUNTERBYTES);
xorBuf(s.nonce[cryptoSecretStreamXchacha20poly1305Counterbytes:], mac)
bufInc(s.nonce[:cryptoSecretStreamXchacha20poly1305Counterbytes])
// TODO
//if ((tag & crypto_secretstream_xchacha20poly1305_TAG_REKEY) != 0 ||
//sodium_is_zero(STATE_COUNTER(state),
//crypto_secretstream_xchacha20poly1305_COUNTERBYTES)) {
//crypto_secretstream_xchacha20poly1305_rekey(state);
//}
//if (outlen_p != NULL) {
//*outlen_p = crypto_secretstream_xchacha20poly1305_ABYTES + mlen;
//}
//return 0;
return out, nil
}
func NewDecryptor(key, header []byte) (Decryptor, error) {
stream := &decryptor{}
//crypto_core_hchacha20(state->k, in, k, NULL);
k, err := chacha20.HChaCha20(key, header[:16])
if err != nil {
fmt.Printf("error: %v", err)
return nil, err
}
copy(stream.k[:], k)
//_crypto_secretstream_xchacha20poly1305_counter_reset(state);
stream.reset()
//memcpy(STATE_INONCE(state), in + crypto_core_hchacha20_INPUTBYTES,
// crypto_secretstream_xchacha20poly1305_INONCEBYTES);
copy(stream.nonce[cryptoSecretStreamXchacha20poly1305Counterbytes:],
header[cryptoCoreHchacha20InputBytes:])
//memset(state->_pad, 0, sizeof state->_pad);
copy(stream.pad[:], pad0[:])
//fmt.Printf("decryptor: %+v\n", stream.streamState)
return stream, nil
}
func (s *decryptor) Pull(cipher []byte) ([]byte, byte, error) {
cipherLen := len(cipher)
//crypto_onetimeauth_poly1305_state poly1305_state;
var poly1305State [32]byte
//unsigned char block[64U];
var block [64]byte
//unsigned char slen[8U];
var slen [8]byte
//unsigned char mac[crypto_onetimeauth_poly1305_BYTES];
//const unsigned char *c;
//const unsigned char *stored_mac;
//unsigned long long mlen; // length of the returned message
//unsigned char tag; // for the return value
//
//if (mlen_p != NULL) {
//*mlen_p = 0U;
//}
//if (tag_p != NULL) {
//*tag_p = 0xff;
//}
/*
if (inlen < crypto_secretstream_xchacha20poly1305_ABYTES) {
return -1;
}
mlen = inlen - crypto_secretstream_xchacha20poly1305_ABYTES;
*/
if cipherLen < XChaCha20Poly1305IetfABYTES {
return nil, 0, invalidInput
}
mlen := cipherLen - XChaCha20Poly1305IetfABYTES
//if (mlen > crypto_secretstream_xchacha20poly1305_MESSAGEBYTES_MAX) {
//sodium_misuse();
//}
//crypto_stream_chacha20_ietf(block, sizeof block, state->nonce, state->k);
chacha, err := chacha20.NewUnauthenticatedCipher(s.k[:], s.nonce[:])
if err != nil {
return nil, 0, err
}
chacha.XORKeyStream(block[:], block[:])
//crypto_onetimeauth_poly1305_init(&poly1305_state, block);
copy(poly1305State[:], block[:])
poly := poly1305.New(&poly1305State)
// TODO
//sodium_memzero(block, sizeof block);
//crypto_onetimeauth_poly1305_update(&poly1305_state, ad, adlen);
//crypto_onetimeauth_poly1305_update(&poly1305_state, _pad0,
//(0x10 - adlen) & 0xf);
//
//memset(block, 0, sizeof block);
//block[0] = in[0];
//crypto_stream_chacha20_ietf_xor_ic(block, block, sizeof block, state->nonce, 1U, state->k);
memZero(block[:])
block[0] = cipher[0]
chacha.XORKeyStream(block[:], block[:])
//tag = block[0];
//block[0] = in[0];
//crypto_onetimeauth_poly1305_update(&poly1305_state, block, sizeof block);
tag := block[0]
block[0] = cipher[0]
if _, err = poly.Write(block[:]); err != nil {
return nil, 0, err
}
//c = in + (sizeof tag);
//crypto_onetimeauth_poly1305_update(&poly1305_state, c, mlen);
//crypto_onetimeauth_poly1305_update (&poly1305_state, _pad0, (0x10 - (sizeof block) + mlen) & 0xf);
c := cipher[1:]
if _, err = poly.Write(c[:mlen]); err != nil {
return nil, 0, err
}
padLen := (0x10 - len(block) + mlen) & 0xf
if _, err = poly.Write(pad0[:padLen]); err != nil {
return nil, 0, err
}
//
//STORE64_LE(slen, (uint64_t) adlen);
//crypto_onetimeauth_poly1305_update(&poly1305_state, slen, sizeof slen);
binary.LittleEndian.PutUint64(slen[:], uint64(0))
if _, err = poly.Write(slen[:]); err != nil {
return nil, 0, err
}
//STORE64_LE(slen, (sizeof block) + mlen);
//crypto_onetimeauth_poly1305_update(&poly1305_state, slen, sizeof slen);
binary.LittleEndian.PutUint64(slen[:], uint64(len(block)+mlen))
if _, err = poly.Write(slen[:]); err != nil {
return nil, 0, err
}
//
//crypto_onetimeauth_poly1305_final(&poly1305_state, mac);
//sodium_memzero(&poly1305_state, sizeof poly1305_state);
mac := poly.Sum(nil)
memZero(poly1305State[:])
//stored_mac = c + mlen;
//if (sodium_memcmp(mac, stored_mac, sizeof mac) != 0) {
//sodium_memzero(mac, sizeof mac);
//return -1;
//}
storedMac := c[mlen:]
if !bytes.Equal(mac, storedMac) {
memZero(mac)
return nil, 0, cryptoFailure
}
//crypto_stream_chacha20_ietf_xor_ic(m, c, mlen, state->nonce, 2U, state->k);
//XOR_BUF(STATE_INONCE(state), mac, crypto_secretstream_xchacha20poly1305_INONCEBYTES);
//sodium_increment(STATE_COUNTER(state), crypto_secretstream_xchacha20poly1305_COUNTERBYTES);
m := make([]byte, mlen)
chacha.XORKeyStream(m, c[:mlen])
xorBuf(s.nonce[cryptoSecretStreamXchacha20poly1305Counterbytes:], mac)
bufInc(s.nonce[:cryptoSecretStreamXchacha20poly1305Counterbytes])
// TODO
//if ((tag & crypto_secretstream_xchacha20poly1305_TAG_REKEY) != 0 ||
//sodium_is_zero(STATE_COUNTER(state),
//crypto_secretstream_xchacha20poly1305_COUNTERBYTES)) {
//crypto_secretstream_xchacha20poly1305_rekey(state);
//}
//if (mlen_p != NULL) {
//*mlen_p = mlen;
//}
//if (tag_p != NULL) {
//*tag_p = tag;
//}
//return 0;
return m, tag, nil
}

View file

@ -53,11 +53,11 @@ For encryption, we are using `XChaCha20-Poly1305` algorithm.
## How to use the exported data
* **ente Authenticator app**: You can directly import the codes in the ente Authenticator app.
* **Ente Authenticator app**: You can directly import the codes in the Ente Authenticator app.
> Settings -> Data -> Import Codes -> ente Encrypted export.
* **Decryption Tool** : You can download the prebuilt [decryption tool](decrypt/bin/) (or build it from [source](decrypt)) and run the following command.
* **Decrypt using Ente CLI** : Download the latest version of [Ente CLI](https://github.com/ente-io/ente/releases?q=CLI&expanded=false), and run the following command
```
./decrypt <export_file> <password> <output_file>
./ente auth decrypt <export_file> <output_file>
```