Skip to content

Commit

Permalink
Feature/688: Credentials sharing (#708)
Browse files Browse the repository at this point in the history
* Implement share credentials

---------

Co-authored-by: Mountain Star <[email protected]>
  • Loading branch information
lkleisa and mtnstar authored Jul 11, 2023
1 parent 0d1ca83 commit 35632e3
Show file tree
Hide file tree
Showing 34 changed files with 1,161 additions and 164 deletions.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
5.0
5.1
1 change: 1 addition & 0 deletions app/controllers/api/encryptables_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def query_param
end

def encryptable_move_handler
entry.sender_id = nil
EncryptableMoveHandler.new(entry, session[:private_key], current_user)
end

Expand Down
20 changes: 16 additions & 4 deletions app/controllers/api/encryptables_transfer_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,35 @@ class Api::EncryptablesTransferController < ApiController
self.permitted_attrs = [:name, :description, :receiver_id, :file]

def create
prepare_encryptable_file
if params[:file].present?
prepare_encryptable_file
else
prepare_encryptable_credential
end
authorize entry
transfer_file
transfer_encryptable

render json: messages
end

private

def transfer_file
def transfer_encryptable
@encryptable = EncryptableTransfer.new.transfer(
entry, User::Human.find(receiver_id), current_user
)

add_info('flashes.encryptable_transfer.file.transferred')
end

def prepare_encryptable_credential
shared_encryptable = current_user.encryptables.find(params['encryptable_id'])

shared_encryptable.decrypt(decrypted_team_password(shared_encryptable.team))

instance_variable_set(:"@#{ivar_name}", shared_encryptable.dup)
end

def prepare_encryptable_file
filename = params[:file].original_filename

Expand All @@ -41,7 +53,7 @@ def new_file(inbox_folder_receiver, description, name)
end

def model_class
Encryptable::File
params[:file].present? ? Encryptable::File : Encryptable::Credentials
end

def model_params
Expand Down
12 changes: 9 additions & 3 deletions app/models/encryptable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ def inbox_folder_present?
Folder.find(folder_id)&.name == 'inbox' if folder_id
end

def decrypt_transferred(private_key)
decrypt(plaintext_transfer_password(private_key))
end

def plaintext_transfer_password(private_key)
Crypto::Rsa.decrypt(Base64.decode64(encrypted_transfer_password), private_key)
end
Expand All @@ -74,9 +78,11 @@ def used_encrypted_data_attrs
def encrypt_attr(attr, team_password)
cleartext_value = send(:"cleartext_#{attr}")

encrypted_value =
cleartext_value.presence &&
Crypto::Symmetric::Aes256.encrypt(cleartext_value, team_password)
encrypted_value = if cleartext_value.presence
Crypto::Symmetric::Aes256.encrypt(cleartext_value, team_password)
end

return if transferred? && encrypted_value.blank?

build_encrypted_data(attr, encrypted_value)
end
Expand Down
4 changes: 0 additions & 4 deletions app/models/encryptable/file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ def encrypt(team_password)
encrypt_attr(:file, team_password)
end

def decrypt_transferred(private_key)
decrypt(plaintext_transfer_password(private_key))
end

def team
folder&.team || encryptable_credential.folder.team
end
Expand Down
6 changes: 5 additions & 1 deletion app/serializers/encryptable_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@
# https://github.com/puzzle/cryptopus.

class EncryptableSerializer < ApplicationSerializer
attributes :id, :name, :description
attributes :id, :name, :description, :sender_name

belongs_to :folder

def sender_name
object.sender&.label
end
end
14 changes: 14 additions & 0 deletions app/utils/encryptable_transfer.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
# frozen_string_literal: true

class EncryptableTransfer

def transfer(encryptable, receiver, sender)
transfer_password = new_transfer_password
encryptable.encrypt(transfer_password)

encryptable.name = encryptable_destination_name(encryptable, receiver)

encryptable.update!(
folder: receiver.inbox_folder,
sender_id: sender.id,
Expand All @@ -28,6 +31,13 @@ def receive(encryptable, private_key, personal_team_password)

private

def encryptable_destination_name(encryptable, receiver)
existing_names = receiver.inbox_folder.encryptables.pluck(:name)
is_file = encryptable.is_a?(Encryptable::File)

transfered_name(encryptable.name, existing_names, is_file).destination_name
end

def encrypted_transfer_password(password, receiver)
Crypto::Rsa.encrypt(
password,
Expand All @@ -39,4 +49,8 @@ def new_transfer_password
Crypto::Symmetric::Aes256.random_key
end

def transfered_name(name, existing_names, is_file)
EncryptableTransferedName.new(name, existing_names, is_file)
end

end
90 changes: 90 additions & 0 deletions app/utils/encryptable_transfered_name.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# frozen_string_literal: true

class EncryptableTransferedName

INCREMENT_REGEX = /\((\d+)\)/

def initialize(name, existing_names, is_file)
@name = name
@existing_names = existing_names
@is_file = is_file
end

def destination_name
destination_name = @name
if @existing_names.present?
similar_names = find_similar_names
if similar_names.present? && similar_names.include?(@name)
similar_names = sort_similar_names(similar_names)
latest_name = similar_names.last
destination_name = copy_name(@name, latest_name)
end
end

destination_name
end

def copy_name(name, latest_name)
if @is_file
suffix = File.extname(name)
name = File.basename(name, '.*')
end

next_copy_name(name, latest_name, suffix)
end

def sort_similar_names(similar_names)
if @is_file
similar_names.sort_by do |element|
number_match = element.match(/\((\d+)\)/)
number_match ? number_match[1].to_i : 0
end
else
similar_names.sort
end
end

def next_copy_name(name, latest_name, suffix)
# Remove (NUMBER) if it already exists in name
if INCREMENT_REGEX.match(name)
name = name.sub(/\(\d+\)\z/, '')
end

# If last encryptable in inbox has (NUMBER) add (NUMBER + 1)
name += if INCREMENT_REGEX.match(latest_name)
"(#{INCREMENT_REGEX.match(latest_name)[1].to_i + 1})"
else
'(1)'
end

name += suffix if @is_file

name
end

def find_similar_names
suffix = File.extname(@name)
name = File.basename(@name, '.*')

# For the loop through inbox encryptables remove (NUMBER) from name
name = basename_of_copy(name)

regex_pattern = /\A#{Regexp.escape(name)}(\(\d+\))?\z/
@existing_names.select do |existing_name|
current_suffix = File.extname(existing_name)
current_name = File.basename(existing_name, '.*')
current_name.match?(regex_pattern) && current_suffix == suffix
end
end

def basename_of_copy(name)
basename_regex = /\A.*\(\d+\)\z/

if name.match?(basename_regex)
name = name.gsub(/\(\d+\)\z/, '')
end

name
end

end
87 changes: 87 additions & 0 deletions frontend/app/components/credential-transfer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { action } from "@ember/object";
import Changeset from "ember-changeset";
import { inject as service } from "@ember/service";
import { tracked } from "@glimmer/tracking";
import BaseFormComponent from "./base-form-component";
import ENV from "frontend/config/environment";
import lookupValidator from "ember-changeset-validations";
import EncryptableTransferCredential from "../validations/encryptable-transfer-credential";

export default class CredentialTransfer extends BaseFormComponent {
@service store;
@service router;
@service navService;
@tracked encryptableId;
@tracked encryptableName;
@tracked candidates;
@tracked receiver;

constructor() {
super(...arguments);

this.record = this.store.createRecord("encryptable-transfer");

this.changeset = new Changeset(
this.record,
lookupValidator(EncryptableTransferCredential),
EncryptableTransferCredential
);

this.changeset.csrfToken = ENV.CSRFToken;

this.encryptableId = this.args.encryptableId;
this.encryptableName = this.args.encryptableName;
this.changeset.encryptableId = this.encryptableId;

this.loadValidation();

this.loadCandidates();
}

loadCandidates() {
this.store
.query("user-human", {
candidates: true
})
.then((res) => (this.candidates = res));
}

async loadValidation() {
await this.changeset.validate();
}

@action
selectReceiver(receiver) {
this.changeset.receiver = receiver;
this.loadValidation();
}

@action
search(input) {
return this.candidates.filter(
(c) => c.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
);
}

@action
abort() {
if (this.args.onAbort) {
this.args.onAbort();
}
}

showSuccessMessage() {
let msg = this.intl.t(
"flashes.encryptable_transfer.credentials.transferred"
);
this.notify.success(msg);
}

handleSubmitSuccess() {
this.abort();
}

handleSubmitError(response) {
this.errors = JSON.parse(response.body).errors;
}
}
9 changes: 8 additions & 1 deletion frontend/app/components/encryptable/row.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,16 @@ export default class RowComponent extends Component {

@tracked
isFile = this.args.encryptable.isFile;
@tracked
isInboxFolder;

@tracked isTransferredCredentials =
this.args.encryptable.sender_name !== null;

constructor() {
super(...arguments);

this.isInboxFolder = this.args.encryptable.folder.get("isInboxFolder");
}

// get set attribute amount to hide attribute fields in encryptable row when encryptable has more than two attributes
Expand Down Expand Up @@ -60,7 +67,7 @@ export default class RowComponent extends Component {
}

get downloadLink() {
return `/api/encryptables/${this.args.encryptable.get("id")}`;
return `/api/encryptables/${this.args.encryptable.id}`;
}

@action
Expand Down
11 changes: 11 additions & 0 deletions frontend/app/components/encryptable/show.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ export default class ShowComponent extends Component {
@tracked
isFile = this.args.encryptable.isFile;

@tracked
isCredentialSharing = false;

@tracked isTransferredCredentials =
this.args.encryptable.sender_name !== null && !this.isFile;

@action
toggleEncryptableEdit() {
this.isEncryptableEditing = !this.isEncryptableEditing;
Expand All @@ -40,6 +46,11 @@ export default class ShowComponent extends Component {
this.router.transitionTo("/teams");
}

@action
toggleCredentialSharing() {
this.isCredentialSharing = !this.isCredentialSharing;
}

@action
transitionBack() {
this.router.transitionTo(
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/components/file-transfer.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default class FileTransfer extends BaseFormComponent {
constructor() {
super(...arguments);

this.record = this.store.createRecord("encryptable-transferred");
this.record = this.store.createRecord("encryptable-transfer");

this.changeset = new Changeset(
this.record,
Expand Down
Loading

0 comments on commit 35632e3

Please sign in to comment.