mirror of
https://github.com/henrybear327/Proton-API-Bridge.git
synced 2026-04-19 14:26:53 -04:00
Initial commit
This commit is contained in:
28
.github/workflows/check.yml
vendored
Normal file
28
.github/workflows/check.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Lint and Test
|
||||
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get sources
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go 1.18
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.18'
|
||||
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: v1.50.0
|
||||
args: --timeout=180s
|
||||
skip-cache: true
|
||||
|
||||
# - name: Run tests
|
||||
# run: go test -v ./...
|
||||
|
||||
# - name: Run tests with race check
|
||||
# run: go test -v -race ./...
|
||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.DS_Store
|
||||
.credential
|
||||
.*.credential
|
||||
data
|
||||
config.toml
|
||||
31
Documentation.md
Normal file
31
Documentation.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Documentation
|
||||
|
||||
Since the Proton API isn't open sourced, this document serves as the team's understanding for future reference.
|
||||
|
||||
# Proton Drive API
|
||||
|
||||
## Terminology
|
||||
|
||||
### Volume
|
||||
|
||||
### Share
|
||||
|
||||
### Node
|
||||
|
||||
### Link
|
||||
|
||||
## Encryption
|
||||
|
||||
Encryption, decryption, and signature signing and verification, etc. are all performed by using the go-crypto library.
|
||||
|
||||
### Login
|
||||
|
||||
Proton uses SRP for logging in the users. After logging in, there is a small time window (several minutes) where users can access certain routes, which is in the `scope` field, e.g. getting user salt.
|
||||
|
||||
Since the user and address key rings are encrypted with passphrase tied to salt and user password, we need to cache this information as soon as the first log in happens for future usage.
|
||||
|
||||
### User Key
|
||||
|
||||
### Address Key
|
||||
|
||||
### Node/Link Key
|
||||
135
README.md
135
README.md
@@ -1,2 +1,137 @@
|
||||
# Proton API Bridge
|
||||
|
||||
Thanks to Proton open sourcing [proton-go-api](https://github.com/ProtonMail/go-proton-api) and the web, iOS, and Android client codebases, we don't need to completely reverse engineer the APIs.
|
||||
|
||||
[proton-go-api](https://github.com/ProtonMail/go-proton-api) provides the basic building blocks of API calls and error handling, such as 429 exponential back-off, but it is pretty much just a barebone interface to the Proton API. For example, the encryption and decryption of the Proton Drive file are not provided in this library.
|
||||
|
||||
This codebase, Proton API Bridge, bridges the gap, so software like [rclone](https://github.com/rclone/rclone) can be built on top of this quickly. This codebase handles the intricate tasks before and after calling Proton APIs, particularly the complex encryption scheme, allowing developers to implement features for other software on top of this codebase.
|
||||
|
||||
Currently, only Proton Drive APIs are bridged, as we are aiming to implement a backend for rclone.
|
||||
|
||||
## Sidenotes
|
||||
|
||||
We are using a fork of the [proton-go-api](https://github.com/henrybear327/go-proton-api), adding quite some new code to it. We will try to commit back to the upstream once we feel like the code changes are stable.
|
||||
|
||||
# Instructions to run the code
|
||||
|
||||
## Compiling and running
|
||||
|
||||
`go run .`
|
||||
|
||||
## Unit testing and linting
|
||||
|
||||
`golangci-lint run && go test -race -failfast -v ./...`
|
||||
|
||||
# Drive APIs
|
||||
|
||||
> In collaboration with Azimjon Pulatov, in memory of our good old days at Meta, London, in the summer of 2022.
|
||||
|
||||
Currently, the development are split into 2 versions. V1 supports the features [required by rclone](https://github.com/henrybear327/rclone/blob/master/fs/types.go), such as `file listing`. V2 will support the extra features that rclone has interface for, such as `move file` and `move folder` operations.
|
||||
|
||||
## V1
|
||||
|
||||
### Features
|
||||
|
||||
- [x] Log in to an account without 2FA using username and password
|
||||
- [x] Obtain keyring
|
||||
- [x] Cache access token, etc. to be able to reuse the session
|
||||
- [x] Bug: 403: Access token does not have sufficient scope - used the wrong newClient function
|
||||
- [x] Volume actions
|
||||
- [x] List all volumes
|
||||
- [x] Share actions
|
||||
- [x] Get all shares
|
||||
- [x] Get default share
|
||||
- [x] Fix context with proper propagation instead of using `ctx` everywhere
|
||||
- [x] Folder actions
|
||||
- [x] List all folders and files within the root folder
|
||||
- [x] BUG: listing directory - missing signature when there are more than 1 share
|
||||
- maybe the way I decrypt the keyring is wrong
|
||||
- (wrong fix for the first time) bug on no name for the root folder
|
||||
- (correct fix) we need to check for the "active" folder type first
|
||||
- [x] List all folders and files recursively within the root folder
|
||||
- [x] Delete
|
||||
- [x] Implement delete all for testing -> very dangerous, thus currently guarded with a hardcoded string
|
||||
- [x] Create
|
||||
- [ ] File actions
|
||||
- [x] Download
|
||||
- [x] Download empty file
|
||||
- [ ] Properly handle large files and empty files (check iOS codebase)
|
||||
- esp. large files, where buffering in-memory will screw up the runtime
|
||||
- [ ] Check signature and hash
|
||||
- [x] Delete
|
||||
- [x] Upload
|
||||
- [x] Handle empty file
|
||||
- [x] Parse mime type
|
||||
- [ ] Force overwrite
|
||||
- [ ] Add revision
|
||||
- [ ] Improve to handle large files
|
||||
- [ ] Upload verification
|
||||
- [ ] Handle failed / interrupted upload
|
||||
- [x] Modified time
|
||||
- [ ] List file metadata
|
||||
- [ ] Duplicated file/folder name handling: 422: A file or folder with that name already exists (Code=2500, Status=422)
|
||||
- [ ] Handle ERROR RESTY 422: File or folder was not found. (Code=2501, Status=422), Attempt 1
|
||||
- [x] Init ProtonDrive with config passed in as Map
|
||||
- [x] Remove all `log.Fatalln` and use proper error propagation (basically remove `HandleError` and we go from there)
|
||||
- [x] Integration tests
|
||||
- [x] Remove drive demo code
|
||||
- [x] Create a Drive struct to encapsulate all the functions (maybe?)
|
||||
- [x] Move comments to proper places
|
||||
- [x] Modify `shouldRejectDestructiveActions()`
|
||||
- [ ] Check file metadata
|
||||
- [ ] Try to check if all functions are used at least once so we know if it's functioning or not
|
||||
- [ ] Documentation
|
||||
- [x] Reduce config options on caching access token
|
||||
- [x] Remove integration test safeguarding
|
||||
- [ ] Improve file searching function to use HMAC instead of just using string comparison
|
||||
- [ ] Remove e.g. proton.link related exposures in the function signature (this library should abstract them all)
|
||||
|
||||
### TODO
|
||||
|
||||
- [x] address go dependencies
|
||||
- Fixed by doing the following in the `go-proton-api` repo to bump to use the latest commit
|
||||
- `go get github.com/ProtonMail/go-proton-api@ea8de5f674b7f9b0cca8e3a5076ffe3c5a867e01`
|
||||
- `go get github.com/ProtonMail/gluon@fb7689b15ae39c3efec3ff3c615c3d2dac41cec8`
|
||||
- [x] Remove mail-related apis (to reduce dependencies)
|
||||
- [x] Make a "super class" and expose all necessary methods for the outside to call
|
||||
- [x] Add 2FA login
|
||||
- [ ] Go through Drive iOS source code and check the logic control flow
|
||||
- [x] Fix the function argument passing (using pointers)
|
||||
- [ ] Use proper AppVersion (we need to be friendly to the Proton servers)
|
||||
- [ ] Handle account with
|
||||
- [ ] multiple addresses
|
||||
- [ ] multiple keys per addresses
|
||||
- [ ] multiple shares
|
||||
- [x] Update RClone's contribution.md file
|
||||
- [x] Remove delete all's hardcoded string
|
||||
- [ ] Address TODO and FIXME
|
||||
- [ ] Use CI to run integration tests
|
||||
- [ ] Figure out the bottleneck by doing some profiling
|
||||
- [ ] Some error handling from [here](https://github.com/ProtonMail/WebClients/blob/main/packages/shared/lib/drive/constants.ts) MAX_NAME_LENGTH, TIMEOUT
|
||||
- [x] Point to the right proton-go-api branch
|
||||
- [x] Run `go get github.com/henrybear327/go-proton-api@dev` to update go mod
|
||||
|
||||
### Known limitations
|
||||
|
||||
- Large file handling: for uploading, files will be loaded into the memory entirely, encrypted, and then chunked; for downloading, the file will be written when all blocks are decrypted and checked
|
||||
- Crypto-related operations, e.g. signature verification, still needs to cross check with iOS or web open source codebase
|
||||
- No move for file and folders, thumbnails, respecting accepted MIME types, max upload size, can't init Proton Drive (coming in V2)
|
||||
- Assumptions: only one main share per account
|
||||
|
||||
## V2
|
||||
|
||||
Moving files and folders are [features](https://github.com/rclone/rclone/blob/51a468b2bae4ca8e21760435211623a8199a9167/fs/features.go#L25)
|
||||
|
||||
- [ ] Folder
|
||||
- [ ] (Feature) Update (force overwrite)
|
||||
- [ ] (Feature) Move
|
||||
- [ ] Commit back to proton-go-api and switch to using upstream (make sure the tag is at the tip though)
|
||||
- [ ] Support legacy 2-password mode
|
||||
- [ ] Support thumbnail
|
||||
- [ ] Proton Drive init (no prior Proton Drive login before -> probably will have no key, volume, etc. to start with at all)
|
||||
- [ ] linkID caching -> would need to listen to the event api though
|
||||
|
||||
# Questions
|
||||
|
||||
- [x] rclone's folder / file rename detection? -> just implement the interface and rclone will deal with the rest!
|
||||
- [ ] How often will we run into 429 on login
|
||||
|
||||
96
common/config.go
Normal file
96
common/config.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package common
|
||||
|
||||
import "os"
|
||||
|
||||
type Config struct {
|
||||
/* Login */
|
||||
FirstLoginCredential *FirstLoginCredentialData
|
||||
ReusableCredential *ReusableCredentialData
|
||||
UseReusableLogin bool
|
||||
CredentialCacheFile string // If CredentialCacheFile is empty, no credential will be logged
|
||||
RefreshAccessToken bool
|
||||
|
||||
/* Setting */
|
||||
DestructiveIntegrationTest bool // CAUTION: the integration test requires a clean proton drive
|
||||
EmptyTrashAfterIntegrationTest bool // CAUTION: the integration test will clean up all the data in the trash
|
||||
|
||||
/* Drive */
|
||||
DataFolderName string
|
||||
}
|
||||
|
||||
type FirstLoginCredentialData struct {
|
||||
Username string
|
||||
Password string
|
||||
TwoFA string
|
||||
}
|
||||
|
||||
type ReusableCredentialData struct {
|
||||
UID string
|
||||
AccessToken string
|
||||
RefreshToken string
|
||||
SaltedKeyPass string // []byte <-> base64
|
||||
}
|
||||
|
||||
func NewConfigWithDefaultValues() *Config {
|
||||
return &Config{
|
||||
// login
|
||||
FirstLoginCredential: &FirstLoginCredentialData{
|
||||
Username: "",
|
||||
Password: "",
|
||||
TwoFA: "",
|
||||
},
|
||||
ReusableCredential: &ReusableCredentialData{
|
||||
UID: "",
|
||||
AccessToken: "",
|
||||
RefreshToken: "",
|
||||
SaltedKeyPass: "", // []byte <-> base64
|
||||
},
|
||||
UseReusableLogin: false,
|
||||
CredentialCacheFile: "",
|
||||
RefreshAccessToken: false,
|
||||
|
||||
DestructiveIntegrationTest: false,
|
||||
EmptyTrashAfterIntegrationTest: false,
|
||||
|
||||
DataFolderName: "data",
|
||||
}
|
||||
}
|
||||
|
||||
func NewConfigForIntegrationTests() *Config {
|
||||
username := os.Getenv("PROTON_API_BRIDGE_TEST_USERNAME")
|
||||
password := os.Getenv("PROTON_API_BRIDGE_TEST_PASSWORD")
|
||||
twoFA := os.Getenv("PROTON_API_BRIDGE_TEST_TWOFA")
|
||||
|
||||
useReusableLoginStr := os.Getenv("PROTON_API_BRIDGE_TEST_USE_REUSABLE_LOGIN")
|
||||
useReusableLogin := false
|
||||
if useReusableLoginStr == "1" {
|
||||
useReusableLogin = true
|
||||
}
|
||||
|
||||
uid := os.Getenv("PROTON_API_BRIDGE_TEST_UID")
|
||||
accessToken := os.Getenv("PROTON_API_BRIDGE_TEST_ACCESS_TOKEN")
|
||||
refreshToken := os.Getenv("PROTON_API_BRIDGE_TEST_REFRESH_TOKEN")
|
||||
saltedKeyPass := os.Getenv("PROTON_API_BRIDGE_TEST_SALTEDKEYPASS")
|
||||
|
||||
return &Config{
|
||||
FirstLoginCredential: &FirstLoginCredentialData{
|
||||
Username: username,
|
||||
Password: password,
|
||||
TwoFA: twoFA,
|
||||
},
|
||||
ReusableCredential: &ReusableCredentialData{
|
||||
UID: uid,
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
SaltedKeyPass: saltedKeyPass, // []byte <-> base64
|
||||
},
|
||||
UseReusableLogin: useReusableLogin,
|
||||
CredentialCacheFile: ".credential",
|
||||
RefreshAccessToken: false,
|
||||
|
||||
DestructiveIntegrationTest: true,
|
||||
EmptyTrashAfterIntegrationTest: true,
|
||||
|
||||
DataFolderName: "data",
|
||||
}
|
||||
}
|
||||
11
common/error.go
Normal file
11
common/error.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package common
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrKeyPassOrSaltedKeyPassMustBeNotNil = errors.New("either keyPass or saltedKeyPass must be not nil")
|
||||
ErrFailedToUnlockUserKeys = errors.New("failed to unlock user keys")
|
||||
|
||||
ErrUsernameAndPasswordRequired = errors.New("username and password are required")
|
||||
Err2FACodeRequired = errors.New("this account requires a 2FA code")
|
||||
)
|
||||
70
common/keyring.go
Normal file
70
common/keyring.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/henrybear327/go-proton-api"
|
||||
)
|
||||
|
||||
/*
|
||||
The Proton account keys are organized in the following hierarchy.
|
||||
|
||||
An account has some users, each of the user will have one or more user keys.
|
||||
Each of the user will have some addresses, each of the address will have one or more address keys.
|
||||
|
||||
A key is encrypted by a passphrase, and the passphrase is encrypted by another key.
|
||||
|
||||
The address keyrings are encrypted with the primary user keyring at the time.
|
||||
|
||||
The primary address key is used to create (encrypt) and retrieve (decrypt) data, e.g. shares
|
||||
*/
|
||||
func getAccountKRs(ctx context.Context, c *proton.Client, keyPass, saltedKeyPass []byte) (*crypto.KeyRing, map[string]*crypto.KeyRing, []proton.Address, []byte, error) {
|
||||
/* Code taken and modified from proton-bridge */
|
||||
|
||||
user, err := c.GetUser(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
// log.Printf("user %#v", user)
|
||||
|
||||
addr, err := c.GetAddresses(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
// log.Printf("addr %#v", addr)
|
||||
|
||||
if saltedKeyPass == nil {
|
||||
if keyPass == nil {
|
||||
return nil, nil, nil, nil, ErrKeyPassOrSaltedKeyPassMustBeNotNil
|
||||
}
|
||||
|
||||
/*
|
||||
Notes for -> BUG: Access token does not have sufficient scope
|
||||
Only within the first x minutes that the user logs in with username and password, the getSalts route will be available to be called!
|
||||
*/
|
||||
salts, err := c.GetSalts(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
// log.Printf("salts %#v", salts)
|
||||
|
||||
saltedKeyPass, err = salts.SaltForKey(keyPass, user.Keys.Primary().ID)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
// log.Printf("saltedKeyPass ok")
|
||||
}
|
||||
|
||||
userKR, addrKRs, err := proton.Unlock(user, addr, saltedKeyPass, nil)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
|
||||
} else if userKR.CountDecryptionEntities() == 0 {
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, ErrFailedToUnlockUserKeys
|
||||
}
|
||||
}
|
||||
|
||||
return userKR, addrKRs, addr, saltedKeyPass, nil
|
||||
}
|
||||
28
common/proton_manager.go
Normal file
28
common/proton_manager.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/henrybear327/go-proton-api"
|
||||
)
|
||||
|
||||
// TODO: use proper appname and version
|
||||
func AppVersion() string {
|
||||
// return "web-drive@5.0.13.8"
|
||||
return "ios-drive@1.14.0"
|
||||
}
|
||||
|
||||
func defaultAPIOptions() []proton.Option {
|
||||
return []proton.Option{
|
||||
proton.WithAppVersion(AppVersion()),
|
||||
}
|
||||
}
|
||||
|
||||
func getProtonManager() *proton.Manager {
|
||||
/*
|
||||
Notes on API calls
|
||||
|
||||
If the app version is not specified, the api calls will be rejected.
|
||||
*/
|
||||
m := proton.New(defaultAPIOptions()...)
|
||||
|
||||
return m
|
||||
}
|
||||
156
common/user.go
Normal file
156
common/user.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/henrybear327/go-proton-api"
|
||||
)
|
||||
|
||||
func cacheCredentialToFile(config *Config) error {
|
||||
if config.CredentialCacheFile != "" {
|
||||
str, err := json.Marshal(config.ReusableCredential)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := os.Create(config.CredentialCacheFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
_, err = file.WriteString(string(str))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
Log in methods
|
||||
- username and password to log in
|
||||
- UID and refresh token
|
||||
|
||||
Keyring decryption
|
||||
The password will be salted, and then used to decrypt the keyring. The salted password needs to be and can be cached, so the keyring can be re-decrypted when needed
|
||||
*/
|
||||
func Login(ctx context.Context, config *Config) (*proton.Manager, *proton.Client, *crypto.KeyRing, map[string]*crypto.KeyRing, []proton.Address, error) {
|
||||
var c *proton.Client
|
||||
var auth proton.Auth
|
||||
var userKR *crypto.KeyRing
|
||||
var addrKRs map[string]*crypto.KeyRing
|
||||
var addr []proton.Address
|
||||
|
||||
// get manager
|
||||
m := getProtonManager()
|
||||
|
||||
if config.UseReusableLogin {
|
||||
/*
|
||||
Using NewClientWithRefresh so the credential can last longer,
|
||||
as each run of the program will trigger a access token refresh
|
||||
*/
|
||||
var err error
|
||||
if config.RefreshAccessToken {
|
||||
c, auth, err = m.NewClientWithRefresh(ctx, config.ReusableCredential.UID, config.ReusableCredential.RefreshToken)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
config.ReusableCredential.UID = auth.UID
|
||||
config.ReusableCredential.AccessToken = auth.AccessToken
|
||||
config.ReusableCredential.RefreshToken = auth.RefreshToken
|
||||
} else {
|
||||
c = m.NewClient(config.ReusableCredential.UID, config.ReusableCredential.AccessToken, config.ReusableCredential.RefreshToken)
|
||||
}
|
||||
|
||||
err = cacheCredentialToFile(config)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
SaltedKeyPassByteArr, err := base64.StdEncoding.DecodeString(config.ReusableCredential.SaltedKeyPass)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
userKR, addrKRs, addr, _, err = getAccountKRs(ctx, c, nil, SaltedKeyPassByteArr)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
} else {
|
||||
username := config.FirstLoginCredential.Username
|
||||
password := config.FirstLoginCredential.Password
|
||||
if username == "" || password == "" {
|
||||
return nil, nil, nil, nil, nil, ErrUsernameAndPasswordRequired
|
||||
}
|
||||
|
||||
// perform login
|
||||
var err error
|
||||
c, auth, err = m.NewClientWithLogin(ctx, username, []byte(password))
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
if auth.TwoFA.Enabled&proton.HasTOTP != 0 {
|
||||
if config.FirstLoginCredential.TwoFA != "" {
|
||||
err := c.Auth2FA(ctx, proton.Auth2FAReq{
|
||||
TwoFactorCode: config.FirstLoginCredential.TwoFA,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, nil, nil, nil, nil, Err2FACodeRequired
|
||||
}
|
||||
}
|
||||
|
||||
// decrypt keyring
|
||||
var saltedKeyPassByteArr []byte
|
||||
userKR, addrKRs, addr, saltedKeyPassByteArr, err = getAccountKRs(ctx, c, []byte(password), nil)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
saltedKeyPass := base64.StdEncoding.EncodeToString(saltedKeyPassByteArr)
|
||||
config.ReusableCredential.UID = auth.UID
|
||||
config.ReusableCredential.AccessToken = auth.AccessToken
|
||||
config.ReusableCredential.RefreshToken = auth.RefreshToken
|
||||
config.ReusableCredential.SaltedKeyPass = saltedKeyPass
|
||||
|
||||
err = cacheCredentialToFile(config)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return m, c, userKR, addrKRs, addr, nil
|
||||
}
|
||||
|
||||
func Logout(ctx context.Context, config *Config, m *proton.Manager, c *proton.Client, userKR *crypto.KeyRing, addrKRs map[string]*crypto.KeyRing) error {
|
||||
defer m.Close()
|
||||
defer c.Close()
|
||||
|
||||
if config.CredentialCacheFile == "" {
|
||||
log.Println("Logging out user")
|
||||
|
||||
// log out
|
||||
err := c.AuthDelete(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// clear keyrings
|
||||
userKR.ClearPrivateParams()
|
||||
|
||||
for i := range addrKRs {
|
||||
addrKRs[i].ClearPrivateParams()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
135
crypto.go
Normal file
135
crypto.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package proton_api_bridge
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"io"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/gopenpgp/v2/helper"
|
||||
)
|
||||
|
||||
func generatePassphrase() (string, error) {
|
||||
token, err := crypto.RandomToken(32)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tokenBase64 := base64.StdEncoding.EncodeToString(token)
|
||||
return tokenBase64, nil
|
||||
}
|
||||
|
||||
func generateCryptoKey() (string, string, error) {
|
||||
passphrase, err := generatePassphrase()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// all hardcoded values from iOS drive
|
||||
key, err := helper.GenerateKey("Drive key", "noreply@protonmail.com", []byte(passphrase), "x25519", 0)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return passphrase, key, nil
|
||||
}
|
||||
|
||||
// taken from Proton Go API Backend
|
||||
func encryptWithSignature(kr, addrKR *crypto.KeyRing, b []byte) (string, string, error) {
|
||||
enc, err := kr.Encrypt(crypto.NewPlainMessage(b), nil)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
encArm, err := enc.GetArmored()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
sig, err := addrKR.SignDetached(crypto.NewPlainMessage(b))
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
sigArm, err := sig.GetArmored()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return encArm, sigArm, nil
|
||||
}
|
||||
|
||||
func generateNodeKeys(kr, addrKR *crypto.KeyRing) (string, string, string, error) {
|
||||
nodePassphrase, nodeKey, err := generateCryptoKey()
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
nodePassphraseEnc, nodePassphraseSignature, err := encryptWithSignature(kr, addrKR, []byte(nodePassphrase))
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
return nodeKey, nodePassphraseEnc, nodePassphraseSignature, nil
|
||||
}
|
||||
|
||||
func getKeyRing(kr, addrKR *crypto.KeyRing, key, passphrase, passphraseSignature string) (*crypto.KeyRing, error) {
|
||||
enc, err := crypto.NewPGPMessageFromArmored(passphrase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dec, err := kr.Decrypt(enc, nil, crypto.GetUnixTime())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sig, err := crypto.NewPGPSignatureFromArmored(passphraseSignature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := addrKR.VerifyDetached(dec, sig, crypto.GetUnixTime()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lockedKey, err := crypto.NewKeyFromArmored(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unlockedKey, err := lockedKey.Unlock(dec.GetBinary())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return crypto.NewKeyRing(unlockedKey)
|
||||
}
|
||||
|
||||
func decryptBlockIntoBuffer(sessionKey *crypto.SessionKey, addrKR, nodeKR *crypto.KeyRing, encSignature string, buffer io.ReaderFrom, block io.ReadCloser) error {
|
||||
data, err := io.ReadAll(block)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
plainMessage, err := sessionKey.Decrypt(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encSignatureArm, err := crypto.NewPGPMessageFromArmored(encSignature)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = addrKR.VerifyDetachedEncrypted(plainMessage, encSignatureArm, nodeKR, crypto.GetUnixTime())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = buffer.ReadFrom(plainMessage.NewReader())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
105
delete.go
Normal file
105
delete.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package proton_api_bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/henrybear327/go-proton-api"
|
||||
)
|
||||
|
||||
func (protonDrive *ProtonDrive) moveToTrash(ctx context.Context, parentLinkID string, linkIDs ...string) error {
|
||||
/*
|
||||
Assumption:
|
||||
- only operates on main share
|
||||
*/
|
||||
err := protonDrive.c.TrashChildren(ctx, protonDrive.MainShare.ShareID, parentLinkID, linkIDs...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (protonDrive *ProtonDrive) MoveFileToTrashByID(ctx context.Context, linkID string) error {
|
||||
fileLink, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, linkID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fileLink.Type != proton.LinkTypeFile {
|
||||
return ErrLinkTypeMustToBeFolderType
|
||||
}
|
||||
|
||||
return protonDrive.moveToTrash(ctx, fileLink.ParentLinkID, linkID)
|
||||
}
|
||||
|
||||
func (protonDrive *ProtonDrive) MoveFolderToTrashByID(ctx context.Context, linkID string, onlyOnEmpty bool) error {
|
||||
folderLink, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, linkID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if folderLink.Type != proton.LinkTypeFolder {
|
||||
return ErrLinkTypeMustToBeFolderType
|
||||
}
|
||||
|
||||
childrenLinks, err := protonDrive.c.ListChildren(ctx, protonDrive.MainShare.ShareID, linkID, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if onlyOnEmpty {
|
||||
if len(childrenLinks) > 0 {
|
||||
return ErrFolderIsNotEmpty
|
||||
}
|
||||
}
|
||||
|
||||
return protonDrive.moveToTrash(ctx, folderLink.ParentLinkID, linkID)
|
||||
}
|
||||
|
||||
// WARNING!!!!
|
||||
// Everything in the root folder will be moved to trash
|
||||
// Most likely only used for debugging when the key is messed up
|
||||
func (protonDrive *ProtonDrive) EmptyRootFolder(ctx context.Context) error {
|
||||
links, err := protonDrive.c.ListChildren(ctx, protonDrive.MainShare.ShareID, protonDrive.MainShare.LinkID, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
{
|
||||
linkIDs := make([]string, 0)
|
||||
for i := range links {
|
||||
if links[i].State == proton.LinkStateActive /* use TrashChildren */ {
|
||||
linkIDs = append(linkIDs, links[i].LinkID)
|
||||
}
|
||||
}
|
||||
|
||||
err := protonDrive.c.TrashChildren(ctx, protonDrive.MainShare.ShareID, protonDrive.MainShare.LinkID, linkIDs...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
linkIDs := make([]string, 0)
|
||||
for i := range links {
|
||||
if links[i].State != proton.LinkStateActive {
|
||||
linkIDs = append(linkIDs, links[i].LinkID)
|
||||
}
|
||||
}
|
||||
|
||||
err := protonDrive.c.DeleteChildren(ctx, protonDrive.MainShare.ShareID, protonDrive.MainShare.LinkID, linkIDs...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Empty the trash
|
||||
func (protonDrive *ProtonDrive) EmptyTrash(ctx context.Context) error {
|
||||
err := protonDrive.c.EmptyTrash(ctx, protonDrive.MainShare.ShareID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
162
drive.go
Normal file
162
drive.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package proton_api_bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/henrybear327/Proton-API-Bridge/common"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/henrybear327/go-proton-api"
|
||||
)
|
||||
|
||||
type ProtonDrive struct {
|
||||
MainShare *proton.Share
|
||||
RootLink *proton.Link
|
||||
|
||||
MainShareKR *crypto.KeyRing
|
||||
AddrKR *crypto.KeyRing
|
||||
|
||||
Config *common.Config
|
||||
|
||||
c *proton.Client
|
||||
m *proton.Manager
|
||||
userKR *crypto.KeyRing
|
||||
addrKRs map[string]*crypto.KeyRing
|
||||
addrData []proton.Address
|
||||
signatureAddress string
|
||||
}
|
||||
|
||||
func NewDefaultConfig() *common.Config {
|
||||
return common.NewConfigWithDefaultValues()
|
||||
}
|
||||
|
||||
func NewProtonDrive(ctx context.Context, config *common.Config) (*ProtonDrive, error) {
|
||||
/* Log in and logout */
|
||||
m, c, userKR, addrKRs, addrData, err := common.Login(ctx, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
/*
|
||||
Current understanding (at the time of the commit)
|
||||
|
||||
The volume is the mount point.
|
||||
|
||||
A link is like a folder in POSIX.
|
||||
|
||||
A share is associated with a link to represent the access control,
|
||||
and serves as an entry point to a location in the file structure (Volume).
|
||||
It points to a link, of file or folder type, anywhere in the tree and holds a key called the ShareKey.
|
||||
To access a link, of file or folder type, a user must be a member of a share.
|
||||
|
||||
A volume has a default share for access control and is owned by the creator of the volume.
|
||||
A volume has a default link as it's root folder.
|
||||
|
||||
MIMETYPE holds type, e.g. folder, image/png, etc.
|
||||
*/
|
||||
volumes, err := listAllVolumes(ctx, c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// log.Printf("all volumes %#v", volumes)
|
||||
|
||||
mainShareID := ""
|
||||
for i := range volumes {
|
||||
// iOS drive: first active volume
|
||||
if volumes[i].State == proton.VolumeStateActive {
|
||||
mainShareID = volumes[i].Share.ShareID
|
||||
}
|
||||
}
|
||||
// log.Println("total volumes", len(volumes), "mainShareID", mainShareID)
|
||||
|
||||
/* Get root folder from the main share of the volume */
|
||||
mainShare, err := getShareByID(ctx, c, mainShareID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// check for main share integrity
|
||||
{
|
||||
mainShareCheck := false
|
||||
shares, err := getAllShares(ctx, c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range shares {
|
||||
if shares[i].ShareID == mainShare.ShareID &&
|
||||
shares[i].LinkID == mainShare.LinkID &&
|
||||
shares[i].Flags == proton.PrimaryShare &&
|
||||
shares[i].Type == proton.ShareTypeMain {
|
||||
mainShareCheck = true
|
||||
}
|
||||
}
|
||||
|
||||
if !mainShareCheck {
|
||||
log.Printf("mainShare %#v", mainShare)
|
||||
log.Printf("shares %#v", shares)
|
||||
return nil, ErrMainSharePreconditionsFailed
|
||||
}
|
||||
}
|
||||
|
||||
// Note: rootLink's parentLinkID == ""
|
||||
/*
|
||||
Link holds the tree structure, for the clients, they represent the files and folders of a given volume.
|
||||
They have a ParentLinkID that points to parent folders.
|
||||
Links also hold the file name (encrypted) and a hash of the name for name collisions.
|
||||
Link data is encrypted with its owning Share keyring.
|
||||
*/
|
||||
rootLink, err := c.GetLink(ctx, mainShare.ShareID, mainShare.LinkID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// log.Printf("rootLink %#v", rootLink)
|
||||
|
||||
// log.Printf("addrKRs %#v", addrKRs)=
|
||||
addrKR := addrKRs[mainShare.AddressID]
|
||||
// log.Println("addrKR CountDecryptionEntities", addrKR.CountDecryptionEntities())
|
||||
|
||||
mainShareKR, err := mainShare.GetKeyRing(addrKR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// log.Println("mainShareKR CountDecryptionEntities", mainShareKR.CountDecryptionEntities())
|
||||
|
||||
return &ProtonDrive{
|
||||
MainShare: mainShare,
|
||||
RootLink: &rootLink,
|
||||
|
||||
MainShareKR: mainShareKR,
|
||||
AddrKR: addrKR,
|
||||
|
||||
Config: config,
|
||||
|
||||
c: c,
|
||||
m: m,
|
||||
userKR: userKR,
|
||||
addrKRs: addrKRs,
|
||||
addrData: addrData,
|
||||
signatureAddress: mainShare.Creator,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (protonDrive *ProtonDrive) Logout(ctx context.Context) error {
|
||||
return common.Logout(ctx, protonDrive.Config, protonDrive.m, protonDrive.c, protonDrive.userKR, protonDrive.addrKRs)
|
||||
}
|
||||
|
||||
func (protonDrive *ProtonDrive) About(ctx context.Context) (*proton.User, error) {
|
||||
user, err := protonDrive.c.GetUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (protonDrive *ProtonDrive) GetLink(ctx context.Context, linkID string) (*proton.Link, error) {
|
||||
link, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, linkID)
|
||||
return &link, err
|
||||
}
|
||||
547
drive_test.go
Normal file
547
drive_test.go
Normal file
@@ -0,0 +1,547 @@
|
||||
package proton_api_bridge
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/henrybear327/Proton-API-Bridge/common"
|
||||
"github.com/henrybear327/Proton-API-Bridge/utility"
|
||||
)
|
||||
|
||||
/* Helper functions */
|
||||
func setup(t *testing.T) (context.Context, context.CancelFunc, *ProtonDrive) {
|
||||
utility.SetupLog()
|
||||
|
||||
config := common.NewConfigForIntegrationTests()
|
||||
|
||||
{
|
||||
// pre-condition check
|
||||
if config.DestructiveIntegrationTest == false {
|
||||
t.Fatalf("CAUTION: the integration test requires a clean proton drive")
|
||||
}
|
||||
if config.EmptyTrashAfterIntegrationTest == false {
|
||||
t.Fatalf("CAUTION: the integration test requires cleaning up the drive after running the tests")
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
protonDrive, err := NewProtonDrive(ctx, config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = protonDrive.EmptyRootFolder(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = protonDrive.EmptyTrash(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return ctx, cancel, protonDrive
|
||||
}
|
||||
|
||||
func tearDown(t *testing.T, ctx context.Context, protonDrive *ProtonDrive) {
|
||||
if protonDrive.Config.EmptyTrashAfterIntegrationTest {
|
||||
err := protonDrive.EmptyTrash(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Integration Tests */
|
||||
func TestCreateAndDeleteFolderAtRoot(t *testing.T) {
|
||||
ctx, cancel, protonDrive := setup(t)
|
||||
t.Cleanup(func() {
|
||||
defer cancel()
|
||||
defer tearDown(t, ctx, protonDrive)
|
||||
})
|
||||
|
||||
{
|
||||
/* Create folder tmp */
|
||||
_, err := protonDrive.CreateNewFolderByID(ctx, protonDrive.RootLink.LinkID, "tmp")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
paths := make([]string, 0)
|
||||
err = protonDrive.ListDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, true, "", &paths)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(paths) != 1 {
|
||||
t.Fatalf("Total path returned is differs from expected: %#v", paths)
|
||||
}
|
||||
if paths[0] != "/tmp" {
|
||||
t.Fatalf("Wrong folder created")
|
||||
}
|
||||
|
||||
paths = make([]string, 0)
|
||||
err = protonDrive.ListDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, false, "", &paths)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(paths) != 2 {
|
||||
t.Fatalf("Total path returned is differs from expected: %#v", paths)
|
||||
}
|
||||
if paths[0] != "/root" {
|
||||
t.Fatalf("Wrong root folder")
|
||||
}
|
||||
if paths[1] != "/root/tmp" {
|
||||
t.Fatalf("Wrong folder created")
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
/* Delete folder tmp */
|
||||
targetFolderLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, "tmp", true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if targetFolderLink == nil {
|
||||
t.Fatalf("Folder tmp not found")
|
||||
} else {
|
||||
err = protonDrive.MoveFolderToTrashByID(ctx, targetFolderLink.LinkID, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
paths := make([]string, 0)
|
||||
err = protonDrive.ListDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, false, "", &paths)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(paths) != 1 {
|
||||
t.Fatalf("Total path returned is differs from expected: %#v", paths)
|
||||
}
|
||||
if paths[0] != "/root" {
|
||||
t.Fatalf("Wrong root folder")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadAndDownloadAndDeleteAFileAtRoot(t *testing.T) {
|
||||
ctx, cancel, protonDrive := setup(t)
|
||||
t.Cleanup(func() {
|
||||
defer cancel()
|
||||
defer tearDown(t, ctx, protonDrive)
|
||||
})
|
||||
|
||||
{
|
||||
/* Upload a file integrationTestImage.png */
|
||||
f, err := os.Open("testcase/integrationTestImage.png")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
info, err := os.Stat("testcase/integrationTestImage.png")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
in := bufio.NewReader(f)
|
||||
|
||||
_, err = protonDrive.UploadFileByReader(ctx, protonDrive.RootLink.LinkID, "integrationTestImage.png", info.ModTime(), in)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
paths := make([]string, 0)
|
||||
err = protonDrive.ListDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, true, "", &paths)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(paths) != 1 {
|
||||
t.Fatalf("Total path returned is not as expected: %#v", paths)
|
||||
}
|
||||
if paths[0] != "/integrationTestImage.png" {
|
||||
t.Fatalf("Wrong file name decrypted")
|
||||
}
|
||||
|
||||
paths = make([]string, 0)
|
||||
err = protonDrive.ListDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, false, "", &paths)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(paths) != 2 {
|
||||
t.Fatalf("Total path returned is differs from expected: %#v", paths)
|
||||
}
|
||||
if paths[0] != "/root" {
|
||||
t.Fatalf("Wrong root folder")
|
||||
}
|
||||
if paths[1] != "/root/integrationTestImage.png" {
|
||||
t.Fatalf("Wrong file name decrypted")
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
/* Download a file integrationTestImage.png */
|
||||
targetFileLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, "integrationTestImage.png", false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if targetFileLink == nil {
|
||||
t.Fatalf("File integrationTestImage.png not found")
|
||||
} else {
|
||||
{
|
||||
_, err := protonDrive.SearchByNameInFolder(ctx, targetFileLink, "integrationTestImage.png", true, false)
|
||||
if err != ErrLinkTypeMustToBeFolderType {
|
||||
t.Fatalf("Wrong error message being returned")
|
||||
}
|
||||
}
|
||||
|
||||
downloadedData, err := protonDrive.DownloadFileByID(ctx, targetFileLink.LinkID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
originalData, err := os.ReadFile("testcase/integrationTestImage.png")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if bytes.Equal(downloadedData, originalData) == false {
|
||||
t.Fatalf("Downloaded content is different from the original content")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
/* TODO: Check file metadata */
|
||||
}
|
||||
|
||||
{
|
||||
/* Delete a file integrationTestImage.png */
|
||||
targetFileLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, "integrationTestImage.png", false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if targetFileLink == nil {
|
||||
t.Fatalf("File integrationTestImage.png not found")
|
||||
} else {
|
||||
err = protonDrive.MoveFileToTrashByID(ctx, targetFileLink.LinkID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
paths := make([]string, 0)
|
||||
err = protonDrive.ListDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, false, "", &paths)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(paths) != 1 {
|
||||
t.Fatalf("Total path returned is differs from expected: %#v", paths)
|
||||
}
|
||||
if paths[0] != "/root" {
|
||||
t.Fatalf("Wrong root folder")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadAndDeleteAnEmptyFileAtRoot(t *testing.T) {
|
||||
ctx, cancel, protonDrive := setup(t)
|
||||
t.Cleanup(func() {
|
||||
defer cancel()
|
||||
defer tearDown(t, ctx, protonDrive)
|
||||
})
|
||||
|
||||
{
|
||||
/* Upload a file integrationTestImage.png */
|
||||
_, err := protonDrive.UploadFileByPath(ctx, protonDrive.RootLink, "empty.txt", "testcase/empty.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
paths := make([]string, 0)
|
||||
err = protonDrive.ListDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, true, "", &paths)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(paths) != 1 {
|
||||
t.Fatalf("Total path returned is differs from expected: %#v", paths)
|
||||
}
|
||||
if paths[0] != "/empty.txt" {
|
||||
t.Fatalf("Wrong file name decrypted")
|
||||
}
|
||||
|
||||
paths = make([]string, 0)
|
||||
err = protonDrive.ListDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, false, "", &paths)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(paths) != 2 {
|
||||
t.Fatalf("Total path returned is differs from expected: %#v", paths)
|
||||
}
|
||||
if paths[0] != "/root" {
|
||||
t.Fatalf("Wrong root folder")
|
||||
}
|
||||
if paths[1] != "/root/empty.txt" {
|
||||
t.Fatalf("Wrong file name decrypted")
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
/* Download a file empty.txt */
|
||||
targetFileLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, "empty.txt", false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if targetFileLink == nil {
|
||||
t.Fatalf("File empty.txt not found")
|
||||
} else {
|
||||
downloadedData, err := protonDrive.DownloadFileByID(ctx, targetFileLink.LinkID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
originalData, err := os.ReadFile("testcase/empty.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if bytes.Equal(downloadedData, originalData) == false {
|
||||
t.Fatalf("Downloaded content is different from the original content")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
/* TODO: Check file metadata */
|
||||
}
|
||||
|
||||
{
|
||||
/* Delete a file empty.txt */
|
||||
targetFileLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, "empty.txt", false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if targetFileLink == nil {
|
||||
t.Fatalf("File empty.txt not found")
|
||||
} else {
|
||||
err = protonDrive.MoveFileToTrashByID(ctx, targetFileLink.LinkID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
paths := make([]string, 0)
|
||||
err = protonDrive.ListDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, false, "", &paths)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(paths) != 1 {
|
||||
t.Fatalf("Total path returned is differs from expected: %#v", paths)
|
||||
}
|
||||
if paths[0] != "/root" {
|
||||
t.Fatalf("Wrong root folder")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadAndDownloadAndDeleteAFileAtAFolderOneLevelFromRoot(t *testing.T) {
|
||||
ctx, cancel, protonDrive := setup(t)
|
||||
t.Cleanup(func() {
|
||||
defer cancel()
|
||||
defer tearDown(t, ctx, protonDrive)
|
||||
})
|
||||
|
||||
{
|
||||
/* Upload a file integrationTestImage.png */
|
||||
_, err := protonDrive.CreateNewFolder(ctx, protonDrive.RootLink, "tmp")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
targetFolderLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, "tmp", true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if targetFolderLink == nil {
|
||||
t.Fatalf("Folder tmp not found")
|
||||
}
|
||||
_, err = protonDrive.UploadFileByPath(ctx, targetFolderLink, "integrationTestImage.png", "testcase/integrationTestImage.png")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
paths := make([]string, 0)
|
||||
err = protonDrive.ListDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, true, "", &paths)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(paths) != 2 {
|
||||
t.Fatalf("Total path returned is differs from expected: %#v", paths)
|
||||
}
|
||||
if paths[0] != "/tmp" {
|
||||
t.Fatalf("Wrong folder name decrypted")
|
||||
}
|
||||
if paths[1] != "/tmp/integrationTestImage.png" {
|
||||
t.Fatalf("Wrong file name decrypted")
|
||||
}
|
||||
|
||||
paths = make([]string, 0)
|
||||
err = protonDrive.ListDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, false, "", &paths)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(paths) != 3 {
|
||||
t.Fatalf("Total path returned is differs from expected: %#v", paths)
|
||||
}
|
||||
if paths[0] != "/root" {
|
||||
t.Fatalf("Wrong root folder")
|
||||
}
|
||||
if paths[1] != "/root/tmp" {
|
||||
t.Fatalf("Wrong folder name decrypted")
|
||||
}
|
||||
if paths[2] != "/root/tmp/integrationTestImage.png" {
|
||||
t.Fatalf("Wrong file name decrypted")
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
/* Download a file integrationTestImage.png */
|
||||
targetFileLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, "integrationTestImage.png", false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
{
|
||||
targetFolderLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, "tmp", true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if targetFolderLink == nil {
|
||||
t.Fatalf("Folder tmp not found")
|
||||
} else {
|
||||
fileLink, err := protonDrive.SearchByNameInFolder(ctx, targetFolderLink, "integrationTestImage.png", true, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if fileLink.LinkID != targetFileLink.LinkID {
|
||||
t.Fatalf("Wrong file being returned")
|
||||
}
|
||||
}
|
||||
|
||||
targetFileLink2, err := protonDrive.SearchByNameRecursively(ctx, targetFolderLink, "integrationTestImage.png", false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if targetFileLink.LinkID != targetFileLink2.LinkID {
|
||||
t.Fatalf("SearchByNameRecursively is broken")
|
||||
}
|
||||
}
|
||||
|
||||
if targetFileLink == nil {
|
||||
t.Fatalf("File integrationTestImage.png not found")
|
||||
} else {
|
||||
downloadedData, err := protonDrive.DownloadFile(ctx, targetFileLink)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
originalData, err := os.ReadFile("testcase/integrationTestImage.png")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if bytes.Equal(downloadedData, originalData) == false {
|
||||
t.Fatalf("Downloaded content is different from the original content")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
/* TODO: Check file metadata */
|
||||
}
|
||||
|
||||
{
|
||||
/* Delete a file integrationTestImage.png */
|
||||
targetFileLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, "integrationTestImage.png", false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if targetFileLink == nil {
|
||||
t.Fatalf("File integrationTestImage.png not found")
|
||||
} else {
|
||||
err = protonDrive.MoveFileToTrashByID(ctx, targetFileLink.LinkID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
paths := make([]string, 0)
|
||||
err = protonDrive.ListDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, false, "", &paths)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(paths) != 2 {
|
||||
t.Fatalf("Total path returned is differs from expected: %#v", paths)
|
||||
}
|
||||
if paths[0] != "/root" {
|
||||
t.Fatalf("Wrong root folder")
|
||||
}
|
||||
if paths[1] != "/root/tmp" {
|
||||
t.Fatalf("Wrong tmp folder")
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
/* Delete a folder tmp */
|
||||
targetFolderLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, "tmp", true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if targetFolderLink == nil {
|
||||
t.Fatalf("Folder tmp not found")
|
||||
} else {
|
||||
err = protonDrive.MoveFolderToTrashByID(ctx, targetFolderLink.LinkID, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
paths := make([]string, 0)
|
||||
err = protonDrive.ListDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, false, "", &paths)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(paths) != 1 {
|
||||
t.Fatalf("Total path returned is differs from expected: %#v", paths)
|
||||
}
|
||||
if paths[0] != "/root" {
|
||||
t.Fatalf("Wrong root folder")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
TODO
|
||||
- Revision
|
||||
- Rename
|
||||
- Move
|
||||
*/
|
||||
11
error.go
Normal file
11
error.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package proton_api_bridge
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrMainSharePreconditionsFailed = errors.New("the main share assumption has failed")
|
||||
ErrDataFolderNameIsEmpty = errors.New("please supply a DataFolderName to enabling file downloading")
|
||||
ErrLinkTypeMustToBeFolderType = errors.New("the link type must be of folder type")
|
||||
ErrLinkTypeMustToBeFileType = errors.New("the link type must be of file type")
|
||||
ErrFolderIsNotEmpty = errors.New("folder can't be deleted becuase it is not empty")
|
||||
)
|
||||
404
file.go
Normal file
404
file.go
Normal file
@@ -0,0 +1,404 @@
|
||||
package proton_api_bridge
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/henrybear327/go-proton-api"
|
||||
)
|
||||
|
||||
func (protonDrive *ProtonDrive) DownloadFileByID(ctx context.Context, linkID string) ([]byte, error) {
|
||||
link, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, linkID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return protonDrive.DownloadFile(ctx, &link)
|
||||
}
|
||||
|
||||
func (protonDrive *ProtonDrive) DownloadFile(ctx context.Context, link *proton.Link) ([]byte, error) {
|
||||
if link.Type != proton.LinkTypeFile {
|
||||
return nil, ErrLinkTypeMustToBeFileType
|
||||
}
|
||||
|
||||
parentNodeKR, err := protonDrive.getNodeKRByID(ctx, link.ParentLinkID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nodeKR, err := link.GetKeyRing(parentNodeKR, protonDrive.AddrKR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sessionKey, err := link.GetSessionKey(protonDrive.AddrKR, nodeKR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
revisions, err := protonDrive.c.ListRevisions(ctx, protonDrive.MainShare.ShareID, link.LinkID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// log.Printf("revisions %#v", revisions)
|
||||
|
||||
// Revisions are only for files, they represent “versions” of files.
|
||||
// Each file can have 1 active revision and n obsolete revisions.
|
||||
activeRevision := -1
|
||||
for i := range revisions {
|
||||
if revisions[i].State == proton.RevisionStateActive {
|
||||
activeRevision = i
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: compute total blocks required
|
||||
// TODO: handle large file downloading
|
||||
revision, err := protonDrive.c.GetRevision(ctx, protonDrive.MainShare.ShareID, link.LinkID, revisions[activeRevision].ID, 1, 50)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
for i := range revision.Blocks {
|
||||
// parallel download
|
||||
blockReader, err := protonDrive.c.GetBlock(ctx, revision.Blocks[i].BareURL, revision.Blocks[i].Token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer blockReader.Close()
|
||||
|
||||
err = decryptBlockIntoBuffer(sessionKey, protonDrive.AddrKR, nodeKR, revision.Blocks[i].EncSignature, buffer, blockReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func (protonDrive *ProtonDrive) UploadFileByReader(ctx context.Context, parentLinkID string, filename string, modTime time.Time, file io.Reader) (*proton.Link, error) {
|
||||
parentLink, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, parentLinkID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return protonDrive.uploadFile(ctx, &parentLink, filename, time.Now() /* FIXME */, file)
|
||||
}
|
||||
|
||||
func (protonDrive *ProtonDrive) UploadFileByPath(ctx context.Context, parentLink *proton.Link, filename string, filePath string) (*proton.Link, error) {
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
info, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
in := bufio.NewReader(f)
|
||||
|
||||
return protonDrive.uploadFile(ctx, parentLink, filename, info.ModTime(), in)
|
||||
}
|
||||
|
||||
func (protonDrive *ProtonDrive) uploadFile(ctx context.Context, parentLink *proton.Link, filename string, modTime time.Time, file io.Reader) (*proton.Link, error) {
|
||||
// FIXME: check iOS: optimize for large files -> enc blocks on the fly
|
||||
/*
|
||||
Assumptions:
|
||||
- Upload is always done to the mainShare
|
||||
*/
|
||||
// TODO: check for duplicated filename by using checkAvailableHashes
|
||||
|
||||
parentNodeKR, err := protonDrive.getNodeKR(ctx, parentLink)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// detect MIME type
|
||||
fileContent, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mimetype.SetLimit(0)
|
||||
mType := mimetype.Detect(fileContent)
|
||||
mimeType := mType.String()
|
||||
// log.Println("Detected MIME type", mimeType)
|
||||
|
||||
/* step 1: create a draft */
|
||||
newNodeKey, newNodePassphraseEnc, newNodePassphraseSignature, err := generateNodeKeys(parentNodeKR, protonDrive.AddrKR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
createFileReq := proton.CreateFileReq{
|
||||
ParentLinkID: parentLink.LinkID,
|
||||
|
||||
// Name string // Encrypted File Name
|
||||
// Hash string // Encrypted File Name hash
|
||||
MIMEType: mimeType, // MIME Type
|
||||
|
||||
// ContentKeyPacket string // The block's key packet, encrypted with the node key.
|
||||
// ContentKeyPacketSignature string // Unencrypted signature of the content session key, signed with the NodeKey
|
||||
|
||||
NodeKey: newNodeKey, // The private NodeKey, used to decrypt any file/folder content.
|
||||
NodePassphrase: newNodePassphraseEnc, // The passphrase used to unlock the NodeKey, encrypted by the owning Link/Share keyring.
|
||||
NodePassphraseSignature: newNodePassphraseSignature, // The signature of the NodePassphrase
|
||||
|
||||
ModifyTime: modTime.Unix(), // The modified time
|
||||
|
||||
SignatureAddress: protonDrive.signatureAddress, // Signature email address used to sign passphrase and name
|
||||
}
|
||||
|
||||
/* Name is encrypted using the parent's keyring, and signed with address key */
|
||||
err = createFileReq.SetName(filename, protonDrive.AddrKR, parentNodeKR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parentHashKey, err := parentLink.GetHashKey(parentNodeKR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newNodeKR, err := getKeyRing(parentNodeKR, protonDrive.AddrKR, newNodeKey, newNodePassphraseEnc, newNodePassphraseSignature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = createFileReq.SetHash(filename, parentHashKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = createFileReq.SetContentKeyPacketAndSignature(newNodeKR, protonDrive.AddrKR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
createFileResp, err := protonDrive.c.CreateFile(ctx, protonDrive.MainShare.ShareID, createFileReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(fileContent) == 0 {
|
||||
/* step 2 [Skipped]: upload blocks and collect block data */
|
||||
|
||||
/* step 3: mark the file as active by updating the revision */
|
||||
manifestSignatureData := make([]byte, 0)
|
||||
manifestSignature, err := protonDrive.AddrKR.SignDetached(crypto.NewPlainMessage(manifestSignatureData))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
manifestSignatureString, err := manifestSignature.GetArmored()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = protonDrive.c.UpdateRevision(ctx, protonDrive.MainShare.ShareID, createFileResp.ID, createFileResp.RevisionID, proton.UpdateRevisionReq{
|
||||
BlockList: make([]proton.BlockToken, 0),
|
||||
State: proton.RevisionStateActive,
|
||||
ManifestSignature: manifestSignatureString,
|
||||
SignatureAddress: protonDrive.signatureAddress,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
/* step 2: upload blocks and collect block data */
|
||||
// FIXME: handle partial upload (failed midway)
|
||||
|
||||
// FIXME: get block size
|
||||
blockSize := 4 * 1024 * 1024
|
||||
type PendingUploadBlocks struct {
|
||||
blockUploadInfo proton.BlockUploadInfo
|
||||
encData []byte
|
||||
}
|
||||
blocks := make([]PendingUploadBlocks, 0)
|
||||
manifestSignatureData := make([]byte, 0)
|
||||
sessionKey, err := func() (*crypto.SessionKey, error) {
|
||||
keyPacket := createFileReq.ContentKeyPacket
|
||||
keyPacketByteArr, err := base64.StdEncoding.DecodeString(keyPacket)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sessionKey, err := newNodeKR.DecryptSessionKey(keyPacketByteArr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// FIXME: verify the signature of the session key
|
||||
// signatureString, err := crypto.NewPGPMessageFromArmored(createFileReq.ContentKeyPacketSignature)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
// err = protonDrive.AddrKR.VerifyDetachedEncrypted(crypto.NewPlainMessageFromString(sessionKey.GetBase64Key()), signatureString, newNodeKR, crypto.GetUnixTime())
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return sessionKey, nil
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := 0; i*blockSize < len(fileContent); i++ {
|
||||
// encrypt data
|
||||
upperBound := (i + 1) * blockSize
|
||||
if upperBound > len(fileContent) {
|
||||
upperBound = len(fileContent)
|
||||
}
|
||||
data := fileContent[i*blockSize : upperBound]
|
||||
|
||||
dataPlainMessage := crypto.NewPlainMessage(data)
|
||||
encData, err := sessionKey.Encrypt(dataPlainMessage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encSignature, err := protonDrive.AddrKR.SignDetachedEncrypted(dataPlainMessage, newNodeKR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encSignatureStr, err := encSignature.GetArmored()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
h.Write(encData)
|
||||
hash := h.Sum(nil)
|
||||
base64Hash := base64.StdEncoding.EncodeToString(hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
manifestSignatureData = append(manifestSignatureData, hash...)
|
||||
|
||||
blocks = append(blocks, PendingUploadBlocks{
|
||||
blockUploadInfo: proton.BlockUploadInfo{
|
||||
Index: i + 1, // iOS drive: BE starts with 1
|
||||
Size: int64(len(encData)),
|
||||
EncSignature: encSignatureStr,
|
||||
Hash: base64Hash,
|
||||
},
|
||||
encData: encData,
|
||||
})
|
||||
}
|
||||
|
||||
blockList := make([]proton.BlockUploadInfo, 0)
|
||||
for i := 0; i < len(blocks); i++ {
|
||||
blockList = append(blockList, blocks[i].blockUploadInfo)
|
||||
}
|
||||
blockTokens := make([]proton.BlockToken, 0)
|
||||
blockUploadReq := proton.BlockUploadReq{
|
||||
AddressID: protonDrive.MainShare.AddressID,
|
||||
ShareID: protonDrive.MainShare.ShareID,
|
||||
LinkID: createFileResp.ID,
|
||||
RevisionID: createFileResp.RevisionID,
|
||||
|
||||
BlockList: blockList,
|
||||
}
|
||||
blockUploadResp, err := protonDrive.c.RequestBlockUpload(ctx, blockUploadReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := range blockUploadResp {
|
||||
err := protonDrive.c.UploadBlock(ctx, blockUploadResp[i].BareURL, blockUploadResp[i].Token, bytes.NewReader(blocks[i].encData))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blockTokens = append(blockTokens, proton.BlockToken{
|
||||
Index: i + 1,
|
||||
Token: blockUploadResp[i].Token,
|
||||
})
|
||||
}
|
||||
|
||||
/* step 3: mark the file as active by updating the revision */
|
||||
|
||||
// TODO: check iOS Drive CommitableRevision
|
||||
manifestSignature, err := protonDrive.AddrKR.SignDetached(crypto.NewPlainMessage(manifestSignatureData))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
manifestSignatureString, err := manifestSignature.GetArmored()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = protonDrive.c.UpdateRevision(ctx, protonDrive.MainShare.ShareID, createFileResp.ID, createFileResp.RevisionID, proton.UpdateRevisionReq{
|
||||
BlockList: blockTokens,
|
||||
State: proton.RevisionStateActive,
|
||||
ManifestSignature: manifestSignatureString,
|
||||
SignatureAddress: protonDrive.signatureAddress,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
link, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, createFileResp.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &link, nil
|
||||
}
|
||||
|
||||
/*
|
||||
There is a route that proton-go-api doesn't have - checkAvailableHashes.
|
||||
This is used to quickly find the next available filename when the originally supplied filename is taken in the current folder.
|
||||
|
||||
Based on the code below, which is taken from the Proton iOS Drive app, we can infer that:
|
||||
- when a file is to be uploaded && there is filename conflict after the first upload:
|
||||
- on web, user will be prompted with a) overwrite b) keep both by appending filename with iteration number c) do nothing
|
||||
- on the iOS client logic, we can see that when the filename conflict happens (after the upload attampt failed)
|
||||
- the filename will be hashed by using filename + iteration
|
||||
- 10 iterations will be done per batch, each iteration's hash will be sent to the server
|
||||
- the server will return available hashes, and the client will take the lowest iteration as the filename to be used
|
||||
- will be used to search for the next available filename (using hashes avoids the filename being known to the server)
|
||||
|
||||
private func findNextAvailableName(for file: FileNameCheckerModel, offset: Int, completion: @escaping (Result<NameHashPair, Error>) -> Void) {
|
||||
assert(offset >= 0)
|
||||
let fileName = file.originalName.fileName()
|
||||
let `extension` = file.originalName.fileExtension()
|
||||
var possibleNamesHashPairs = [NameHashPair]()
|
||||
|
||||
let lowerBound = offset + 1
|
||||
let upperBound = offset + step
|
||||
|
||||
for iteration in lowerBound...upperBound {
|
||||
let newName = "\(fileName) (\(iteration))" + (`extension`.isEmpty ? "" : "." + `extension`)
|
||||
guard let newHash = try? hasher(newName, file.parentNodeHashKey) else { continue }
|
||||
possibleNamesHashPairs.append(NameHashPair(name: newName, hash: newHash))
|
||||
}
|
||||
|
||||
hashChecker.checkAvailableHashes(among: possibleNamesHashPairs, onFolder: file.parent) { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
|
||||
case .success(let approvedHashes) where approvedHashes.isEmpty:
|
||||
self.findNextAvailableName(for: file, offset: upperBound, completion: completion)
|
||||
|
||||
case .success(let approvedHashes):
|
||||
let approvedPair = possibleNamesHashPairs.first { approvedHashes.contains($0.hash) }!
|
||||
completion(.success(approvedPair))
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
236
folder.go
Normal file
236
folder.go
Normal file
@@ -0,0 +1,236 @@
|
||||
package proton_api_bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/henrybear327/go-proton-api"
|
||||
)
|
||||
|
||||
type ProtonDirectoryData struct {
|
||||
Link *proton.Link
|
||||
Name string
|
||||
IsFolder bool
|
||||
}
|
||||
|
||||
func (protonDrive *ProtonDrive) ListDirectory(
|
||||
ctx context.Context,
|
||||
folderLinkID string) ([]*ProtonDirectoryData, error) {
|
||||
ret := make([]*ProtonDirectoryData, 0)
|
||||
|
||||
folderLink, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, folderLinkID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if folderLink.State == proton.LinkStateActive {
|
||||
childrenLinks, err := protonDrive.c.ListChildren(ctx, protonDrive.MainShare.ShareID, folderLink.LinkID, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if childrenLinks != nil {
|
||||
folderParentKR, err := protonDrive.getNodeKRByID(ctx, folderLink.ParentLinkID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer folderParentKR.ClearPrivateParams()
|
||||
folderLinkKR, err := folderLink.GetKeyRing(folderParentKR, protonDrive.AddrKR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer folderLinkKR.ClearPrivateParams()
|
||||
|
||||
for i := range childrenLinks {
|
||||
if childrenLinks[i].State != proton.LinkStateActive {
|
||||
continue
|
||||
}
|
||||
|
||||
name, err := childrenLinks[i].GetName(folderLinkKR, protonDrive.AddrKR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret = append(ret, &ProtonDirectoryData{
|
||||
Link: &childrenLinks[i],
|
||||
Name: name,
|
||||
IsFolder: childrenLinks[i].Type == proton.LinkTypeFolder,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (protonDrive *ProtonDrive) ListDirectoriesRecursively(
|
||||
ctx context.Context,
|
||||
parentNodeKR *crypto.KeyRing,
|
||||
link *proton.Link,
|
||||
download bool,
|
||||
maxDepth, curDepth /* 0-based */ int,
|
||||
excludeRoot bool,
|
||||
pathSoFar string,
|
||||
paths *[]string) error {
|
||||
/*
|
||||
Assumptions:
|
||||
- we only care about the active ones
|
||||
- we only operate on the mainShare
|
||||
*/
|
||||
if link.State != proton.LinkStateActive {
|
||||
return nil
|
||||
}
|
||||
// log.Println("curDepth", curDepth, "pathSoFar", pathSoFar)
|
||||
|
||||
var currentPath = ""
|
||||
|
||||
if !(excludeRoot && curDepth == 0) {
|
||||
name, err := link.GetName(parentNodeKR, protonDrive.AddrKR)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentPath = pathSoFar + "/" + name
|
||||
// log.Println("currentPath", currentPath)
|
||||
if paths != nil {
|
||||
*paths = append(*paths, currentPath)
|
||||
}
|
||||
}
|
||||
|
||||
if download {
|
||||
if protonDrive.Config.DataFolderName == "" {
|
||||
return ErrDataFolderNameIsEmpty
|
||||
}
|
||||
|
||||
if link.Type == proton.LinkTypeFile {
|
||||
log.Println("Downloading", currentPath)
|
||||
defer log.Println("Completes downloading", currentPath)
|
||||
|
||||
byteArray, err := protonDrive.DownloadFile(ctx, link)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.WriteFile("./"+protonDrive.Config.DataFolderName+"/"+currentPath, byteArray, 0777)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else /* folder */ {
|
||||
if !(excludeRoot && curDepth == 0) {
|
||||
// log.Println("Creating folder", currentPath)
|
||||
// defer log.Println("Completes creating folder", currentPath)
|
||||
|
||||
err := os.Mkdir("./"+protonDrive.Config.DataFolderName+"/"+currentPath, 0777)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if maxDepth == -1 || curDepth < maxDepth {
|
||||
if link.Type == proton.LinkTypeFolder {
|
||||
childrenLinks, err := protonDrive.c.ListChildren(ctx, protonDrive.MainShare.ShareID, link.LinkID, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// log.Printf("childrenLinks len = %v, %#v", len(childrenLinks), childrenLinks)
|
||||
|
||||
if childrenLinks != nil {
|
||||
// get current node's keyring
|
||||
linkKR, err := link.GetKeyRing(parentNodeKR, protonDrive.AddrKR)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer linkKR.ClearPrivateParams()
|
||||
|
||||
for _, childLink := range childrenLinks {
|
||||
err = protonDrive.ListDirectoriesRecursively(ctx, linkKR, &childLink, download, maxDepth, curDepth+1, excludeRoot, currentPath, paths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (protonDrive *ProtonDrive) CreateNewFolderByID(ctx context.Context, parentLinkID string, folderName string) (string, error) {
|
||||
parentLink, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, parentLinkID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return protonDrive.CreateNewFolder(ctx, &parentLink, folderName)
|
||||
}
|
||||
|
||||
func (protonDrive *ProtonDrive) CreateNewFolder(ctx context.Context, parentLink *proton.Link, folderName string) (string, error) {
|
||||
/*
|
||||
Assumptions:
|
||||
- we only operate on the mainShare
|
||||
*/
|
||||
// TODO: check for duplicated folder name
|
||||
|
||||
parentNodeKR, err := protonDrive.getNodeKR(ctx, parentLink)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
newNodeKey, newNodePassphraseEnc, newNodePassphraseSignature, err := generateNodeKeys(parentNodeKR, protonDrive.AddrKR)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
createFolderReq := proton.CreateFolderReq{
|
||||
ParentLinkID: parentLink.LinkID,
|
||||
|
||||
// Name string
|
||||
// Hash string
|
||||
|
||||
NodeKey: newNodeKey,
|
||||
// NodeHashKey string
|
||||
|
||||
NodePassphrase: newNodePassphraseEnc,
|
||||
NodePassphraseSignature: newNodePassphraseSignature,
|
||||
|
||||
SignatureAddress: protonDrive.signatureAddress,
|
||||
}
|
||||
|
||||
/* Name is encrypted using the parent's keyring, and signed with address key */
|
||||
err = createFolderReq.SetName(folderName, protonDrive.AddrKR, parentNodeKR)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
parentHashKey, err := parentLink.GetHashKey(parentNodeKR)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
newNodeKR, err := getKeyRing(parentNodeKR, protonDrive.AddrKR, newNodeKey, newNodePassphraseEnc, newNodePassphraseSignature)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = createFolderReq.SetHash(folderName, parentHashKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = createFolderReq.SetNodeHashKey(newNodeKR)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
createFolderResp, err := protonDrive.c.CreateFolder(ctx, protonDrive.MainShare.ShareID, createFolderReq)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// log.Printf("createFolderResp %#v", createFolderResp)
|
||||
|
||||
return createFolderResp.ID, nil
|
||||
}
|
||||
35
go.mod
Normal file
35
go.mod
Normal file
@@ -0,0 +1,35 @@
|
||||
module github.com/henrybear327/Proton-API-Bridge
|
||||
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1
|
||||
github.com/gabriel-vasile/mimetype v1.4.2
|
||||
github.com/henrybear327/go-proton-api v0.0.0-20230623063450-66171214ea8c
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
|
||||
github.com/ProtonMail/gluon v0.16.1-0.20230526091020-fb7689b15ae3 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230619160724-3fbb1f12458c // indirect
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
|
||||
github.com/ProtonMail/go-srp v0.0.7 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.8.1 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/bradenaw/juniper v0.13.0 // indirect
|
||||
github.com/cloudflare/circl v1.3.3 // indirect
|
||||
github.com/cronokirby/saferith v0.33.0 // indirect
|
||||
github.com/emersion/go-message v0.16.0 // indirect
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
|
||||
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 // indirect
|
||||
github.com/go-resty/resty/v2 v2.7.0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
golang.org/x/crypto v0.10.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
|
||||
golang.org/x/net v0.11.0 // indirect
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
golang.org/x/sys v0.9.0 // indirect
|
||||
golang.org/x/text v0.10.0 // indirect
|
||||
)
|
||||
142
go.sum
Normal file
142
go.sum
Normal file
@@ -0,0 +1,142 @@
|
||||
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||
github.com/ProtonMail/gluon v0.16.1-0.20230526091020-fb7689b15ae3 h1:HsRC3WKWY2xf3OGfXnVn1S/EhJx/8dKrWX4/JJQIBc8=
|
||||
github.com/ProtonMail/gluon v0.16.1-0.20230526091020-fb7689b15ae3/go.mod h1:xYLE11dCH40RrNjkuncXZbYjGyuHKeFtdYKT2nkq6M8=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230619160724-3fbb1f12458c h1:figwFwYep1Qnl64Y+Rc8tyQWE0xvYAN+5EX+rD40pTU=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230619160724-3fbb1f12458c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
||||
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
||||
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1 h1:Awsg7MPc2gD3I7IFac2qE3Gdls0lZW8SzrFZ3k1oz0s=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1/go.mod h1:/BU5gfAVwqyd8EfC3Eu7zmuhwYQpKs+cGD8M//iiaxs=
|
||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/bradenaw/juniper v0.13.0 h1:KKMAiWDkRt45YUNzzw00Jec4nOgWDLVtztjf39E0ppI=
|
||||
github.com/bradenaw/juniper v0.13.0/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
|
||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
||||
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
|
||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||
github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo=
|
||||
github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emersion/go-message v0.16.0 h1:uZLz8ClLv3V5fSFF/fFdW9jXjrZkXIpE1Fn8fKx7pO4=
|
||||
github.com/emersion/go-message v0.16.0/go.mod h1:pDJDgf/xeUIF+eicT6B/hPX/ZbEorKkUMPOxrPVG2eQ=
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
||||
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 h1:hQ1wTMaKcGfobYRT88RM8NFNyX+IQHvagkm/tqViU98=
|
||||
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
|
||||
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/henrybear327/go-proton-api v0.0.0-20230623063450-66171214ea8c h1:9+c5SK3a9k98VpaZid0qDVnGGZ89YsGEz8KMJrwB0FE=
|
||||
github.com/henrybear327/go-proton-api v0.0.0-20230623063450-66171214ea8c/go.mod h1:l42xBSOrCmkAxzWUHcoUsG/cP8m1hMhV72GoChOX3bg=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
|
||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
51
keyring.go
Normal file
51
keyring.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package proton_api_bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/henrybear327/go-proton-api"
|
||||
)
|
||||
|
||||
func (protonDrive *ProtonDrive) getNodeKRByID(ctx context.Context, linkID string) (*crypto.KeyRing, error) {
|
||||
if linkID == "" {
|
||||
// most likely someone requested parent link, which happen to be ""
|
||||
return protonDrive.MainShareKR, nil
|
||||
}
|
||||
|
||||
link, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, linkID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return protonDrive.getNodeKR(ctx, &link)
|
||||
}
|
||||
|
||||
func (protonDrive *ProtonDrive) getNodeKR(ctx context.Context, link *proton.Link) (*crypto.KeyRing, error) {
|
||||
if link.ParentLinkID == "" {
|
||||
nodeKR, err := link.GetKeyRing(protonDrive.MainShareKR, protonDrive.AddrKR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nodeKR, nil
|
||||
}
|
||||
|
||||
parentLink, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, link.ParentLinkID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// parentNodeKR is used to decrypt the current node's KR, as each node has its keyring, which can be decrypted by its parent
|
||||
parentNodeKR, err := protonDrive.getNodeKR(ctx, &parentLink)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nodeKR, err := link.GetKeyRing(parentNodeKR, protonDrive.AddrKR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nodeKR, nil
|
||||
}
|
||||
181
search.go
Normal file
181
search.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package proton_api_bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/henrybear327/go-proton-api"
|
||||
)
|
||||
|
||||
/*
|
||||
Observation: file name is unique, since it's checked (by hash) on the server
|
||||
*/
|
||||
|
||||
func (protonDrive *ProtonDrive) SearchByNameRecursivelyFromRoot(ctx context.Context, targetName string, isFolder bool) (*proton.Link, error) {
|
||||
var linkType proton.LinkType
|
||||
if isFolder {
|
||||
linkType = proton.LinkTypeFolder
|
||||
} else {
|
||||
linkType = proton.LinkTypeFile
|
||||
}
|
||||
return protonDrive.searchByNameRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, targetName, linkType)
|
||||
}
|
||||
|
||||
func (protonDrive *ProtonDrive) SearchByNameRecursivelyByID(ctx context.Context, folderLinkID string, targetName string, isFolder bool) (*proton.Link, error) {
|
||||
folderLink, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, folderLinkID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var linkType proton.LinkType
|
||||
if isFolder {
|
||||
linkType = proton.LinkTypeFolder
|
||||
} else {
|
||||
linkType = proton.LinkTypeFile
|
||||
}
|
||||
|
||||
if folderLink.Type != proton.LinkTypeFolder {
|
||||
return nil, ErrLinkTypeMustToBeFolderType
|
||||
}
|
||||
folderKeyRing, err := protonDrive.getNodeKRByID(ctx, folderLink.ParentLinkID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return protonDrive.searchByNameRecursively(ctx, folderKeyRing, &folderLink, targetName, linkType)
|
||||
}
|
||||
|
||||
func (protonDrive *ProtonDrive) SearchByNameRecursively(ctx context.Context, folderLink *proton.Link, targetName string, isFolder bool) (*proton.Link, error) {
|
||||
var linkType proton.LinkType
|
||||
if isFolder {
|
||||
linkType = proton.LinkTypeFolder
|
||||
} else {
|
||||
linkType = proton.LinkTypeFile
|
||||
}
|
||||
|
||||
if folderLink.Type != proton.LinkTypeFolder {
|
||||
return nil, ErrLinkTypeMustToBeFolderType
|
||||
}
|
||||
folderKeyRing, err := protonDrive.getNodeKRByID(ctx, folderLink.ParentLinkID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return protonDrive.searchByNameRecursively(ctx, folderKeyRing, folderLink, targetName, linkType)
|
||||
}
|
||||
|
||||
func (protonDrive *ProtonDrive) searchByNameRecursively(
|
||||
ctx context.Context,
|
||||
parentNodeKR *crypto.KeyRing,
|
||||
link *proton.Link,
|
||||
targetName string,
|
||||
linkType proton.LinkType) (*proton.Link, error) {
|
||||
/*
|
||||
Assumptions:
|
||||
- we only care about the active ones
|
||||
- we only operate on the mainShare
|
||||
*/
|
||||
if link.State != proton.LinkStateActive {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
name, err := link.GetName(parentNodeKR, protonDrive.AddrKR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if link.Type == linkType && name == targetName {
|
||||
return link, nil
|
||||
}
|
||||
|
||||
if link.Type == proton.LinkTypeFolder {
|
||||
childrenLinks, err := protonDrive.c.ListChildren(ctx, protonDrive.MainShare.ShareID, link.LinkID, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// log.Printf("childrenLinks len = %v, %#v", len(childrenLinks), childrenLinks)
|
||||
|
||||
// get current node's keyring
|
||||
linkKR, err := link.GetKeyRing(parentNodeKR, protonDrive.AddrKR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer linkKR.ClearPrivateParams()
|
||||
|
||||
for _, childLink := range childrenLinks {
|
||||
ret, err := protonDrive.searchByNameRecursively(ctx, linkKR, &childLink, targetName, linkType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ret != nil {
|
||||
return ret, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// if the target isn't found, nil will be returned for both return values
|
||||
func (protonDrive *ProtonDrive) SearchByNameInFolderByID(ctx context.Context,
|
||||
folderLinkID string,
|
||||
targetName string,
|
||||
searchForFile, searchForFolder bool) (*proton.Link, error) {
|
||||
folderLink, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, folderLinkID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return protonDrive.SearchByNameInFolder(ctx, &folderLink, targetName, searchForFile, searchForFolder)
|
||||
}
|
||||
|
||||
func (protonDrive *ProtonDrive) SearchByNameInFolder(
|
||||
ctx context.Context,
|
||||
folderLink *proton.Link,
|
||||
targetName string,
|
||||
searchForFile, searchForFolder bool) (*proton.Link, error) {
|
||||
if !searchForFile && !searchForFolder {
|
||||
// nothing to search
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// we search all folders and files within this designated folder only
|
||||
if folderLink.Type != proton.LinkTypeFolder {
|
||||
return nil, ErrLinkTypeMustToBeFolderType
|
||||
}
|
||||
|
||||
parentNodeKR, err := protonDrive.getNodeKRByID(ctx, folderLink.ParentLinkID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get current node's keyring
|
||||
folderLinkKR, err := folderLink.GetKeyRing(parentNodeKR, protonDrive.AddrKR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer folderLinkKR.ClearPrivateParams()
|
||||
|
||||
childrenLinks, err := protonDrive.c.ListChildren(ctx, protonDrive.MainShare.ShareID, folderLink.LinkID, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, childLink := range childrenLinks {
|
||||
if childLink.State != proton.LinkStateActive {
|
||||
// we only search in the active folders
|
||||
continue
|
||||
}
|
||||
|
||||
name, err := childLink.GetName(folderLinkKR, protonDrive.AddrKR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if searchForFile && childLink.Type == proton.LinkTypeFile && name == targetName {
|
||||
return &childLink, nil
|
||||
} else if searchForFolder && childLink.Type == proton.LinkTypeFolder && name == targetName {
|
||||
return &childLink, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
25
shares.go
Normal file
25
shares.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package proton_api_bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/henrybear327/go-proton-api"
|
||||
)
|
||||
|
||||
func getAllShares(ctx context.Context, c *proton.Client) ([]proton.ShareMetadata, error) {
|
||||
shares, err := c.ListShares(ctx, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return shares, nil
|
||||
}
|
||||
|
||||
func getShareByID(ctx context.Context, c *proton.Client, shareID string) (*proton.Share, error) {
|
||||
share, err := c.GetShare(ctx, shareID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &share, nil
|
||||
}
|
||||
0
testcase/empty.txt
Normal file
0
testcase/empty.txt
Normal file
BIN
testcase/integrationTestImage.png
Normal file
BIN
testcase/integrationTestImage.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
9
utility/init.go
Normal file
9
utility/init.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package utility
|
||||
|
||||
import (
|
||||
"log"
|
||||
)
|
||||
|
||||
func SetupLog() {
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
}
|
||||
16
volumes.go
Normal file
16
volumes.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package proton_api_bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/henrybear327/go-proton-api"
|
||||
)
|
||||
|
||||
func listAllVolumes(ctx context.Context, c *proton.Client) ([]proton.Volume, error) {
|
||||
volumes, err := c.ListVolumes(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return volumes, nil
|
||||
}
|
||||
Reference in New Issue
Block a user