feat: add organization vault support (#3)
* feat: add organization vault support - Add RSA-OAEP decryption (type 3: SHA-256, type 4: SHA-1) - Decrypt user's RSA private key from sync response (PKCS8 DER) - Decrypt organization keys using user's RSA private key - Route cipher decryption to correct key based on organizationId - Personal vault items continue to work unchanged Adds 11 new unit tests covering: - RSA cipher string parsing (type 3/4) - RSA-OAEP round-trip (SHA-1 and SHA-256) - Private key decrypt/parse round-trip - Org key decrypt + encKey/macKey split - Cipher decryption with org vs personal key - Backward compatibility for personal items Also adds CI/CD pipeline (.github/workflows/ci.yml): - Tests + build on PRs and main pushes - Docker build + push to GHCR on main * fix: split concatenated .gitignore entries * chore: clean up gitignore
This commit is contained in:
1 parent
f194e85ec0
commit
2b4340d8b0
5 files changed
+586
-13
No files matched your search
@@ -40,20 +40,30 @@ type SyncResponse struct {
|
||||
|
||||
// SyncProfile contains user profile info.
|
||||
type SyncProfile struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Key string `json:"key"`
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Key string `json:"key"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
Organizations []SyncOrganization `json:"organizations"`
|
||||
}
|
||||
|
||||
// SyncOrganization represents an organization the user belongs to.
|
||||
type SyncOrganization struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
// SyncCipher represents an encrypted vault item from the sync response.
|
||||
type SyncCipher struct {
|
||||
ID string `json:"id"`
|
||||
Type int `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Notes *string `json:"notes"`
|
||||
Login *SyncLogin `json:"login"`
|
||||
Card *SyncCard `json:"card"`
|
||||
Fields []SyncField `json:"fields"`
|
||||
ID string `json:"id"`
|
||||
Type int `json:"type"`
|
||||
OrganizationID *string `json:"organizationId"`
|
||||
Name string `json:"name"`
|
||||
Notes *string `json:"notes"`
|
||||
Login *SyncLogin `json:"login"`
|
||||
Card *SyncCard `json:"card"`
|
||||
Fields []SyncField `json:"fields"`
|
||||
}
|
||||
|
||||
// SyncLogin contains encrypted login data.
|
||||
@@ -304,10 +314,41 @@ func (ac *APIClient) Sync() ([]DecryptedItem, error) {
|
||||
return nil, fmt.Errorf("decode sync response: %w", err)
|
||||
}
|
||||
|
||||
// Decrypt org keys if organizations are present.
|
||||
orgKeys := make(map[string]SymmetricKey)
|
||||
if len(syncResp.Profile.Organizations) > 0 && syncResp.Profile.PrivateKey != "" {
|
||||
privateKey, err := DecryptPrivateKey(syncResp.Profile.PrivateKey, key)
|
||||
if err != nil {
|
||||
logger.Warn.Printf("Failed to decrypt RSA private key, org items will be skipped: %v", err)
|
||||
} else {
|
||||
for _, org := range syncResp.Profile.Organizations {
|
||||
orgKey, err := DecryptOrgKey(org.Key, privateKey)
|
||||
if err != nil {
|
||||
logger.Warn.Printf("Failed to decrypt org key for %s: %v", org.ID, err)
|
||||
continue
|
||||
}
|
||||
orgKeys[org.ID] = orgKey
|
||||
logger.Debug.Printf("Decrypted org key for organization %s", org.ID)
|
||||
}
|
||||
logger.Info.Printf("Decrypted %d organization key(s)", len(orgKeys))
|
||||
}
|
||||
}
|
||||
|
||||
// Decrypt all ciphers.
|
||||
items := make([]DecryptedItem, 0, len(syncResp.Ciphers))
|
||||
for _, c := range syncResp.Ciphers {
|
||||
item, err := decryptCipher(c, key)
|
||||
// Select the correct decryption key.
|
||||
decryptKey := key
|
||||
if c.OrganizationID != nil && *c.OrganizationID != "" {
|
||||
if orgKey, ok := orgKeys[*c.OrganizationID]; ok {
|
||||
decryptKey = orgKey
|
||||
} else {
|
||||
logger.Debug.Printf("No org key for cipher %s (org %s), skipping", c.ID, *c.OrganizationID)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
item, err := decryptCipher(c, decryptKey)
|
||||
if err != nil {
|
||||
logger.Debug.Printf("Failed to decrypt cipher %s: %v", c.ID, err)
|
||||
continue
|
||||
|
||||
Reference in new issue
Block a user