Search code examples
perlsystem

Perl System Call not Interpretting Variable Correctly


I'm using Perl to pass a string ($password) to a system line to be interpreted. It doesn't seem to be interpreted correctly though, because when I go to attach the Sparse Bundle it says the authentication failed. As a note, everything after the pipe works fine.

$password = chomp($password);

### Create the bash system call to create the sparse bundle with the password
my $cmd = `echo $password | hdiutil create -size 200g -type SPARSEBUNDLE -encryption -stdinpass -volname \"Encrypted Storage for Matt\" -fs \"Case-sensitive Journaled HFS+\" -verbose ~/Desktop/SparseBundle`;

Some sample output:

SLC1087-Matt:backups matt$ ./create_sparsebundle.pl 
DIDiskImageCreatorProbe: interface  1, score     1000, CSparseBundleDiskImage
DIDiskImageCreatorProbe: interface  2, score    -1000, CSparseDiskImage
DIDiskImageCreatorProbe: interface  3, score    -1000, CRawDiskImage
DIDiskImageCreatorProbe: interface  7, score    -1000, CWOUDIFDiskImage
DIDiskImageCreatorProbe: interface  9, score    -1000, CCFPlugInDiskImage
DIDiskImageCreateWithCFURL: CSparseBundleDiskImage
CBSDBackingStore::createProbe directory, not a valid image file.
DIBackingStoreCreatorProbe: interface  0, score    -1000, CBSDBackingStore
DIBackingStoreCreatorProbe: interface  1, score     1000, CBundleBackingStore
DIBackingStoreCreatorProbe: interface  2, score        0, CRAMBackingStore
DIBackingStoreCreatorProbe: interface  3, score      100, CCarbonBackingStore
DIBackingStoreCreatorProbe: interface  5, score     -100, CCURLBackingStore
DIBackingStoreCreateWithCFURL: CBundleBackingStore
DIFileEncodingCreatorProbe: interface  2, score     1000, CEncryptedEncoding
DIFileEncodingCreateWithCFURL: CEncryptedEncoding
DIFileEncodingCreatorProbe: interface  2, score    -1000, CEncryptedEncoding
DIBackingStoreCreatorProbe: interface  0, score      100, CBSDBackingStore
DIBackingStoreCreatorProbe: interface  1, score    -1000, CBundleBackingStore
DIBackingStoreCreatorProbe: interface  2, score        0, CRAMBackingStore
DIBackingStoreCreatorProbe: interface  3, score      100, CCarbonBackingStore
DIBackingStoreCreatorProbe: interface  5, score     -100, CCURLBackingStore
DIBackingStoreCreateWithCFURL: CBSDBackingStore
DIBackingStoreCreateWithCFURL: creator returned 0
DIFileEncodingCreateWithCFURL: creator returned 0
DIBackingStoreCreateWithCFURL: creator returned 0
DIDiskImageCreateWithCFURL: creator returned 0
DI_kextWaitQuiet: about to call IOServiceWaitQuiet...
DI_kextWaitQuiet: IOServiceWaitQuiet took 0.000003 seconds
2016-05-18 20:59:09.627 diskimages-helper[68122:1796245] *** -[NSMachPort handlePortMessage:]: dropping incoming DO message because the connection is invalid
hdiutil: create: returning 0
SLC1087-Matt:backups matt$ hdiutil attach ~/Desktop/SparseBundle.sparsebundle
Enter password to access "SparseBundle.sparsebundle": 
hdiutil: attach failed - Authentication error

Everything seems to work fine but the password is apparently wrong.


