diff --git a/src/main/resources/static/js/password-crypto.js b/src/main/resources/static/js/password-crypto.js index c1eac21..e78ae5d 100644 --- a/src/main/resources/static/js/password-crypto.js +++ b/src/main/resources/static/js/password-crypto.js @@ -158,8 +158,86 @@ }); } + /** 원본 입력에서 name 을 잠시 떼어둔 동안 그 이름을 보관하는 표시용 속성 */ + var NAME_HOLDER = 'data-password-crypto-name'; + /** - * 폼의 지정 필드 값을 봉투로 치환한다. 실패해도 reject 하지 않고 평문을 남긴다. + * 입력 요소에서 실제 하나를 고른다. + * 같은 name 이 둘 이상이면 form.elements[name] 이 RadioNodeList 를 돌려주므로, + * 우리가 붙인 hidden 이 아닌 실제 입력창을 골라야 한다. + */ + function resolveInput(el) { + if (!el) { + return null; + } + if (el.tagName) { + return el; + } + for (var i = 0; i < el.length; i++) { + var candidate = el[i]; + if (candidate && candidate.tagName && candidate.type !== 'hidden') { + return candidate; + } + } + return null; + } + + /** + * 원본 입력의 value 는 그대로 두고, name 만 같은 이름의 hidden 으로 옮겨 봉투를 싣는다. + * @returns {Function} 원상복구 함수 + */ + function swapToEnvelopeField(form, input, name, envelope) { + var hidden = (form.ownerDocument || global.document).createElement('input'); + hidden.type = 'hidden'; + hidden.name = name; + hidden.value = envelope; + hidden.setAttribute(NAME_HOLDER, name); + + input.setAttribute(NAME_HOLDER, name); + input.removeAttribute('name'); + form.appendChild(hidden); + + return function restore() { + if (hidden.parentNode) { + hidden.parentNode.removeChild(hidden); + } + if (input.getAttribute(NAME_HOLDER) === name) { + input.setAttribute('name', name); + input.removeAttribute(NAME_HOLDER); + } + }; + } + + /** + * 제출 직후 원상복구한다. 폼 데이터 직렬화는 form.submit()/requestSubmit() 호출 시점에 + * 동기로 끝나므로, 다음 매크로태스크에서 되돌려도 봉투는 이미 전송된 뒤다. + * 뒤로가기(bfcache) 복원 시에도 name 이 떨어진 채 남지 않도록 pageshow 에서 한 번 더 처리한다. + */ + function scheduleRestore(restores) { + if (restores.length === 0) { + return; + } + var done = false; + var run = function () { + if (done) { + return; + } + done = true; + restores.forEach(function (restore) { restore(); }); + global.removeEventListener('pageshow', run); + }; + global.addEventListener('pageshow', run); + global.setTimeout(run, 0); + } + + /** + * 폼의 지정 필드를 봉투로 전송한다. 실패해도 reject 하지 않고 평문을 남긴다. + * + * 입력창()의 value 는 절대 건드리지 않는다. 브라우저 비밀번호 관리자는 + * 제출 시점의 입력값을 그대로 저장하므로 봉투로 덮어쓰면 봉투 문자열이 비밀번호로 저장되고, + * 다음 자동완성 로그인이 깨진다. 대신 원본 입력의 name 을 잠시 떼고 같은 name 의 hidden 에 + * 봉투를 실어 보낸 뒤 제출 직후 되돌린다. + * * @param {HTMLFormElement} form * @param {string[]} fieldNames * @returns {Promise} @@ -171,7 +249,7 @@ var targets = {}; var elements = {}; fieldNames.forEach(function (name) { - var el = form.elements ? form.elements[name] : null; + var el = resolveInput(form.elements ? form.elements[name] : null); if (el && typeof el.value === 'string' && el.value.length > 0) { elements[name] = el; targets[name] = el.value; @@ -181,11 +259,14 @@ return global.Promise.resolve(); } return encryptValues(targets).then(function (encrypted) { + var restores = []; Object.keys(elements).forEach(function (name) { - if (encrypted[name]) { - elements[name].value = encrypted[name]; + // 폴백으로 평문이 그대로 돌아온 경우엔 아무것도 바꾸지 않는다. + if (encrypted[name] && encrypted[name] !== targets[name]) { + restores.push(swapToEnvelopeField(form, elements[name], name, encrypted[name])); } }); + scheduleRestore(restores); }); }