Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Describe backup algorithm #349

Closed
maksim77 opened this issue Jul 12, 2024 · 5 comments
Closed

[Feature] Describe backup algorithm #349

maksim77 opened this issue Jul 12, 2024 · 5 comments
Labels
documentation Improvements or additions to documentation

Comments

@maksim77
Copy link
Contributor

It would be great if there was a detailed description of the backup encryption algorithm somewhere. I would sleep much better if I knew that I could write a script in Python or Golang that would decrypt my backup no matter what happens to my phone.

Describe the solution you'd like
Block diagram or text description of the backup structure

@maksim77 maksim77 added the feature A new feature or a feature request label Jul 12, 2024
@leonlatsch leonlatsch added documentation Improvements or additions to documentation and removed feature A new feature or a feature request labels Jul 12, 2024
@leonlatsch
Copy link
Owner

Structure

This goes for backup version 4

Every backup consists of these files:

  • meta.json (database)
  • .photok (full encrypted image data)
  • .photok.tn (encrypted thumbnail)
  • .photok.vp (encrypted video preview)

Meta.json

A meta.json file looks file this:

{
    "password": "<bcrypt password hash>",
    "createdAt": <timestamp>,
    "backupVersion": 4,
    "photos": [
        {
            "filename": "myimage.jpg",
            "importedAt": 1234567,
            "type": 2, # Mapping in code
            "size": 1234567,
            "uuid": "<uuid>"
        },
        {
            "filename": "myimage.jpg",
            "importedAt": 1234567,
            "type": 2, # Mapping in code
            "size": 1234567,
            "uuid": "<uuid>"
        },
    ],
    "albums": [
        {
            "uuid": "<album_uuid>",
            "name": "My album"
        },
        {
            "uuid": "<album_uuid>",
            "name": "My second album"
        },
    ],
    "albumPhotoRefs": [
        {
            "albumUUID": "<albumUUID>",
            "photoUUID": "<photoUUID>",
            "linkedAt": 1234567
        },
        {
            "albumUUID": "<albumUUID>",
            "photoUUID": "<photoUUID>",
            "linkedAt": 1234567
        },
        {
            "albumUUID": "<albumUUID>",
            "photoUUID": "<photoUUID>",
            "linkedAt": 1234567
        },
        {
            "albumUUID": "<albumUUID>",
            "photoUUID": "<photoUUID>",
            "linkedAt": 1234567
        },
    ]
}

It contains all photos with filenames. The other files each have their uuid as filename.

Albums and albumPhotoRefs are your created albums and the info what photo is linked to what album.

Encryption

The encryption of the files is exactly the same as inside Photok. Meaning you can find the algorithm in the EncryptionManager

The encryption key is derived from the passwords SHA256 hash.

    private fun genSecKey(password: String): SecretKeySpec {
        val md = MessageDigest.getInstance(SHA_256)
        val bytes = md.digest(password.toByteArray(StandardCharsets.UTF_8))
        return SecretKeySpec(bytes, AES)
    }

The IV is also derived from the password (This is kown to be a bit unsecure #204)

    private fun genIv(password: String): IvParameterSpec {
        val iv = ByteArray(16)
        val charArray = password.toCharArray()
        val firstChars = charArray.take(16)
        for (i in firstChars.indices) {
            iv[i] = firstChars[i].toByte()
        }

        return IvParameterSpec(iv)
    }

The encryption algorithm used is AES/GCM/NoPadding with 256 bit key.

Are there more questions?

@maksim77
Copy link
Contributor Author

Not at the moment. Thank you very much! As soon as I get back from vacation, I'll try to sketch out a script.

@leonlatsch
Copy link
Owner

Nice. Then I will close this :)

@u873838
Copy link

u873838 commented Oct 9, 2024

Here is a Go program to decrypt the encrypted .photok{,.tn,.vp} files:

package main

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/sha256"
	"flag"
	"os"
)

func main() {
	password := flag.String("password", "", "photok password")
	flag.Parse()
	key := photokKey(*password)
	iv := photokIV(*password)

	for _, arg := range flag.Args() {
		ciphertext, err := os.ReadFile(arg)
		if err != nil {
			panic(err)
		}
		block, err := aes.NewCipher(key)
		if err != nil {
			panic(err)
		}
		aesgcm, err := cipher.NewGCMWithNonceSize(block, 16)
		if err != nil {
			panic(err)
		}
		plaintext, err := aesgcm.Open(nil, iv, ciphertext, nil)
		if err != nil {
			panic(err)
		}
		if err := os.WriteFile(arg+".plaintext", plaintext, 0644); err != nil {
			panic(err)
		}
	}
}

func photokKey(password string) []byte {
	h := sha256.New()
	h.Write([]byte(password))
	return h.Sum(nil)
}

func photokIV(password string) []byte {
	iv := make([]byte, 16)
	for i := 0; i < 16 && i < len(password); i++ {
		iv[i] = password[i]
	}
	return iv
}

@maksim77
Copy link
Contributor Author

Thank you @u873838! Here’s the version that decodes the entire archive. If you don’t mind, I’ll create a repository that gathers this code and posts the binaries, just in case someone needs them.

package main

import (
	"archive/zip"
	"crypto/aes"
	"crypto/cipher"
	"crypto/sha256"
	"flag"
	"fmt"
	"io"
	"log"
	"os"
	"strings"
	"sync"
)

func main() {
	password := flag.String("password", "", "photok password")
	backup := flag.String("file", "f", "path to backup file")
	num_workers := flag.Int("worker", 10, "number of workers")
	flag.Parse()

	var wg sync.WaitGroup
	jobs := make(chan *zip.File)
	for i := 0; i < *num_workers; i++ {
		wg.Add(1)
		go worker(*password, jobs, &wg)
	}

	r, err := zip.OpenReader(*backup)
	if err != nil {
		panic(err)
	}
	defer r.Close()

	for _, f := range r.File {
		if !strings.HasSuffix(f.Name, ".tn") && strings.HasSuffix(f.Name, ".photok") {
			fmt.Println(f.Name)
			jobs <- f
		}
	}

	close(jobs)

	wg.Wait()
}

func worker(password string, jobs <-chan *zip.File, wg *sync.WaitGroup) {
	defer wg.Done()
	key := photokKey(password)
	iv := photokIV(password)

	block, err := aes.NewCipher(key)
	if err != nil {
		panic(err)
	}
	aesgcm, err := cipher.NewGCMWithNonceSize(block, 16)
	if err != nil {
		panic(err)
	}
	for f := range jobs {
		reader, err := f.Open()
		if err != nil {
			log.Fatal(err)
		}
		ciphertext, err := io.ReadAll(reader)
		if err != nil {
			log.Fatal(err)
		}
		plaintext, err := aesgcm.Open(nil, iv, ciphertext, nil)
		if err != nil {
			panic(err)
		}
		if err := os.WriteFile(f.Name+".plaintext", plaintext, 0o644); err != nil {
			panic(err)
		}
	}
}

func photokKey(password string) []byte {
	h := sha256.New()
	h.Write([]byte(password))
	return h.Sum(nil)
}

func photokIV(password string) []byte {
	iv := make([]byte, 16)
	for i := 0; i < 16 && i < len(password); i++ {
		iv[i] = password[i]
	}
	return iv
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

3 participants