Controlling Rclone in Racket
Written by Dominik Pantůček on 2024-12-05
rcloneracketAnalyzing cryptographic systems usage with respect to how the users interact with it sometimes requires programmatically simulating their actions. It is no different with our recent work on rclone cryptographic keys handling. However we are lucky that Racket provides all the functionality we needed!
First step in running rclone is finding its executable. In the spirit of
configurability with sane defaults the easiest way is to make a parameter with explicit
path and default to whatever can be found in system $PATH
(or
%PATH%
) if not set.
(define rclone-path (make-parameter #f))
(define (rclone-executable)
(or (rclone-path)
(find-executable-path "rclone")))
For many rclone operations the user has to enter a password and the latest rclone versions allow us to provide a callback program to retrieve such password. But we only want to distribute one program that can both provide whatever features we try to implement and also act as a password command program.
That is easy - it is possible to take the currently running program (whether
compiled or as a script) and reserve a special command-line
argument to act as a password command. In our case such command-line argument will be
--password-command
. It is also necessary to add some quotes to the program
path and arguments as on certain broken platforms (Windows 10/11) nothing works as
expected otherwise.
(define password-command-string
(let ((cached-password-command-string #f))
(lambda ()
(or cached-password-command-string
(let ()
(define exec-file
(find-system-path 'exec-file))
(define run-file
(find-system-path 'run-file))
(define this-program
(if (equal? exec-file run-file)
run-file
(format "\"~a\" \"~a\"" exec-file (path->complete-path run-file))))
(define password-command
(format "~a --password-command" this-program))
(set! cached-password-command-string password-command)
password-command)))))
Having composed the password command argument, how do we pass the actual passwords? There is only one reasonable way to do this on all major platforms and that is through environment variables. The calling process sets the password(s) into child rclone process environment variables which leaves them there for its child process created by executing the password command program. First, let us have a look at the calling site:
(define (run-rclone #:capture-stdout (capture-stdout #f)
#:password (password #f)
#:new-password (new-password #f)
. args)
(when password
(putenv "RCLONE_RKT_PASSWORD" password))
(when new-password
(putenv "RCLONE_RKT_NEW_PASSWORD" new-password))
(match-define
(list stdout stdin pid stderr cntl)
(apply
process*
(rclone-executable)
(if (or password new-password)
(cons "--password-command"
(cons (password-command-string)
args))
args)))
(close-output-port stdin)
(cntl 'wait)
(when password
(environment-variables-set!
(current-environment-variables)
#"RCLONE_RKT_PASSWORD" #f))
(when new-password
(environment-variables-set!
(current-environment-variables)
#"RCLONE_RKT_NEW_PASSWORD" #f))
(define stdout-contents
(if capture-stdout
(port->string stdout)
#f))
(close-input-port stdout)
(close-input-port stderr)
(define result-list
`(,(eq? (cntl 'exit-code) 0)
,@(if capture-stdout
(list stdout-contents)
'())))
(apply values result-list))
It contains some convenience features like returning multiple values based on optional
keyword arguments, but other than that the implementation is fairly straightforward.
Now what happens when the program is invoked with the --password-command
option? This command-line argument must be properly handled:
(command-line
#:program "rclone-manager"
#:once-each
("--rclone" path "path to rclone executable"
(rclone-path path))
("--password-command" "helper for --password-command"
(rclone-password-command)
(exit 0))
And the actual password-producing logic (from the prepared environment variables)
lies in rclone-password-command
:
(define (rclone-password-command)
(if (equal? (getenv "RCLONE_PASSWORD_CHANGE") "1")
(displayln (getenv "RCLONE_RKT_NEW_PASSWORD"))
(displayln (getenv "RCLONE_RKT_PASSWORD"))))
Yes, that is all. Still if the attacker can capture the RAM contents, they get access to the password in question. However that is of no concern to us as it is intended mainly for testing purposes and also in case of dumping the system memory, the attacker gets the password anyway.
Hope you liked this quick dive into inter-process communication and see ya next time!