|
| 1 | +"""io_common.py: resolve_recipient_specs / resolve_identity_specs の挙動""" |
| 2 | + |
| 3 | +from __future__ import annotations |
| 4 | + |
| 5 | +import pyrage |
| 6 | +import pytest |
| 7 | + |
| 8 | +from devbase.env import cipher |
| 9 | +from devbase.env import io_common |
| 10 | + |
| 11 | + |
| 12 | +@pytest.fixture |
| 13 | +def fake_home(tmp_path, monkeypatch): |
| 14 | + """``Path.home()`` を ``tmp_path`` に差し替える""" |
| 15 | + from pathlib import Path |
| 16 | + |
| 17 | + monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path)) |
| 18 | + return tmp_path |
| 19 | + |
| 20 | + |
| 21 | +def test_resolve_recipient_specs_returns_first_existing_default(fake_home): |
| 22 | + """recipient は「どの鍵で暗号化するか」を一意に決める必要があるため、 |
| 23 | + 既定鍵が複数存在しても最初に見つかったものだけ返す (ed25519 を優先)。""" |
| 24 | + ssh = fake_home / ".ssh" |
| 25 | + ssh.mkdir() |
| 26 | + (ssh / "id_ed25519.pub").write_text("ssh-ed25519 AAAA dummy\n") |
| 27 | + (ssh / "id_rsa.pub").write_text("ssh-rsa AAAA dummy\n") |
| 28 | + |
| 29 | + specs = io_common.resolve_recipient_specs([]) |
| 30 | + assert len(specs) == 1 |
| 31 | + assert specs[0].endswith("id_ed25519.pub") |
| 32 | + |
| 33 | + |
| 34 | +def test_resolve_recipient_specs_explicit_passthrough(fake_home): |
| 35 | + """明示指定があれば既定鍵探索は行わない (そのまま返す)""" |
| 36 | + ssh = fake_home / ".ssh" |
| 37 | + ssh.mkdir() |
| 38 | + (ssh / "id_ed25519.pub").write_text("ssh-ed25519 AAAA dummy\n") |
| 39 | + |
| 40 | + specs = io_common.resolve_recipient_specs(["age1example"]) |
| 41 | + assert specs == ["age1example"] |
| 42 | + |
| 43 | + |
| 44 | +def test_resolve_recipient_specs_returns_empty_when_no_defaults(fake_home): |
| 45 | + """既定鍵が見つからなければ空 list""" |
| 46 | + assert io_common.resolve_recipient_specs([]) == [] |
| 47 | + |
| 48 | + |
| 49 | +def test_resolve_identity_specs_returns_all_existing_defaults(fake_home): |
| 50 | + """identity は「どの鍵で暗号化されたか」が事前に分からないため、 |
| 51 | + 存在するすべての既定鍵を返す。``pyrage.decrypt`` は複数 identity を |
| 52 | + 受け取れる仕様なので、両方渡しておけばどちらの鍵で暗号化されたバンドル |
| 53 | + でも復号できる (PR #13 gemini 指摘)。""" |
| 54 | + ssh = fake_home / ".ssh" |
| 55 | + ssh.mkdir() |
| 56 | + (ssh / "id_ed25519").write_text("dummy ed25519 key\n") |
| 57 | + (ssh / "id_rsa").write_text("dummy rsa key\n") |
| 58 | + |
| 59 | + specs = io_common.resolve_identity_specs([]) |
| 60 | + assert len(specs) == 2 |
| 61 | + # ed25519 が先に来る (default_identity_paths の順序を維持) |
| 62 | + assert specs[0].endswith("id_ed25519") |
| 63 | + assert specs[1].endswith("id_rsa") |
| 64 | + |
| 65 | + |
| 66 | +def test_resolve_identity_specs_returns_only_existing(fake_home): |
| 67 | + """片方しか存在しなければそれだけ返す""" |
| 68 | + ssh = fake_home / ".ssh" |
| 69 | + ssh.mkdir() |
| 70 | + (ssh / "id_rsa").write_text("dummy\n") |
| 71 | + |
| 72 | + specs = io_common.resolve_identity_specs([]) |
| 73 | + assert len(specs) == 1 |
| 74 | + assert specs[0].endswith("id_rsa") |
| 75 | + |
| 76 | + |
| 77 | +def test_resolve_identity_specs_explicit_passthrough(fake_home): |
| 78 | + """明示指定があれば既定鍵探索は行わない""" |
| 79 | + ssh = fake_home / ".ssh" |
| 80 | + ssh.mkdir() |
| 81 | + (ssh / "id_ed25519").write_text("dummy\n") |
| 82 | + |
| 83 | + specs = io_common.resolve_identity_specs(["/path/to/explicit.key"]) |
| 84 | + assert specs == ["/path/to/explicit.key"] |
| 85 | + |
| 86 | + |
| 87 | +def test_resolve_identity_specs_returns_empty_when_no_defaults(fake_home): |
| 88 | + """既定鍵が一切無ければ空""" |
| 89 | + assert io_common.resolve_identity_specs([]) == [] |
| 90 | + |
| 91 | + |
| 92 | +def test_decrypt_uses_correct_identity_from_multiple_defaults(tmp_path, fake_home): |
| 93 | + """``resolve_identity_specs`` が返した複数 identity を ``cipher.decrypt`` に |
| 94 | + 渡すと、その中から正しい identity が選ばれて復号される。 |
| 95 | +
|
| 96 | + シナリオ: 既定 ssh 鍵が 2 つ (id_ed25519 / id_rsa) 存在する状況を模した上で、 |
| 97 | + `id_rsa` (実体は age 鍵) で暗号化したバンドルを「両方の identity を試す」 |
| 98 | + `cipher.decrypt(identities=[both])` で復号できることを確認する。 |
| 99 | + `id_ed25519` 側は別 age 鍵で、こちらは復号に使われない。 |
| 100 | + """ |
| 101 | + # 異なる 2 つの age 鍵を用意し、ssh 既定パスに配置して |
| 102 | + # resolve_identity_specs から両方が返るようにする |
| 103 | + id1 = pyrage.x25519.Identity.generate() |
| 104 | + id2 = pyrage.x25519.Identity.generate() |
| 105 | + |
| 106 | + ssh = fake_home / ".ssh" |
| 107 | + ssh.mkdir() |
| 108 | + ed_path = ssh / "id_ed25519" |
| 109 | + rsa_path = ssh / "id_rsa" |
| 110 | + ed_path.write_text(str(id1)) # ed25519 スロットに id1 |
| 111 | + rsa_path.write_text(str(id2)) # rsa スロットに id2 (=暗号化に使う鍵) |
| 112 | + |
| 113 | + # id2 の公開鍵だけで暗号化 → id1 では復号できないバンドル |
| 114 | + blob = cipher.encrypt(b"team-secret", recipients=[str(id2.to_public())]) |
| 115 | + |
| 116 | + # resolve_identity_specs は両方返す |
| 117 | + identities = io_common.resolve_identity_specs([]) |
| 118 | + assert len(identities) == 2 |
| 119 | + |
| 120 | + # 両 identity を渡して復号 → pyrage が正しい鍵 (id2) を選んで復号する |
| 121 | + plain = cipher.decrypt(blob, identities=identities) |
| 122 | + assert plain == b"team-secret" |
0 commit comments