Encryption 1™ - Online : ソースコード


<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Encryption 1</title>
  <meta name="description" content="無料のブラウザベース暗号化ツール。AES-GCMを使い、テキストやファイルをローカルで暗号化・復号できます。アカウント登録、インストール、サーバー保存は不要。オフラインでも動作します。" />
  <link rel="canonical" href="https://www.encryption-one.com/ja/ec1.html" />
  <link rel="alternate" hreflang="en" href="https://www.encryption-one.com/ec1.html" />
  <link rel="alternate" hreflang="de" href="https://www.encryption-one.com/de/ec1.html" />
  <link rel="alternate" hreflang="es" href="https://www.encryption-one.com/es/ec1.html" />
  <link rel="alternate" hreflang="fr" href="https://www.encryption-one.com/fr/ec1.html" />
  <link rel="alternate" hreflang="ja" href="https://www.encryption-one.com/ja/ec1.html" />
  <link rel="alternate" hreflang="x-default" href="https://www.encryption-one.com/ec1.html" />
  <meta property="og:type" content="website" />
  <meta property="og:site_name" content="Encryption 1" />
  <meta property="og:title" content="Encryption 1" />
  <meta property="og:description" content="無料のブラウザベース暗号化ツール。AES-GCMを使い、テキストやファイルをローカルで暗号化・復号できます。アカウント登録、インストール、サーバー保存は不要。オフラインでも動作します。" />
  <meta property="og:url" content="https://www.encryption-one.com/ja/ec1.html" />
  <meta property="og:locale" content="ja_JP" />
  <meta property="og:locale:alternate" content="en_US" />
  <meta property="og:locale:alternate" content="de_DE" />
  <meta property="og:locale:alternate" content="es_ES" />
  <meta property="og:locale:alternate" content="fr_FR" />
  <!-- og:image: add once a preview image is ready, e.g.
       <meta property="og:image" content="https://www.encryption-one.com/og-image.png" /> -->

