3
$\begingroup$

Is following approach secure?

Planning on using implementation to XOR two passphrases before feeding result to Argon2.

const xor = (a: string, b: string) => {
  if (a.length > b.length) {
    b = `${b}${b.substring(0, a.length - b.length)}`
  } else if (b.length > a.length) {
    a = `${a}${a.substring(0, b.length - a.length)}`
  }
  let result = ""
  for (let index = 0; index < a.length; index++) {
    result += (
      parseInt(a.charAt(index), 16) ^ parseInt(b.charAt(index), 16)
    ).toString(16)
  }
  return result
}
$\endgroup$

2 Answers 2

6
$\begingroup$

No, XOR is almost never the answer in these kind of circumstances.

With XOR it might well be that certain values of the passwords a and b would cancel each other our, for instance. If I were an attacker the very first that I would do is to try all zero strings up to a certain size, as that would indicate identical passwords. That is however only the most obvious problem that might be encountered.


Actually, just concatenation of the passwords and some indication of password size or a unique value as separator of at least one of them would be a better idea. The size is required to provide slightly more security, as otherwise some the passwords may overflow into each other, e.g. somethingcompletely and different would create the same string as something and completelydifferent. However, simply including a special character would overcome this issue: somethingcompletely🦉different and something🦉completelydifferent are indeed different strings. You could use a non-printable character such as the ASCII Unit Separator instead of the Unicode owl of course, but make sure that it isn't dropped somewhere in the process.

Here is the initial calculation in Argon2, after which the password isn't used anymore. As you can see, it directly hashes most of the input data including the password. For some other PBKDF's such as PBKDF2 it might make sense to perform an initial hash, but for Argon2 this initial hash is already included in the algorithm.

buffer ← parallelism ∥ tagLength ∥ memorySizeKB ∥ iterations ∥ version ∥ hashType
    ∥ Length(password)       ∥ Password
    ∥ Length(salt)           ∥ salt
    ∥ Length(key)            ∥ key
    ∥ Length(associatedData) ∥ associatedData
 H0 ← Blake2b(buffer, 64) //default hash size of Blake2b is 64-bytes

(source: Wikipedia)

Because of this initial hash there is no real limit to the password size in Argon2 (passwords of 4GiB or more are not often used).


Note that this answer directly targets the XOR construction. It might well be that different strategies exist that would be even better than the one proposed here for a particular purpose (e.g. using Argon2 on two passwords separately and using HKDF on the concatenated result).

$\endgroup$
8
  • $\begingroup$ Thanks for helping out! One aspect I like about XOR is it is order agnostic… that said, if I understood your answer correctly, concatenation is more secure. $\endgroup$
    – sunknudsen
    Jul 3, 2022 at 20:20
  • 1
    $\begingroup$ What I am trying to achieve is to establish a governance scheme whereby two independent parties each have a passphrase. Without both, decryption cannot occur. I am aware of Shamir Secret Sharing, but given I am trying to achieve a 2 of 2, it is overkill. $\endgroup$
    – sunknudsen
    Jul 3, 2022 at 20:24
  • 1
    $\begingroup$ Just thinking out loud: you could lexicographically order the passwords first. $\endgroup$
    – Maarten Bodewes
    Jul 3, 2022 at 20:41
  • $\begingroup$ It is naive to consider each passphrase as additional entropy in the context of concatenation which means order does not affect security? $\endgroup$
    – sunknudsen
    Jul 3, 2022 at 20:43
  • 2
    $\begingroup$ @sunknudsen Possibly, but it depends a lot on the use case. Maybe the passwords are related in some kind of way. Would be worse in case of XOR though, as you can then rule out specific combinations. A PBKDF is a strengthened PRF, so it should not be possible to get any information out of that. If you lexicographically order then the adversary only has to test in lexicographical order, but that cannot be avoided and it doesn't downgrade security significantly. $\endgroup$
    – Maarten Bodewes
    Jul 3, 2022 at 20:59
1
$\begingroup$

I totally agree with Maarten Bodewes answer.

I will refer to another aspect of the question. Just how to securely xor in Javascript, which is the title of the question, not why or if it really needed.

Personally I would prefer WebAssembly for any cryptographic related stuff since Javascript is a high level language. As you can understand since WASM is an intermediate low level language there is no contract that the outputed assembly will be the same on any browser, or from time to time, it is just more probably to be secure that plain Javascript. Lastly, another aspect of the security of web applications is that there are just so many other attack vectors that nobody will focus on your cryptography.

This is an example of C++ code with the corresponding WASM and the actual x86_64 that will be executed by the browser.

C++

int a(char **str1, int str1_size, char **str2, int str2_size, char**result) {
  
  if (str1_size != str2_size) {
    return -1;
  }
  
  for (int i = 0; i < str1_size; i++) {
    (*result)[i] = (*str1)[i] ^ (*str2)[i];
  }
}

WASM

