Skip to content

Merge #5

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

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
118 changes: 88 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,56 +1,114 @@
# Web Browser Based File Encryption / Decryption
# Save action

Use your web browser to encrypt and decrypt files.
The save action saves a cache. It works similarly to the `cache` action except that it doesn't first do a restore. This action provides granular ability to save a cache without having to restore it, or to do a save at any stage of the workflow job -- not only in post phase.

## Usage
## Documentation

Download [web-browser-based-file-encryption-decryption.html](https://github.com/meixler/web-browser-based-file-encryption-decryption/blob/master/web-browser-based-file-encryption-decryption.html), then open the .html document in your web browser. Or, simply point your web browser to the .html document hosted [here](https://www.meixler-tech.com/web-browser-based-file-encryption-decryption.html).
### Inputs

Then, use the web page rendered in your browser to encrypt a file using a password. Use the same password later to decrypt the file. IMPORTANT: The same password that was used to encrypt the file must be used to decrypt the file later. If you loose or forget the password, it cannot be recovered!
* `key` - An explicit key for a cache entry. See [creating a cache key](../README.md#creating-a-cache-key).
* `path` - A list of files, directories, and wildcard patterns to cache. See [`@actions/glob`](https://github.com/actions/toolkit/tree/main/packages/glob) for supported patterns.
* `upload-chunk-size` - The chunk size used to split up large files during upload, in bytes

## Operation and privacy
### Outputs

The page uses javascript running within your web browser to encrypt and decrypt files client-side, in-browser. The page makes no network connections during this process, to ensure that your files and password do not leave your web browser during the process. This can be independently verified by reviewing the source code for the page, or by monitoring your web browser's [networking activity](https://developer.mozilla.org/en-US/docs/Tools/Network_Monitor) during operation of the page. The page can also be downloaded and run locally on your system offline.
This action has no outputs.

## Cryptography
## Use cases

All client-side cryptography is implemented using the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API). Files are encrypted using AES-CBC 256-bit symmetric encryption. The encryption key is derived from the password and a random salt using PBKDF2 derivation with 10000 iterations of SHA256 hashing.

### Only save cache

## Compatibility with openssl
If you are using separate jobs for generating common artifacts and sharing them across jobs, this action will take care of your cache saving needs.

The encryption used by the page is compatible with [openssl](https://www.openssl.org/docs/man1.1.1/man1/openssl-enc.html).
```yaml
steps:
- uses: actions/checkout@v4

Files encrypted using the page can be decrypted using openssl using the following command:

openssl aes-256-cbc -d -salt -pbkdf2 -iter 10000 -in encryptedfilename -out plaintextfilename
- name: Install Dependencies
run: /install.sh

Files encrypted using the following openssl command can be decrypted using the page:
- name: Build artifacts
run: /build.sh

openssl aes-256-cbc -e -salt -pbkdf2 -iter 10000 -in plaintextfilename -out encryptedfilename
- uses: actions/cache/save@v4
id: cache
with:
path: path/to/dependencies
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
```

## Running the page offline
### Re-evaluate cache key while saving

The web page is self-contained. The page does not require any supporting files; all javascript and css for the page is contained in the source code of the page.
To run the page locally on your system offline, simply save the page to your system as a .html file, then open the file from your system in your web browser (optionally with networking disabled).
With this save action, the key can now be re-evaluated while executing the action. This helps in cases where lockfiles are generated during the build.

## Verifying the integrity of the page
Let's say we have a restore step that computes a key at runtime.

The expected SHA256 checksum hash of the .html file containing the page is:
#### Restore a cache

c7398059dffd25fa8a9d81c570250887fba61dc4eafcfca42f9081196389ed05
```yaml
uses: actions/cache/restore@v4
id: restore-cache
with:
key: cache-${{ hashFiles('**/lockfiles') }}
```

If loading the page from a web server, you can verify that the checksum hash of the .html file downloaded from the web server matches the expected checksum hash using the [Page Integrity browser extension](https://www.pageintegrity.net/).
If running the page offline, it is recommended that you verify that the checksum hash of the .html file matches the expected checksum hash before opening the file in your web browser.
#### Case 1 - Where a user would want to reuse the key as it is
```yaml
uses: actions/cache/save@v4
with:
key: ${{ steps.restore-cache.outputs.cache-primary-key }}
```

## Contributing
#### Case 2 - Where the user would want to re-evaluate the key

Pull requests are welcome.
```yaml
uses: actions/cache/save@v4
with:
key: npm-cache-${{hashfiles(package-lock.json)}}
```

## License
### Always save cache

This project is licensed under the [GPL-3.0](https://www.gnu.org/licenses/gpl-3.0.en.html) open source license.
There are instances where some flaky test cases would fail the entire workflow and users would get frustrated because the builds would run for hours and the cache couldn't be saved as the workflow failed in between.
For such use-cases, users now have the ability to use the `actions/cache/save` action to save the cache by using an [`always()`](https://docs.github.com/actions/writing-workflows/choosing-what-your-workflow-does/expressions#always) condition.
This way the cache will always be saved if generated, or a warning will be generated that nothing is found on the cache path. Users can also use the `if` condition to only execute the `actions/cache/save` action depending on the output of previous steps. This way they get more control of when to save the cache.

## Contact
To avoid saving a cache that already exists, the `cache-hit` output from a restore step should be checked.

Please [contact MTI](https://www.meixler-tech.com/contact.php) for any questions or comments concerning this project.
The `cache-primary-key` output from the restore step should also be used to ensure
the cache key does not change during the build if it's calculated based on file contents.

```yaml
name: Always Caching Primes

on: push

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Restore cached Primes
id: cache-primes-restore
uses: actions/cache/restore@v4
with:
key: ${{ runner.os }}-primes
path: |
path/to/dependencies
some/other/dependencies

# Intermediate workflow steps

- name: Always Save Primes
id: cache-primes-save
if: always() && steps.cache-primes-restore.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
key: ${{ steps.cache-primes-restore.outputs.cache-primary-key }}
path: |
path/to/dependencies
some/other/dependencies
```
148 changes: 148 additions & 0 deletions decrypt_photos.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# -*- coding: utf-8 -*-
## Copyright (c) 2014, Fundación Dr. Manuel Sadosky. Todos los derechos reservados.
##
## La redistribución y el uso en las formas de código fuente y binario, con o sin
## modificaciones, están permitidos siempre que se cumplan las siguientes condiciones:
##
## 1. Las redistribuciones del código fuente deben conservar el aviso de copyright
## anterior, esta lista de condiciones y el siguiente descargo de responsabilidad.
##
## 2. Las redistribuciones en formato binario deben reproducir el aviso de copyright
## anterior, esta lista de condiciones y la siguiente renuncia en la documentación
## y/u otros materiales suministrados con la distribución.
##
## ESTE SOFTWARE SE SUMINISTRA POR LA Fundación Dr. Manuel Sadosky ''COMO ESTÁ'' Y CUALQUIER
## GARANTÍA EXPRESA O IMPLÍCITAS, INCLUYENDO, PERO NO LIMITADO A, LAS GARANTÍAS
## IMPLÍCITAS DE COMERCIALIZACIÓN Y APTITUD PARA UN PROPÓSITO DETERMINADO SON
## RECHAZADAS. EN NINGÚN CASO Fundación Dr. Manuel Sadosky SERÁ RESPONSABLE POR NINGÚN
## DAÑO DIRECTO, INDIRECTO, INCIDENTAL, ESPECIAL, EJEMPLAR O CONSECUENTE (INCLUYENDO,
## PERO NO LIMITADO A, LA ADQUISICIÓN DE BIENES O SERVICIOS; LA PÉRDIDA DE USO, DE
## DATOS O DE BENEFICIOS; O INTERRUPCIÓN DE LA ACTIVIDAD EMPRESARIAL) O POR
## CUALQUIER TEORÍA DE RESPONSABILIDAD, YA SEA POR CONTRATO, RESPONSABILIDAD ESTRICTA
## O AGRAVIO (INCLUYENDO NEGLIGENCIA O CUALQUIER OTRA CAUSA) QUE SURJA DE CUALQUIER
## MANERA DEL USO DE ESTE SOFTWARE, INCLUSO SI SE HA ADVERTIDO DE LA POSIBILIDAD DE
## TALES DAÑOS.
##
## Las opiniones y conclusiones contenidas en el software y la documentación son las
## de los autores y no deben interpretarse como la representación de las políticas
## oficiales, ya sea expresa o implícita, de Fundación Dr. Manuel Sadosky .
##
## Decifrado de imagenes de Snapchat version 5.0.34.10
## Author: Joaquín Rinaudo
##

import subprocess
import shlex
import md5
from hashlib import md5
from Crypto.Cipher import AES
from Crypto import Random
import json
import time
import os
import base64
import re

def run_get_output(command):
try:
return subprocess.check_output(shlex.split(command),stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as grepexc:
return grepexc.output

def run_blocking_cmd(command):
return subprocess.call(shlex.split(command),stdout=subprocess.PIPE,stderr=subprocess.PIPE)

def get_android_id():
try:
android_id = run_get_output(''' adb shell content query --uri content://settings/secure --projection name:value --where "name=\\'android_id\\'" ''')
p = re.compile('Row: 0 name=android_id, value=(.*)')
android_id = p.findall(android_id)[0].strip()
except:
android_id = run_get_output(''' adb shell settings get secure android_id ''').strip()
print android_id
return android_id

def get_version():
version = run_get_output(''' adb shell dumpsys package com.snapchat.android ''')
p = re.compile('versionName=(.*)')
print p.findall(version)[0].strip()
return p.findall(version)[0].strip()

def pull_bananas_cache_file():
version38 = ''
if VERSION >= '5.0.38.1':
version38 = '1'
run_blocking_cmd(''' adb pull /data/data/com.snapchat.android/cache/bananas%s encrypted_bananas_file ''' %version38)

def snapchat_bananas_password():
m = md5()
m.update( get_android_id() )
m.update('seems legit...')
return m.hexdigest()

def decrypt_bananas_file():
with open("encrypted_bananas_file") as encrypted_bananas:
with open("decrypted_bananas_file",'w') as decrypted_bananas:
cipher = AES.new(snapchat_bananas_password(), AES.MODE_ECB)
decrypt_file(encrypted_bananas,decrypted_bananas,cipher )

def pull_images():
run_blocking_cmd(''' adb pull /data/data/com.snapchat.android/cache/received_image_snaps/ encrypted_received_image_snaps/ ''')

def extract_key_and_iv(json_bananas):
if VERSION < '5.0.38.1':
key= json_bananas['a']
iv= json_bananas['b']
else:
key= json_bananas[0]
iv= json_bananas[1]
return (base64.b64decode(key).encode('hex'),base64.b64decode(iv).encode('hex'))

def decrypt_images():
if not os.path.exists('decrypted_received_image_snaps'): os.makedirs('decrypted_received_image_snaps')
with open("decrypted_bananas_file") as json_file:
bananas = json.loads(json_file.read().decode('utf8'))
if not bananas.get('snapKeysAndIvs',None):
print "Images were already viewed, the key and IV were deleted from the /cache/bananas file. Try reopening Snapchat to check if images where downloaded."
exit(0)
json_bananas = json.loads(bananas['snapKeysAndIvs'],encoding='utf8')
if len(json_bananas) < len( os.listdir("encrypted_received_image_snaps") ):
print "There are less keys than snaps, some snaps won't be able to be decrypted"
for file_name in os.listdir("encrypted_received_image_snaps"):
could_decrypt = False
for snap_pair in json_bananas:
(key,iv) = extract_key_and_iv( json_bananas[snap_pair] )

s = run_get_output('openssl aes-128-cbc -K %s -iv %s -d -in encrypted_received_image_snaps/%s -out decrypted_received_image_snaps/%s' %(key,iv,file_name,file_name))
if s == '':
could_decrypt = True
break
#no error then the image was decoded properly
if not could_decrypt:
print "The image %s could not be decrypted, none of the keys in the bananas file worked" %file_name
#break when decrypt work

def decrypt_file(in_file, out_file, cipher):
bs = AES.block_size
next_chunk = ''
finished = False
while not finished:
chunk, next_chunk = next_chunk, cipher.decrypt(in_file.read(1024 * bs))
if len(next_chunk) == 0:
padding_length = ord(chunk[-1])
chunk = chunk[:-padding_length]
finished = True
out_file.write(chunk)

if __name__ == '__main__':
global VERSION
VERSION = get_version()
#stop the application to save the 'bananas file'
run_blocking_cmd('adb shell am force-stop com.snapchat.android')
pull_images()
pull_bananas_cache_file()
decrypt_bananas_file()
decrypt_images()
#Cleaning up
run_blocking_cmd('rm encrypted_bananas_file decrypted_bananas_file')
run_blocking_cmd('rm -r encrypted_received_image_snaps')
Loading