Solution

  • The hdiutil utility requires a null-terminated password.

    That is, "mypass" isn't good enough. It needs to see "mypass\0" because the password is allowed to contain funny characters like embedded newlines.

    So, what it's seeing is "mypass\n" instead of "mypass\0"

    You could try this as it will work under linux:

    echo -n -e "mypass\x00" | hdiutil ...
    

    But ... AFAICT, the OSX version of echo doesn't have -e, so you may need to experiment.

    If all else fails, replace the echo with:

    echo "mypass" | perl -e '$_ = <STDIN>; chomp ; print $_,"\x00"'
    

    There may be simpler ways to do/express this. You could create a second perl script (call it passme):

    #!/usr/bin/perl
    print $ARGV[0],"\x00";
    

    Then replace the echo with passme. I would put double quotes around the argument.


    UPDATE:

    I'm pretty sure because of the stdinpass that the printf has to be piped to the command. I don't know if I'm exactly right on that.

    echo and printf will be piped to the command. But, there are even more ways to do this that I will recommend over those.

    As others have mentioned, the cleanest way may be to use IPC::Run3. It's one stop shopping.

    But, it may not be installed by default on your system [or other OSX systems]. Dunno--YMMV. You may need to investigate this. So, you might have to install it yourself [from CPAN, I presume(?)]

    If your script is only for your own usage, the above may be fine. However, if you're creating the script to be used by others, it may end up on a system that does not have Run3 and the sysadm won't install it.

    So, you have to balance ease of use vs ubiquity. That will be your choice.

    Run3 sets up both stdin and stdout on the hdiutil command, which is what you need because you want to give it the password and capture its output.

    If you only needed one, you could use perl's open function in "pipe" mode. See man perlipc for details. It's pretty simple to use. I've got an example for this--see below.

    Or, you could "roll your own" equivalent of what run3 does by using perl's intrinsic pipe call. You would need two pipes. It's a bit of work, so maybe something for the future.

    The biggest problem with the echo, printf, [and passme] approaches is [as others pointed out], is that because they use argv to get the password, they "bleed" it to other processes [briefly].

    So, we'd like to have a method that is secure [e.g. Run3 would be]. That's why hdiutil takes a password on stdin instead of a command line argument.


    (1) Actually, the simplest way may be to dump the password to a temp file:

    use Fcntl;
    
    my $fd;
    my $tmp = "/tmp/pwfile";
    
    my $password = "whatever";
    
    # NOTE: by using sysopen and setting the 600 permission atomically, we avoid a
    # race condition that may allow another user to open this file
    unlink($tmp);
    sysopen($fd,$tmp,O_WRONLY | O_CREAT | O_EXCL,0600) or
        die("unable to open '$tmp' -- $!\n");
    syswrite($fd,$password . "\x00");
    close($fd)
    
    my $cmd = `hdiutil ... < $tmp`;
    unlink($tmp);
    

    (2) If you don't want to write the password to a temp file, no matter how briefly it may linger around, there is yet another way. It's similar to the passme script approach above, except it passes down the password as an environment variable. This avoids exposure of the password via command line arguments.

    Here's the passme variant:

    #!/usr/bin/perl
    print($ENV{"PASSME"},"\x00");
    

    This could also be done with an inline:

    perl -e 'print($ENV{"PASSME"},"\x00")'
    

    Then, your script becomes:

    my $password = "whatever";
    
    $ENV{"PASSME"} = $password;
    my $cmd = `passme | hdiutil ...`;
    delete($ENV{"PASSME"});
    

    (3) Yet another way is the use [the aforementioned] use of perl's open in pipe mode. Set it up as an input pipe to hdiutil and divert the output to a temp file which you read back after the command has executed.

    my $fdout;
    my $fdin;
    my $tmp = "/tmp/results";
    my $buf;
    
    my $password = "whatever";
    
    open($fdout,"| hdiutil ... > $tmp") ||
        die("unable to open hdiutil pope -- $!\n");
    print($fdout $password,"\x00");
    close($fdout);
    
    open($fdin,"<$tmp") ||
        die("unable to open '$tmp' -- $!\n");
    while ($buf = <$fdin>) {
        chomp($buf);
        # do whatever ...
    }
    close($fdin);