(module
 (table 0 anyfunc)
 (memory $0 1)
 (export "memory" (memory $0))
 (export "_Z1aPPciS0_iS0_" (func $_Z1aPPciS0_iS0_))
 (func $_Z1aPPciS0_iS0_ (; 0 ;) (param $0 i32) (param $1 i32) (param $2 i32) (param $3 i32) (param $4 i32) (result i32)
  (block $label$0
   (br_if $label$0
    (i32.ne
     (get_local $1)
     (get_local $3)
    )
   )
   (set_local $1
    (i32.const 0)
   )
   (loop $label$1
    (i32.store8
     (i32.add
      (i32.load
       (get_local $4)
      )
      (get_local $1)
     )
     (i32.xor
      (i32.load8_u
       (i32.add
        (i32.load
         (get_local $2)
        )
        (get_local $1)
       )
      )
      (i32.load8_u
       (i32.add
        (i32.load
         (get_local $0)
        )
        (get_local $1)
       )
      )
     )
    )
    (set_local $1
     (i32.add
      (get_local $1)
      (i32.const 1)
     )
    )
    (br $label$1)
   )
  )
  (i32.const -1)
 )
)

x86_64

wasm-function[0]:
  sub rsp, 8                            ; 0x000000 48 83 ec 08
  cmp esi, ecx                          ; 0x000004 3b f1
  jne 0x35                              ; 0x000006 0f 85 29 00 00 00
 0x00000c:                              
  xor eax, eax                          ; 0x00000c 33 c0
 0x00000e:                              ; 0x00000e from: [0x000033]
  mov ecx, dword ptr [r15 + r8]         ; 0x00000e 43 8b 0c 07
  add ecx, eax                          ; 0x000012 03 c8
  mov ebx, dword ptr [r15 + rdx]        ; 0x000014 41 8b 1c 17
  add ebx, eax                          ; 0x000018 03 d8
  movzx ebx, byte ptr [r15 + rbx]       ; 0x00001a 41 0f b6 1c 1f
  mov ebp, dword ptr [r15 + rdi]        ; 0x00001f 41 8b 2c 3f
  add ebp, eax                          ; 0x000023 03 e8
  movzx ebp, byte ptr [r15 + rbp]       ; 0x000025 41 0f b6 2c 2f
  xor ebx, ebp                          ; 0x00002a 33 dd
  mov byte ptr [r15 + rcx], bl          ; 0x00002c 41 88 1c 0f
  add eax, 1                            ; 0x000030 83 c0 01
  jmp 0xe                               ; 0x000033 eb d9
 0x000035:                              
  mov eax, 0xffffffff                   ; 0x000035 b8 ff ff ff ff
  nop                                   ; 0x00003a 66 90
  add rsp, 8                            ; 0x00003c 48 83 c4 08
  ret                                   ; 0x000040 c3

The final assembly seems fine to me.

Lastly, probably a XOR operation because of it's simplicity, could work fine even in plain Javascript, I'm just posting this answer as an idea to alternative (more secure in general?) ways to implement cryptography in a browser.

EDIT: Tool used for the generated code.

$\endgroup$
5
  • $\begingroup$ That's hilariously inefficient asm, redoing the (*result) and other dereference every time through the loop. Your function doesn't update the caller's pointers, only the pointed-to char data, so you could change the signature to char *str1 and so on, and remove the extra level of indirection. Then it JITs to fairly same asm with 3 pointer increments, but only one load and one store. $\endgroup$ Jul 4, 2022 at 8:46
  • $\begingroup$ Oh, also you need return 0; at the end, otherwise you fall off a non-void function. Firefox strangely assumes the loop must be infinite so that path of execution isn't reached, because that's UB in C++. That's why you have a jmp 0xe instead of an add esi, -1 / test esi,esi / jnz 0x26. (Yes, Firefox is that dumb, according to mbebenita.github.io/WasmExplorer, not using FLAGS set by add esi, -1.) Unfortunately we don't get SIMD (or even scalar 8 bytes at a time), like LLVM does when compiling straight to x86-64 asm with -O3. Although not a big deal for short strings. $\endgroup$ Jul 4, 2022 at 8:48
  • $\begingroup$ BTW, the "create a persistent link" button isn't working, or I don't know how to use it. godbolt.org/z/5M6KMYqaY is the source I was looking at, with clang -O3 -fno-vectorize -fno-unroll-loops x86-64 asm (using the x86-64 SysV calling convention instead of whatever we get with WASM), as well as clang -target wasm32. $\endgroup$ Jul 4, 2022 at 8:56
  • $\begingroup$ Please don't XOR strings in C and hope for a string output. If a given character happens to be the same in both input strings, the XOR will give zero, which will terminate the output string. So the XOR of "Aardvark" and "Antelope" is the null string. That's probably not what you want. $\endgroup$
    – abligh
    Jul 4, 2022 at 10:51
  • 1
    $\begingroup$ @abligh: This is XORing two explicit-length arrays that happen to hold char elements, not implicit-length C strings. (e.g. like C++ std:::string, or perhaps how JavaScript engines hold strings.) But good point, porting this to just take char* inputs without lengths would be fatally flawed. $\endgroup$ Jul 4, 2022 at 11:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.