<style>
  :root {
    --bg: #f5f7fb;
    --card: #ffffff;
    --text: #1f2937;
    --muted: #6b7280;
    --border: #dbe3ee;
    --accent: #0d6efd;
    --accent-2: #0b5ed7;
    --danger: #b42318;
    --success: #157347;
    --warning-bg: #ffe6e6;
    --warning-border: #ff8080;
    --shadow: 0 10px 30px rgba(0,0,0,.08);
  }

  * { box-sizing: border-box; }

  body {
    margin: 0;
    font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    background: linear-gradient(180deg, #eef4ff 0%, var(--bg) 100%);
    color: var(--text);
    min-height: 100vh;
  }

  .nolines { text-decoration: none; font-size: small; }

  .container { max-width: 980px; margin: 0 auto; padding: 24px; }

  .hero { text-align: center; margin-bottom: 20px; }
  .hero h1 { margin: 0 0 8px; font-size: clamp(1.8rem, 3vw, 2.4rem); }
  .hero p { margin: 0; color: var(--muted); line-height: 1.55; }

  .badge {
    display: inline-block;
    background: #e9f2ff;
    color: #0a58ca;
    border-radius: 999px;
    padding: 6px 12px;
    font-size: .85rem;
    font-weight: 700;
    margin-bottom: 10px;
  }

  .warning {
    margin: 20px 0;
    padding: 5px 5px;
    border: 1px solid var(--warning-border);
    background: var(--warning-bg);
    border-radius: 14px;
    color: #5f4b00;
    line-height: 1.5;
    font-size: small;
    text-align: center;
  }

  .card {
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: 20px;
    box-shadow: var(--shadow);
    overflow: hidden;
  }

  .tabs { display: flex; border-bottom: 1px solid var(--border); background: #fbfcff; flex-wrap: wrap; }
  .tab-btn {
    appearance: none;
    border: 0;
    background: transparent;
    padding: 16px 20px;
    cursor: pointer;
    font: inherit;
    font-size: 1rem;
    color: var(--muted);
    border-bottom: 3px solid transparent;
    transition: .2s ease;
  }
  .tab-btn:hover,
  .tab-btn.active {
    color: var(--accent);
    border-bottom-color: var(--accent);
    background: rgba(13, 110, 253, .04);
  }

  .panel { display: none; padding: 24px; }
  .panel.active { display: block; }

  .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
  @media (max-width: 820px) {
    .grid { grid-template-columns: 1fr; }
  }

  label { display: block; font-weight: 600; margin-bottom: 8px; }

  textarea,
  input[type="password"],
  input[type="text"],
  input[type="file"] {
    width: 100%;
    padding: 12px 14px;
    border: 1px solid var(--border);
    border-radius: 14px;
    font: inherit;
    background: #fff;
    color: var(--text);
  }
  textarea { resize: vertical; line-height: 1.5; }

  .field { margin-bottom: 16px; }
  .help { margin-top: 6px; font-size: .92rem; color: var(--muted); line-height: 1.45; }
  .actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }

  button {
    appearance: none;
    border: 0;
    cursor: pointer;
    padding: 12px 16px;
    border-radius: 14px;
    font: inherit;
    font-weight: 600;
    transition: transform .05s ease, background .2s ease, opacity .2s ease;
  }
  button:active { transform: translateY(1px); }
  button:disabled { opacity: .55; cursor: not-allowed; }

  .btn-primary { background: var(--accent); color: #fff; }
  .btn-primary:hover:not(:disabled) { background: var(--accent-2); }
  .btn-secondary { background: #eef2f7; color: var(--text); }
  .btn-secondary:hover:not(:disabled) { background: #e3e9f2; }
  .btn-secondary.small { padding: 8px 12px; font-size: .85rem; white-space: nowrap; flex: none; }
  .btn-danger { background: #fbeaea; color: var(--danger); }
  .btn-danger:hover:not(:disabled) { background: #f7dede; }

  .status { margin-top: 16px; min-height: 24px;font-size:small;}
  .status.error { color: var(--danger); }
  .status.success { color: var(--success); }

  .meta {
    margin-top: 16px;
    padding: 14px 16px;
    border-radius: 14px;
    background: #f8fafc;
    border: 1px solid var(--border);
    color: var(--muted);
    line-height: 1.5;
    font-size: .95rem;
  }

  .mono {
    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
    word-break: break-word;
  }

  .footer { margin-top: 18px; color: var(--muted); font-size: .9rem; line-height: 1.5; }

  .strength-wrap { margin-top: 10px; }
  .strength-bar {
    width: 100%;
    height: 10px;
    border-radius: 999px;
    background: #e9eef5;
    border: 1px solid var(--border);
    overflow: hidden;
  }
  .strength-fill {
    height: 100%;
    width: 0;
    border-radius: 999px;
    background: #d0d7e2;
    transition: width .2s ease, background .2s ease;
  }
  .strength-text { margin-top: 8px; font-size: .92rem; color: var(--muted); line-height: 1.4; }

  .dropzone {
    border: 2px dashed #b9c9dc;
    border-radius: 18px;
    padding: 28px 18px;
    text-align: center;
    background: #f8fbff;
    cursor: pointer;
    transition: .15s;
  }
  .dropzone:hover,
  .dropzone.dragover { border-color: var(--accent); background: #eef6ff; }
  .dropzone strong { display: block; font-size: 1.05rem; margin-bottom: 5px; }

  .file-info {
    margin-top: 12px;
    padding: 12px 14px;
    background: #f8fafc;
    border: 1px solid var(--border);
    border-radius: 12px;
    font-size: .92rem;
  }
  .limit { font-weight: 700; color: #0a58ca; }

  .mode-note {
    padding: 12px 14px;
    border-radius: 12px;
    background: #eef6ff;
    color: #0a58ca;
    margin-bottom: 18px;
    font-size: .92rem;
  }

  .hidden { display: none; }

  .password-row { display: flex; gap: 8px; align-items: stretch; }
  .password-row input { flex: 1; min-width: 0; }
</style>
</head>
<body>
<div class="container">
    <div class="hero">
      <h1>🔐 Encryption 1</h1>
      <p><strong>Encryption 1</strong>  •  
	  <a class="nolines" href="journal.html">EC-1 日記</a>  •  
	  <a class="nolines" href="faq.html">よくある質問</a>  •  
	  <a class="nolines" href="security-guide.html">セキュリティガイド</a></p>
    </div>
    <div class="warning">
      <strong>重要:</strong> パスワードを忘れると、ファイルやテキストを復元することはできません。
    </div>
  <div class="card">
    <div class="tabs">
      <button class="tab-btn active" data-tab="encryptText">テキストを暗号化</button>
      <button class="tab-btn" data-tab="encryptFile">ファイルを暗号化</button>
      <button class="tab-btn" data-tab="decrypt">復号</button>
    </div>

    <div id="encryptText" class="panel active">
      <!-- <div class="mode-note">The original Encryption 1 text workflow is preserved here.</div> -->
      <div class="grid">
        <div class="field"><label for="plainText">暗号化するテキスト</label><textarea id="plainText" rows="13" placeholder="ここにテキストを入力または貼り付けてください..."></textarea></div>
        <div>
          <div class="field"><label for="textPassword">パスワード/パスフレーズ</label><div class="password-row"><input id="textPassword" type="password" placeholder="強力なパスワードを入力してください" autocomplete="new-password"><button id="textToggleBtn" class="btn-secondary small" type="button" aria-pressed="false" aria-label="パスワードを表示">表示</button><button id="textGenerateBtn" class="btn-secondary small" type="button">生成</button></div><div class="strength-wrap"><div class="strength-bar"><div id="textStrengthBar" class="strength-fill"></div></div><div id="textStrengthText" class="strength-text">パスワードの強度: 未入力</div></div><div id="textStatus" class="status" aria-live="polite"></div></div>
          <div class="field"><label for="textPassword2">パスワードの確認</label><input id="textPassword2" type="password" placeholder="パスワードを再入力してください" autocomplete="new-password"></div>
          <div class="field"><label for="textFilename">ダウンロードファイル名</label><input id="textFilename" type="text" value="private-note.pvault"></div>
          <div class="actions"><button id="encryptTextBtn" class="btn-primary">暗号化してダウンロード</button><button id="clearTextBtn" class="btn-danger">クリア</button></div>
          
		  <div id="textMeta" class="meta hidden"></div>
        </div>
      </div>
    </div>

    <div id="encryptFile" class="panel">
      <!-- <div class="mode-note"><strong>File limit: 10 MB.</strong> Images, PDFs, Word documents, spreadsheets, ZIP files, and other files can be encrypted as raw bytes.</div> -->
      <div class="grid">
        <div>
          <label>ファイルを選択</label>
          <div id="dropzone" class="dropzone" tabindex="0" role="button"><strong>ここにファイルをドロップ</strong><span>またはクリックして選択</span><div class="help">最大ファイルサイズ: <span class="limit">10 MB</span></div></div>
          <input id="fileInput" type="file" class="hidden">
          <div id="fileInfo" class="file-info hidden"></div>
        </div>
        <div>
          <div class="field"><label for="filePassword">パスワード/パスフレーズ</label><div class="password-row"><input id="filePassword" type="password" placeholder="強力なパスワードを入力してください" autocomplete="new-password"><button id="fileToggleBtn" class="btn-secondary small" type="button" aria-pressed="false" aria-label="パスワードを表示">表示</button><button id="fileGenerateBtn" class="btn-secondary small" type="button">生成</button></div><div class="strength-wrap"><div class="strength-bar"><div id="fileStrengthBar" class="strength-fill"></div></div><div id="fileStrengthText" class="strength-text">パスワードの強度: 未入力</div></div>
		  <div id="fileStatus" class="status" aria-live="polite"></div>
		  </div>
          <div class="field"><label for="filePassword2">パスワードの確認</label><input id="filePassword2" type="password" placeholder="パスワードを再入力してください" autocomplete="new-password"></div>
          <div class="actions"><button id="encryptFileBtn" class="btn-primary">ファイルを暗号化してダウンロード</button><button id="clearFileBtn" class="btn-danger">クリア</button></div>
          
		  <div id="fileMeta" class="meta hidden"></div>
        </div>
      </div>
    </div>

    <div id="decrypt" class="panel">
      <div class="grid">
        <div>
          <div class="field"><label for="encryptedFile">暗号化されたファイル</label><input id="encryptedFile" type="file" accept=".pvault,.json,application/json,text/plain"><div class="help">Encryption 1 のテキストファイルおよびファイルパッケージに対応しています。</div></div>
          <div class="field"><label for="decryptPassword">パスワード/パスフレーズ</label><input id="decryptPassword" type="password" placeholder="暗号化時に使用したものと同じパスワードを入力してください" autocomplete="current-password"></div>
          <div class="actions"><button id="decryptBtn" class="btn-primary">復号</button><button id="clearDecryptBtn" class="btn-danger">クリア</button></div>
          <div id="decryptStatus" class="status" aria-live="polite"></div><div id="decryptMeta" class="meta hidden"></div>
        </div>
        <div>
          <div id="textDecryptArea" class="field"><label for="decryptedText">復号されたテキスト</label><textarea id="decryptedText" placeholder="復号されたテキストがここに表示されます..." spellcheck="false" rows="13"></textarea></div>
          <div id="textDecryptActions" class="actions"><button id="copyPlainBtn" class="btn-secondary" disabled>テキストをコピー</button><button id="downloadPlainBtn" class="btn-secondary" disabled>.txtとしてダウンロード</button></div>
          <div id="fileDecryptArea" class="meta hidden"></div>
        </div>
      </div>
    </div>
  </div>
<footer>
  <div class="footer">🔐 <strong>Encryption 1</strong> © 1996-<span id="year"></span> by Seán L. Young. 全著作権所有<br />無料で利用可能 • アカウント不要 • オフライン対応 • <a href="EC-1.zip">ZIPをダウンロード</a> • <a href="privacy.html">プライバシー</a> • <a href="tos.html">利用規約</a></div>
</footer>
<script>
  document.getElementById("year").textContent = new Date().getFullYear();
</script>
<div id="browserStatus" class="status" aria-live="polite"></div>
</div>
<script>
'use strict';
const APP_VERSION = 2.0;
const PBKDF2_ITERATIONS = 250000;
const SALT_LENGTH = 16, IV_LENGTH = 12, MAX_FILE_SIZE = 10 * 1024 * 1024;
const encoder = new TextEncoder(), decoder = new TextDecoder();
const $ = id => document.getElementById(id);
const els = {
 tabs: document.querySelectorAll('.tab-btn'), panels: document.querySelectorAll('.panel'),
 plainText:$('plainText'), textPassword:$('textPassword'), textPassword2:$('textPassword2'), textToggleBtn:$('textToggleBtn'), textGenerateBtn:$('textGenerateBtn'), textFilename:$('textFilename'), encryptTextBtn:$('encryptTextBtn'), clearTextBtn:$('clearTextBtn'), textStatus:$('textStatus'), textMeta:$('textMeta'), textStrengthBar:$('textStrengthBar'), textStrengthText:$('textStrengthText'),
 dropzone:$('dropzone'), fileInput:$('fileInput'), fileInfo:$('fileInfo'), filePassword:$('filePassword'), filePassword2:$('filePassword2'), fileToggleBtn:$('fileToggleBtn'), fileGenerateBtn:$('fileGenerateBtn'), encryptFileBtn:$('encryptFileBtn'), clearFileBtn:$('clearFileBtn'), fileStatus:$('fileStatus'), fileMeta:$('fileMeta'), fileStrengthBar:$('fileStrengthBar'), fileStrengthText:$('fileStrengthText'),
 encryptedFile:$('encryptedFile'), decryptPassword:$('decryptPassword'), decryptBtn:$('decryptBtn'), clearDecryptBtn:$('clearDecryptBtn'), decryptStatus:$('decryptStatus'), decryptMeta:$('decryptMeta'), decryptedText:$('decryptedText'), copyPlainBtn:$('copyPlainBtn'), downloadPlainBtn:$('downloadPlainBtn'), textDecryptArea:$('textDecryptArea'), textDecryptActions:$('textDecryptActions'), fileDecryptArea:$('fileDecryptArea'), browserStatus:$('browserStatus')
};
let selectedFile = null, currentDecryptedText = '';
// True only while the clipboard holds a password this app generated, so Clear
// can wipe it without touching anything the user copied from elsewhere.
let generatedPasswordInClipboard = false;
function setStatus(el,msg,type=''){el.textContent=msg;el.className='status'+(type?' '+type:'')}
function showMeta(el,html){el.innerHTML=html;el.classList.remove('hidden')}
function hide(el){el.classList.add('hidden')}
function uint8ToBase64(bytes){let binary='';const chunk=0x8000;for(let i=0;i<bytes.length;i+=chunk)binary+=String.fromCharCode(...bytes.subarray(i,i+chunk));return btoa(binary)}
function base64ToUint8(base64){const binary=atob(base64);const bytes=new Uint8Array(binary.length);for(let i=0;i<binary.length;i++)bytes[i]=binary.charCodeAt(i);return bytes}
function sanitizeFilename(name){const cleaned=(name||'private-note.pvault').trim().replace(/[\\/:*?"<>|]+/g,'_');return cleaned||'private-note.pvault'}
function ensurePvaultExtension(name){return /\.pvault$/i.test(name)?name:name+'.pvault'}
function formatBytes(n){if(n<1024)return n+' B';if(n<1024*1024)return (n/1024).toFixed(1)+' KB';return (n/(1024*1024)).toFixed(2)+' MB'}
async function deriveKey(password,salt,usages){const material=await crypto.subtle.importKey('raw',encoder.encode(password),'PBKDF2',false,['deriveKey']);return crypto.subtle.deriveKey({name:'PBKDF2',salt,iterations:PBKDF2_ITERATIONS,hash:'SHA-256'},material,{name:'AES-GCM',length:256},false,usages)}
async function encryptBytes(bytes,password,kind,extra={}){const salt=crypto.getRandomValues(new Uint8Array(SALT_LENGTH)),iv=crypto.getRandomValues(new Uint8Array(IV_LENGTH));const key=await deriveKey(password,salt,['encrypt']);const encrypted=await crypto.subtle.encrypt({name:'AES-GCM',iv},key,bytes);return {version:APP_VERSION,app:'Encryption 1',format:kind==='file'?'E1A-FILE-1':'E1-TEXT-1',cipher:'AES-GCM',kdf:'PBKDF2',hash:'SHA-256',iterations:PBKDF2_ITERATIONS,salt:uint8ToBase64(salt),iv:uint8ToBase64(iv),data:uint8ToBase64(new Uint8Array(encrypted)),createdAt:new Date().toISOString(),...extra}}
async function decryptBytes(payload,password){validatePayload(payload);const salt=base64ToUint8(payload.salt),iv=base64ToUint8(payload.iv),data=base64ToUint8(payload.data);const key=await deriveKey(password,salt,['decrypt']);return new Uint8Array(await crypto.subtle.decrypt({name:'AES-GCM',iv},key,data))}
function validatePayload(p){if(!p||typeof p!=='object')throw Error('暗号化ファイルの形式が無効です。');for(const k of ['version','cipher','kdf','salt','iv','data'])if(!(k in p))throw Error('暗号化ファイルのフィールドが見つかりません: '+k);if(p.cipher!=='AES-GCM'||p.kdf!=='PBKDF2')throw Error('サポートされていない暗号化形式です。');if(p.iterations!==PBKDF2_ITERATIONS)throw Error('サポートされていない PBKDF2 の反復回数です。');}
function evaluatePasswordStrength(password){const c={length8:password.length>=8,uppercase:/[A-Z]/.test(password),special:/[^A-Za-z0-9]/.test(password),lowercase:/[a-z]/.test(password),number:/\d/.test(password),length12:password.length>=12,length16:password.length>=16};let score=0;if(c.length8)score+=30;if(c.uppercase)score+=20;if(c.special)score+=20;if(c.lowercase)score+=10;if(c.number)score+=10;if(c.length12)score+=5;if(c.length16)score+=5;let label='非常に弱い',color='#b42318';if(!password){label='未入力';color='#d0d7e2';score=0}else if(score>=90){label='非常に強い';color='#157347'}else if(score>=70){label='強い';color='#2d8a57'}else if(score>=50){label='普通';color='#b7791f'}else if(score>=30){label='弱い';color='#c05621'}return{score,label,color,checks:c,meetsMinimum:c.length8&&c.uppercase&&c.special}}
function updateStrength(input,bar,text){const r=evaluatePasswordStrength(input.value);bar.style.width=r.score+'%';bar.style.background=r.color;const missing=[];if(!r.checks.length8)missing.push('8文字以上');if(!r.checks.uppercase)missing.push('大文字を1つ');if(!r.checks.special)missing.push('特殊文字を1つ');text.textContent=!input.value?'パスワードの強度: 未入力':r.meetsMinimum?'パスワードの強度: '+r.label+'。最低要件を満たしています。':'パスワードの強度: '+r.label+'。あと必要: '+missing.join('、')+'。';return r}
function downloadBlob(filename,blob){const url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=filename;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1000)}
function downloadPayload(filename,payload){downloadBlob(filename,new Blob([JSON.stringify(payload,null,2)],{type:'application/json'}))}
function checkSecure(){return !!window.crypto?.subtle}
function setBrowserCompatibilityStatus(){const secure=!!window.isSecureContext,cryptoSupported=!!window.crypto?.subtle,supported=secure&&cryptoSupported;if(!supported){setStatus(els.browserStatus,'このブラウザは古すぎるか、暗号化に必要なセキュアコンテキストで実行されていません。ブラウザを更新するか、より新しいブラウザでこのページを開いてください。','error');[els.encryptTextBtn,els.encryptFileBtn,els.decryptBtn].forEach(b=>b.disabled=true)}else{setStatus(els.browserStatus,'ブラウザの互換性: 合格','success')}}
function switchTab(name){els.tabs.forEach(b=>b.classList.toggle('active',b.dataset.tab===name));els.panels.forEach(p=>p.classList.toggle('active',p.id===name))}
function validPassword(p1,p2){if(!p1)return 'パスワードを入力してください。';if(p1!==p2)return 'パスワードが一致しません。';if(!evaluatePasswordStrength(p1).meetsMinimum)return 'パスワードの要件: 8文字以上で、大文字を1つ以上、特殊文字を1つ以上含めてください。';return ''}
function generateStrongPassword(length=24){const alphabet='ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%^&*-_=+';const random=new Uint32Array(length);crypto.getRandomValues(random);let result='';for(let i=0;i<random.length;i++)result+=alphabet[random[i]%alphabet.length];return result}
function setFieldVisibility(input,toggleBtn,visible){input.type=visible?'text':'password';toggleBtn.textContent=visible?'非表示':'表示';toggleBtn.setAttribute('aria-pressed',String(visible));toggleBtn.setAttribute('aria-label',visible?'パスワードを非表示':'パスワードを表示')}
async function generateInto(input,input2,toggleBtn,statusEl,strengthFn){const password=generateStrongPassword();input.value=password;input2.value=password;setFieldVisibility(input,toggleBtn,true);strengthFn();input.focus();input.select();if(!navigator.clipboard?.writeText){setStatus(statusEl,"パスワードが生成され、上に表示されました。このブラウザは自動クリップボードコピーに対応していないため、暗号化する前に手動でコピーしてください。",'error');return}try{await navigator.clipboard.writeText(password);generatedPasswordInClipboard=true;setStatus(statusEl,'パスワードが生成され、クリップボードにコピーされました。後で復元できないため、今すぐ安全な場所に保存してください。「クリア」を押すとクリップボードからも削除されます。','success')}catch{setStatus(statusEl,'パスワードが生成され、上に表示されました。クリップボードへの自動コピーに失敗しました。暗号化する前に手動でコピーしてください。','error')}}
function clearGeneratedClipboard(){if(generatedPasswordInClipboard&&navigator.clipboard?.writeText){navigator.clipboard.writeText('').catch(()=>{});generatedPasswordInClipboard=false}}

els.tabs.forEach(b=>b.addEventListener('click',()=>switchTab(b.dataset.tab)));
els.textPassword.addEventListener('input',()=>updateStrength(els.textPassword,els.textStrengthBar,els.textStrengthText));
els.filePassword.addEventListener('input',()=>updateStrength(els.filePassword,els.fileStrengthBar,els.fileStrengthText));
els.textToggleBtn.addEventListener('click',()=>{setFieldVisibility(els.textPassword,els.textToggleBtn,els.textPassword.type==='password');els.textPassword.focus()});
els.fileToggleBtn.addEventListener('click',()=>{setFieldVisibility(els.filePassword,els.fileToggleBtn,els.filePassword.type==='password');els.filePassword.focus()});
els.textGenerateBtn.addEventListener('click',()=>generateInto(els.textPassword,els.textPassword2,els.textToggleBtn,els.textStatus,()=>updateStrength(els.textPassword,els.textStrengthBar,els.textStrengthText)));
els.fileGenerateBtn.addEventListener('click',()=>generateInto(els.filePassword,els.filePassword2,els.fileToggleBtn,els.fileStatus,()=>updateStrength(els.filePassword,els.fileStrengthBar,els.fileStrengthText)));

els.encryptTextBtn.addEventListener('click',async()=>{hide(els.textMeta);setStatus(els.textStatus,'');if(!checkSecure()){setStatus(els.textStatus,'このブラウザコンテキストは必要な Web Crypto API を提供していません。','error');return}const text=els.plainText.value,p=els.textPassword.value,p2=els.textPassword2.value,err=validPassword(p,p2);if(!text.trim())return setStatus(els.textStatus,'暗号化するテキストを入力してください。','error');if(err)return setStatus(els.textStatus,err,'error');try{els.encryptTextBtn.disabled=true;setStatus(els.textStatus,'ローカルで暗号化中...','success');const payload=await encryptBytes(encoder.encode(text),p,'text');downloadPayload(ensurePvaultExtension(sanitizeFilename(els.textFilename.value)),payload);showMeta(els.textMeta,'<strong>暗号化が完了しました。</strong><br>テキストはローカルで暗号化され、暗号化されたファイルがダウンロードされました。');setStatus(els.textStatus,'暗号化が完了しました。','success')}catch(e){setStatus(els.textStatus,'暗号化に失敗しました: '+(e.message||e),'error')}finally{els.encryptTextBtn.disabled=false}});

function selectFile(file){if(!file)return;if(file.size>MAX_FILE_SIZE){selectedFile=null;hide(els.fileInfo);setStatus(els.fileStatus,'そのファイルは '+formatBytes(file.size)+' です。Encryption 1 は最大 10 MB までのファイルに対応しています。','error');return}selectedFile=file;els.fileInfo.innerHTML='<strong>'+escapeHtml(file.name)+'</strong><br>'+formatBytes(file.size)+(file.type?' • '+escapeHtml(file.type):'');els.fileInfo.classList.remove('hidden');setStatus(els.fileStatus,'ファイルは暗号化の準備ができています。','success')}
function escapeHtml(s){return String(s).replace(/[&<>'"]/g,c=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c]))}
els.fileInput.addEventListener('change',()=>selectFile(els.fileInput.files[0]));els.dropzone.addEventListener('click',()=>els.fileInput.click());els.dropzone.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' ')els.fileInput.click()});['dragenter','dragover'].forEach(ev=>els.dropzone.addEventListener(ev,e=>{e.preventDefault();els.dropzone.classList.add('dragover')}));['dragleave','drop'].forEach(ev=>els.dropzone.addEventListener(ev,e=>{e.preventDefault();els.dropzone.classList.remove('dragover')}));els.dropzone.addEventListener('drop',e=>selectFile(e.dataTransfer.files[0]));

els.encryptFileBtn.addEventListener('click',async()=>{hide(els.fileMeta);if(!checkSecure()){setStatus(els.fileStatus,'このブラウザコンテキストは必要な Web Crypto API を提供していません。','error');return}if(!selectedFile)return setStatus(els.fileStatus,'最初にファイルを選択してください。','error');const p=els.filePassword.value,p2=els.filePassword2.value,err=validPassword(p,p2);if(err)return setStatus(els.fileStatus,err,'error');try{els.encryptFileBtn.disabled=true;setStatus(els.fileStatus,'ローカルで読み込み・暗号化中...','success');const bytes=new Uint8Array(await selectedFile.arrayBuffer());const container=encoder.encode(JSON.stringify({filename:selectedFile.name,mimeType:selectedFile.type||'application/octet-stream',size:selectedFile.size,data:uint8ToBase64(bytes)}));const payload=await encryptBytes(container,p,'file');downloadPayload(sanitizeFilename(selectedFile.name)+'.pvault',payload);showMeta(els.fileMeta,'<strong>暗号化が完了しました。</strong><br>元のファイル: <span class="mono">'+escapeHtml(selectedFile.name)+'</span><br>サイズ: '+formatBytes(selectedFile.size)+'<br>暗号化されたパッケージがローカルにダウンロードされました。');setStatus(els.fileStatus,'ファイルの暗号化に成功しました。','success')}catch(e){setStatus(els.fileStatus,'暗号化に失敗しました: '+(e.message||e),'error')}finally{els.encryptFileBtn.disabled=false}});

els.decryptBtn.addEventListener('click',async()=>{hide(els.decryptMeta);hide(els.fileDecryptArea);els.decryptedText.value='';els.textDecryptArea.classList.remove('hidden');els.textDecryptActions.classList.remove('hidden');els.copyPlainBtn.disabled=true;els.downloadPlainBtn.disabled=true;currentDecryptedText='';if(!checkSecure()){setStatus(els.decryptStatus,'このブラウザコンテキストは必要な Web Crypto API を提供していません。','error');return}const file=els.encryptedFile.files[0],p=els.decryptPassword.value;if(!file)return setStatus(els.decryptStatus,'暗号化された .pvault ファイルを選択してください。','error');if(!p)return setStatus(els.decryptStatus,'パスワードを入力してください。','error');try{els.decryptBtn.disabled=true;setStatus(els.decryptStatus,'ローカルで復号中...','success');const payload=JSON.parse(await file.text()),bytes=await decryptBytes(payload,p);if(payload.format==='E1A-FILE-1'){const container=JSON.parse(decoder.decode(bytes));if(!container.filename||!container.data)throw Error('暗号化されたファイルパッケージが無効です。');const original=base64ToUint8(container.data);const blob=new Blob([original],{type:container.mimeType||'application/octet-stream'});downloadBlob(sanitizeFilename(container.filename),blob);els.textDecryptArea.classList.add('hidden');els.textDecryptActions.classList.add('hidden');els.fileDecryptArea.innerHTML='<strong>ファイルの復号に成功しました。</strong><br>復元されたファイル: <span class="mono">'+escapeHtml(container.filename)+'</span><br>サイズ: '+formatBytes(original.byteLength)+'<br><button id="downloadRestored" class="btn-primary" style="margin-top:12px">復元したファイルを再度ダウンロード</button>';els.fileDecryptArea.classList.remove('hidden');$('downloadRestored').addEventListener('click',()=>downloadBlob(sanitizeFilename(container.filename),new Blob([original],{type:container.mimeType||'application/octet-stream'})));showMeta(els.decryptMeta,'<strong>ファイルパッケージが復号されました。</strong><br>元のファイルはローカルで復元され、ダウンロードされました。');setStatus(els.decryptStatus,'復号に成功しました。','success')}else{const text=decoder.decode(bytes);currentDecryptedText=text;els.decryptedText.value=text;els.copyPlainBtn.disabled=false;els.downloadPlainBtn.disabled=false;showMeta(els.decryptMeta,'<strong>テキストの復号に成功しました。</strong><br>これは Encryption 1 の旧形式のテキストパッケージ、または Advanced のテキストパッケージである可能性があります。');setStatus(els.decryptStatus,'復号に成功しました。','success')}}catch(e){setStatus(els.decryptStatus,'復号に失敗しました。パスワードが間違っているか、ファイルが破損しているか、ファイル形式がサポートされていない可能性があります。','error')}finally{els.decryptBtn.disabled=false}});
els.copyPlainBtn.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(currentDecryptedText);generatedPasswordInClipboard=false;setStatus(els.decryptStatus,'復号されたテキストがクリップボードにコピーされました。','success')}catch{setStatus(els.decryptStatus,'このブラウザではテキストをクリップボードにコピーできませんでした。','error')}});
els.downloadPlainBtn.addEventListener('click',()=>{if(currentDecryptedText)downloadBlob('decrypted-text.txt',new Blob([currentDecryptedText],{type:'text/plain;charset=utf-8'}))});
function clearText(){els.plainText.value='';els.textPassword.value='';els.textPassword2.value='';els.textFilename.value='private-note.pvault';setFieldVisibility(els.textPassword,els.textToggleBtn,false);updateStrength(els.textPassword,els.textStrengthBar,els.textStrengthText);hide(els.textMeta);setStatus(els.textStatus,'');clearGeneratedClipboard()}
function clearFile(){selectedFile=null;els.fileInput.value='';els.filePassword.value='';els.filePassword2.value='';hide(els.fileInfo);hide(els.fileMeta);setFieldVisibility(els.filePassword,els.fileToggleBtn,false);updateStrength(els.filePassword,els.fileStrengthBar,els.fileStrengthText);setStatus(els.fileStatus,'');clearGeneratedClipboard()}
function clearDecrypt(){els.encryptedFile.value='';els.decryptPassword.value='';els.decryptedText.value='';currentDecryptedText='';els.copyPlainBtn.disabled=true;els.downloadPlainBtn.disabled=true;hide(els.decryptMeta);hide(els.fileDecryptArea);els.textDecryptArea.classList.remove('hidden');els.textDecryptActions.classList.remove('hidden');setStatus(els.decryptStatus,'');clearGeneratedClipboard()}
els.clearTextBtn.addEventListener('click',clearText);els.clearFileBtn.addEventListener('click',clearFile);els.clearDecryptBtn.addEventListener('click',clearDecrypt);
updateStrength(els.textPassword,els.textStrengthBar,els.textStrengthText);updateStrength(els.filePassword,els.fileStrengthBar,els.fileStrengthText);
setBrowserCompatibilityStatus();
if(!checkSecure()){setStatus(els.textStatus,'このブラウザコンテキストでは Web Crypto を使用できません。','error');setStatus(els.fileStatus,'このブラウザコンテキストでは Web Crypto を使用できません。','error');setStatus(els.decryptStatus,'このブラウザコンテキストでは Web Crypto を使用できません。','error');[els.encryptTextBtn,els.encryptFileBtn,els.decryptBtn].forEach(b=>b.disabled=true)}
</script>



<script src='https://storage.ko-fi.com/cdn/scripts/overlay-widget.js'></script>
<script>
  kofiWidgetOverlay.draw('youngslanguageconsulting', {
    'type': 'floating-chat',
    'floating-chat.donateButton.text': 'EC-1を支援',
    'floating-chat.donateButton.background-color': '#d5eef6',
    'floating-chat.donateButton.text-color': '#323842'
  });
</script>

</body>
</html>