Add decryption tool for auth encrypted export (#230)

This commit is contained in:
Neeraj Gupta 2023-09-05 13:18:26 +05:30 committed by GitHub
commit 5d0007d749
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 262 additions and 7 deletions

View file

@ -0,0 +1,70 @@
package main
import (
"bytes"
"encoding/base64"
"fmt"
"io"
"log"
"github.com/jamesruan/sodium"
"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 decryptChaCha20poly1305(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
}

View file

@ -0,0 +1,53 @@
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 := decryptChaCha20poly1305(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)
}
}

BIN
migration-guides/decrypt/decrypt Executable file

Binary file not shown.

View file

@ -0,0 +1,105 @@
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 := decryptChaCha20poly1305(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

@ -0,0 +1,10 @@
module decrypt
go 1.20
require (
github.com/jamesruan/sodium v1.0.14
golang.org/x/crypto v0.11.0
)
require golang.org/x/sys v0.10.0 // indirect

View file

@ -0,0 +1,6 @@
github.com/jamesruan/sodium v1.0.14 h1:JfOHobip/lUWouxHV3PwYwu3gsLewPrDrZXO3HuBzUU=
github.com/jamesruan/sodium v1.0.14/go.mod h1:GK2+LACf7kuVQ9k7Irk0MB2B65j5rVqkz+9ylGIggZk=
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

@ -2,8 +2,9 @@
## Overview
When we export the auth codes, the data is encrypted using a key derived from the user's password.
This document describes the JSON structure used to organize exported data, including versioning and key derivation parameters.
When we export the auth codes, the data is encrypted using a key derived from the user's password.
This document describes the JSON structure used to organize exported data, including versioning and key derivation
parameters.
## Export JSON Sample
@ -27,10 +28,12 @@ The main object used to represent the export data. It contains the following key
- `encryptedData"`: The encrypted authentication data.
- `encryptionNonce`: The nonce used for encryption.
### Version
### Version
Export version is used to identify the format of the export data.
Export version is used to identify the format of the export data.
#### Ver: 1
* KDF Algorithm: `ARGON2ID`
* Decrypted data format: `otpauth://totp/...`, separated by a new line.
* Encryption Algo: `XChaCha20-Poly1305`
@ -44,9 +47,17 @@ This section contains the parameters that were using during KDF operation:
- `salt`: The salt used in the derivation process.
#### Encrypted Data
As mentioned above, the auth data is encrypted using a key that's derived by using user provided password & kdf params.
For encryption, we are using `XChaCha20-Poly1305` algorithm.
For encryption, we are using `XChaCha20-Poly1305` algorithm.
## How to use the export data
* **ente Authenticator app**: You can directly import the codes in the ente Authenticator app.
>Settings -> Data -> Import Codes -> ente Encrypted export.
* **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 [decrypt tool](decrypt/decrypt) and run the following command.
```./decrypt <export_file> <password> <output_file>```