【问题标题】:How to map Windows API CredWrite/CredRead in JNA?如何在 JNA 中映射 Windows API CredWrite/CredRead?
【发布时间】:2016-07-15 20:23:13
【问题描述】:

我正在尝试在 JNA 中映射 CredWrite/CredRead,以便将在我的 Java 应用程序中使用的第三方凭据存储在 Windows 凭据管理器 (OS Windows 10) 中。

这是 C 中的原始签名:

// https://msdn.microsoft.com/en-us/library/aa375187(v=vs.85).aspx
BOOL CredWrite(
  _In_ PCREDENTIAL Credential,
  _In_ DWORD       Flags
);

// https://msdn.microsoft.com/en-us/library/aa374804(v=vs.85).aspx
BOOL CredRead(
  _In_  LPCTSTR     TargetName,
  _In_  DWORD       Type,
  _In_  DWORD       Flags,
  _Out_ PCREDENTIAL *Credential
);

typedef struct _CREDENTIAL {
  DWORD                 Flags;
  DWORD                 Type;
  LPTSTR                TargetName;
  LPTSTR                Comment;
  FILETIME              LastWritten;
  DWORD                 CredentialBlobSize;
  LPBYTE                CredentialBlob;
  DWORD                 Persist;
  DWORD                 AttributeCount;
  PCREDENTIAL_ATTRIBUTE Attributes;
  LPTSTR                TargetAlias;
  LPTSTR                UserName;
} CREDENTIAL, *PCREDENTIAL;

typedef struct _CREDENTIAL_ATTRIBUTE {
  LPTSTR Keyword;
  DWORD  Flags;
  DWORD  ValueSize;
  LPBYTE Value;
} CREDENTIAL_ATTRIBUTE, *PCREDENTIAL_ATTRIBUTE;

这是我的 Java 地图:

WinCrypt instance = (WinCrypt) Native.loadLibrary("Advapi32", WinCrypt.class, W32APIOptions.DEFAULT_OPTIONS);

public boolean CredWrite(
        CREDENTIAL.ByReference Credential,
        int Flags
        );

public boolean CredRead(
        String TargetName,
        int Type,
        int Flags,
        PointerByReference Credential
        );

public static class CREDENTIAL extends Structure {
    public int Flags;
    public int Type;
    public String TargetName;
    public String Comment;
    public FILETIME LastWritten;
    public int CredentialBlobSize;
    public byte[] CredentialBlob = new byte[128];
    public int Persist;
    public int AttributeCount;
    public CREDENTIAL_ATTRIBUTE.ByReference Attributes;
    public String TargetAlias;
    public String UserName;

    public static class ByReference extends CREDENTIAL implements Structure.ByReference {
        public ByReference() {
        }

        public ByReference(Pointer memory) {
            super(memory);                      // LINE 55
        }
    }

    public CREDENTIAL() {
        super();
    }

    public CREDENTIAL(Pointer memory) {
        super(memory); 
        read();                                 // LINE 65
    }

    @Override
    protected List<String> getFieldOrder() {
        return Arrays.asList(new String[] {
                "Flags",
                "Type",
                "TargetName",
                "Comment",
                "LastWritten",
                "CredentialBlobSize",
                "CredentialBlob",
                "Persist",
                "AttributeCount",
                "Attributes",
                "TargetAlias",
                "UserName"
        });
    }
}

public static class CREDENTIAL_ATTRIBUTE extends Structure {
    public String Keyword;
    public int Flags;
    public int ValueSize;
    public byte[] Value = new byte[128];

    public static class ByReference extends CREDENTIAL_ATTRIBUTE implements Structure.ByReference {
    }

    @Override
    protected List<String> getFieldOrder() {
        return Arrays.asList(new String[] {
                "Keyword",
                "Flags",
                "ValueSize",
                "Value"
        });
    }
}

首先我尝试向 Windows 凭据管理器写入凭据:

String password = "passwordtest";
int cbCreds = 1 + password.length();

CREDENTIAL.ByReference credRef = new CREDENTIAL.ByReference();
credRef.Type = WinCrypt.CRED_TYPE_GENERIC;
credRef.TargetName = "TEST/account";
credRef.CredentialBlobSize = cbCreds;
credRef.CredentialBlob = password.getBytes();
credRef.Persist = WinCrypt.CRED_PERSIST_LOCAL_MACHINE;
credRef.UserName = "administrator";

boolean ok = WinCrypt.instance.CredWrite(credRef, 0);
int rc = Kernel32.INSTANCE.GetLastError();
String errMsg = Kernel32Util.formatMessage(rc);
System.out.println("CredWrite() - ok: " + ok + ", errno: " + rc + ", errmsg: " + errMsg);

尝试写入的输出:

CredWrite() - ok: false, errno: 87, errmsg: The parameter is incorrect.

然后我尝试从 Windows 凭据管理器中读取现有凭据:

PointerByReference pref = new PointerByReference();
boolean ok = WinCrypt.instance.CredRead("build-apps", WinCrypt.CRED_TYPE_DOMAIN_PASSWORD, 0, pref);
int rc = Kernel32.INSTANCE.GetLastError();
String errMsg = Kernel32Util.formatMessage(rc);
System.out.println("CredRead() - ok: " + ok + ", errno: " + rc + ", errmsg: " + errMsg);
CREDENTIAL cred = new CREDENTIAL.ByReference(pref.getPointer());        // LINE 44

尝试读取的输出:

CredRead() - ok: true, errno: 0, errmsg: The operation completed successfully.
Exception in thread "main" java.lang.IllegalArgumentException: Structure exceeds provided memory bounds
    at com.sun.jna.Structure.ensureAllocated(Structure.java:366)
    at com.sun.jna.Structure.ensureAllocated(Structure.java:346)
    at com.sun.jna.Structure.read(Structure.java:552)
    at com.abc.crypt.WinCrypt$CREDENTIAL.<init>(WinCrypt.java:65)
    at com.abc.crypt.WinCrypt$CREDENTIAL$ByReference.<init>(WinCrypt.java:55) 
    at com.abc.crypt.CryptTest.main(CryptTest.java:44)
Caused by: java.lang.IndexOutOfBoundsException: Bounds exceeds available space : size=8, offset=200
    at com.sun.jna.Memory.boundsCheck(Memory.java:203)
    at com.sun.jna.Memory$SharedMemory.boundsCheck(Memory.java:87)
    at com.sun.jna.Memory.share(Memory.java:131)
    at com.sun.jna.Structure.ensureAllocated(Structure.java:363)
    ... 5 more

所以尝试写入失败,尝试读取成功但未能根据输出创建 CREDENTIAL 对象。

根据CredWrite API的网页,我在写测试中得到的errno 87是以下错误:

ERROR_INVALID_PARAMETER

无法更改现有凭据中的某些字段。如果字段与受保护的值不匹配,则返回此错误 现有凭据的字段。

但是,我在 CREDENTIAL 实例中输入的值是新的凭据,而不是 Windows 凭据管理器中的现有凭据。

感谢任何有关如何修复/改进的建议或想法。

====================================

应用修复后更新:

新的CredRead:

public boolean CredRead(
        String TargetName,
        int Type,
        int Flags,
        CREDENTIAL.ByReference Credential
        );

CredRead 测试:

CREDENTIAL.ByReference pref = new CREDENTIAL.ByReference();
boolean ok = WinCrypt.instance.CredRead("TEST/account", WinCrypt.CRED_TYPE_GENERIC, 0, pref);
int rc = Kernel32.INSTANCE.GetLastError();
String errMsg = Kernel32Util.formatMessage(rc);
System.out.println("CredRead() - ok: " + ok + ", errno: " + rc + ", errmsg: " + errMsg);
System.out.println(String.format("Read username = '%s', password='%S' (%d bytes)\n",
        pref.UserName, pref.CredentialBlob, pref.CredentialBlobSize));

结果:

CredRead() - ok: true, errno: 0, errmsg: The operation completed successfully.
Read username = 'null', password='NULL' (0 bytes)

我检查了 contrib 中的 JNA 样本如何在 out arg 上使用 ByReference,它们通过新的 ByReference 并传递给函数以相同的方式进行操作。

【问题讨论】:

    标签: java winapi jna credentials


    【解决方案1】:

    如果你查看CredRead() 的 WIN32 定义,第四个参数是 PCREDENTIAL* 类型,即它是一个指向指针的指针。所以...

    • 您需要传入一个指针的地址,即一个 4 字节的内存块。
    • Windows 分配一块内存来保存 CREDENTIAL 结构,然后通过将新内存块的地址放在您传入的 4 字节块中来告诉您它在哪里。
    • 当您取消引用您的原始指针(您传递给CredRead() 的那个)时,您会得到另一个指针(4 字节块),它本身需要被取消引用才能到达 CREDENTIAL。

    欢迎来到 C :-)

    TL;DR:CREDENTIAL 类需要这样定义:

    public static class CREDENTIAL extends Structure {
        public int Flags;
        public int Type;
        public WString TargetName;
        public WString Comment;
        public FILETIME LastWritten;
        public int CredentialBlobSize;
        public Pointer CredentialBlob; // <== discussed below
        public int Persist;
        public int AttributeCount;
        public Pointer Attributes;
        public WString TargetAlias;
        public WString UserName;
        private Pointer RawMemBlock; // <== discussed below
    
        public CREDENTIAL() { }
    
        public CREDENTIAL( Pointer ptr ) 
        { 
            // initialize ourself from the raw memory block returned to us by ADVAPI32
            super( ptr ) ; 
            RawMemBlock = ptr ; 
            read() ;
        }
    
        @Override
        protected void finalize()
        {    
            // clean up
            WinCrypt.INSTANCE.CredFree( RawMemBlock ) ;
        }
    
        @Override
        protected List<String> getFieldOrder()
        {
            return Arrays.asList( new String[] { "Flags" , "Type" , "TargetName" , "Comment" , "LastWritten" , "CredentialBlobSize" , "CredentialBlob" , "Persist" , "AttributeCount" , "Attributes" , "TargetAlias" , "UserName" } ) ;
        }
    } ;
    

    要调用CredRead(),声明如下:

    public boolean CredRead( String target , int type , int flags , PointerByReference cred ) ;
    

    并像这样调用它:

    PointerByReference pptr = new PointerByReference() ;
    boolean rc = WinCrypt.INSTANCE.CredRead( target , credType , 0 , pptr ) ;
    if ( ! rc )
        ... ; // handle the error
    CREDENTIAL cred = new CREDENTIAL( pptr.getValue() ) ;
    String userName = cred.UserName.toString() ;
    String password = new String( cred.CredentialBlob.getByteArray(0,cred.CredentialBlobSize) , "UTF-16LE" ) ;
    

    凭据 blob 是 Windows 分配的另一块内存,因此您不需要自己分配它,Windows 会这样做,并会通过将其地址放在 CredentialBlob 字段中来告诉您它在哪里。

    由于 Windows 已为您分配了这些内存块,并且由于它无法知道您何时会完成它们,因此您有责任释放它们。因此,CREDENTIAL 构造函数保留给它的原始指针 CredRead() 的副本,并在终结器中调用 CredFree() 以释放该内存。 CredFree() 声明如下:

    public void CredFree( Pointer cred ) ;
    

    要保存凭据,您需要以 CredWrite() 所期望的方式准备凭据 blob,即将指向它的指针存储在 CREDENTIAL.CredentialBlob 字段中:

    // prepare the credential blob
    byte[] credBlob = password.getBytes( "UTF-16LE" ) ;
    Memory credBlobMem = new Memory( credBlob.length ) ;
    credBlobMem.write( 0 , credBlob , 0 , credBlob.length ) ;
    
    // create the credential
    CREDENTIAL cred = new CREDENTIAL() ;
    cred.Type = CRED_TYPE_GENERIC ;
    cred.TargetName = new WString( target ) ;
    cred.CredentialBlobSize = (int) credBlobMem.size() ;
    cred.CredentialBlob = credBlobMem ;
    cred.Persist = CRED_PERSIST_LOCAL_MACHINE ;
    cred.UserName = new WString( userName ) ;
    
    // save the credential
    boolean rc = WinCrypt.INSTANCE.CredWrite( cred , 0 ) ;
    if ( ! rc )
        ... ; // handle the error
    

    作为附录,如果在服务帐户或任何其他没有永久配置文件的帐户下运行,所有这些都会遇到问题。我需要为通过任务计划程序运行的作业执行此操作,使用没有交互式登录权限的服务帐户,会发生什么:

    • 我创建了一个设置密码的批处理文件,并通过任务计划程序运行它(以便它在服务帐户下运行,并且密码进入正确的存储区)
    • Windows 会创建一个临时配置文件(检查事件日志)并将密码输入其中。
    • 另一个转储密码的批处理文件显示密码设置成功。
    • 运行主作业有效,因为临时配置文件仍然存在,但 5 或 10 分钟后,Windows 将其删除,包括您设置的密码 :-/,所以下次运行主作业时,它失败,因为密码不再存在。

    解决方案是创建一个永久配置文件,最好是通过交互方式登录,只需要完成一次。如果您不能这样做,可以通过programmatically 进行操作,尽管您需要管理员权限。

    【讨论】:

      【解决方案2】:

      CredRead.PCREDENTIAL 应该是CREDENTIAL.ByReference。使用 PointerByReference 最终会传入一个指向 NULL 值的指针,而不是预期的指向 CREDENTIAL 结构的指针。

      CREDENTAL.CredentialBlob 必须是 PointerPointerType(如果您自己初始化块,则可能是 Memory)。使用内联字节数组将整个结构移动数组大小,其中被调用者期望指向内存块的指针。

      更新

      我想我误读了CredRead()的声明。

      CredRead 应该继续使用PointerByReference。使用PointerByReference.getValue()CredRead() 中提取“返回”的指针值,以便基于指针创建一个新的CREDENTIALS 实例。 PointerByReference.getPointer() 为您提供为保存指针值而分配的内存地址。

      public boolean CredWrite(
          CREDENTIAL Credential,
          int Flags
          );
      
      public boolean CredRead(
          String TargetName,
          int Type,
          int Flags,
          PointerByReference pref
          );
      
      PointerByReference pref = new PointerByReference()
      CredRead(name, type, flags, pref);
      creds = new Credentials(pref.getValue())
      

      【讨论】:

      • 第二个建议修复了 CredWrite,现在凭证被写入 CredManager。感谢那!但是,在 CredRead 中应用第一个建议后,在 CredRead() 返回后,它表示操作成功,但 CREDENTIAL.ByReference obj 仍然为空,并且没有设置 cred 值。新代码在说明中更新。
      【解决方案3】:

      Microsoft 提供了一个 MIT 许可的 Java 库,用于访问 VSTS 令牌。 https://github.com/microsoft/vsts-authentication-library-for-java

      它们在此处提供到 Credential Manager 功能和用法的 JNA 映射: https://github.com/microsoft/vsts-authentication-library-for-java/tree/master/storage/src/main/java/com/microsoft/alm/storage/windows/internal

      如果您从头开始,非常有帮助。

      【讨论】:

        【解决方案4】:

        根据 taka 的回答,但考虑到以下额外考虑,我实施了一个完整的示例。

        考虑了以下其他更正和方面:

        • 在 CredReadW 中,targetName 必须是 WString 类型,而不是 String。使用 CredWriteW 将数据写入 windows vauld 时,也已经使用了 WString。
        • 简化:我没有使用 getFieldOrder() 方法,而是使用了注解样式。
        • 不建议直接读取 Kernel32 GetLastError,因为 JNA 可能会调用其他调用来删除以前的 LastError。正如How to make GetLastError reliably work with JNA? 中所讨论的,我更改为将最后一个错误作为异常捕获。
        • 如 taka 所示,无需在 credentialBlobSize 中添加“1+” - 但更重要的是使用实际内存或 byte[] 大小,因为由于 UTF-8,字符串长度包含更少的字符编码,而不是用于 Windows API 函数的 UTF-16LE 编码生成的内存。

        完整样本: (请注意,它需要 JNA 库;我使用的是 JNA 版本 5.6.0,可在 https://github.com/java-native-access/jna 获得)

        package at.christoph-bimminger.sample;
        
        import java.io.UnsupportedEncodingException;
        import java.util.Arrays;
        import java.util.List;
        
        import com.sun.jna.LastErrorException;
        import com.sun.jna.Library;
        import com.sun.jna.Memory;
        import com.sun.jna.Native;
        import com.sun.jna.Platform;
        import com.sun.jna.Pointer;
        import com.sun.jna.Structure;
        import com.sun.jna.Structure.FieldOrder;
        import com.sun.jna.WString;
        import com.sun.jna.ptr.PointerByReference;
        
        public class Main {
        
        
            public interface WinCrypt extends Library {
        
                WinCrypt INSTANCE = (WinCrypt) Native.load("Advapi32", WinCrypt.class);
        
                boolean CredWriteW(CREDENTIAL.ByReference credentialw, int flags) throws LastErrorException;
        
                boolean CredReadW(WString TargetName, int Type, int Flags, PointerByReference pptr) throws LastErrorException;
        
                public static final class Type {
                    /**
                     * The credential is a generic credential. The credential will not be used by
                     * any particular authentication package. The credential will be stored securely
                     * but has no other significant characteristics.
                     */
                    final static int CRED_TYPE_GENERIC = 1;
        
                    /**
                     * The credential is a password credential and is specific to Microsoft's
                     * authentication packages. The NTLM, Kerberos, and Negotiate authentication
                     * packages will automatically use this credential when connecting to the named
                     * target.
                     */
                    final static int CRED_TYPE_DOMAIN_PASSWORD = 2;
        
                    /**
                     * The credential is a certificate credential and is specific to Microsoft's
                     * authentication packages. The Kerberos, Negotiate, and Schannel authentication
                     * packages automatically use this credential when connecting to the named
                     * target.
                     * 
                     */
                    final static int CRED_TYPE_DOMAIN_CERTIFICATE = 3;
        
                    /**
                     * This value is no longer supported. Windows Server 2003 and Windows XP: The
                     * credential is a password credential and is specific to authentication
                     * packages from Microsoft. The Passport authentication package will
                     * automatically use this credential when connecting to the named target.
                     * 
                     * Additional values will be defined in the future. Applications should be
                     * written to allow for credential types they do not understand.
                     * 
                     */
                    final static int CRED_TYPE_DOMAIN_VISIBLE_PASSWORD = 4;
        
                    /**
                     * The credential is a certificate credential that is a generic authentication
                     * package. Windows Server 2008, Windows Vista, Windows Server 2003 and Windows
                     * XP: This value is not supported.
                     */
                    final static int CRED_TYPE_GENERIC_CERTIFICATE = 5;
        
                    /**
                     * The credential is supported by extended Negotiate packages. Windows Server
                     * 2008, Windows Vista, Windows Server 2003 and Windows XP: This value is not
                     * supported.
                     * 
                     */
                    final static int CRED_TYPE_DOMAIN_EXTENDED = 6;
        
                    /**
                     * The maximum number of supported credential types.Windows Server 2008, Windows
                     * Vista, Windows Server 2003 and Windows XP: This value is not supported.
                     * 
                     */
                    final static int CRED_TYPE_MAXIMUM = 7;
        
                    final static int CRED_TYPE_MAXIMUM_EX = CRED_TYPE_MAXIMUM + 1000;
                }
        
                public static final class Persist {
                    final static int CRED_PERSIST_SESSION = 1;
                    final static int CRED_PERSIST_LOCAL_MACHINE = 2;
                    final static int CRED_PERSIST_ENTERPRISE = 3;
                }
        
            }
        
            /**
             * Representation of native struct FILETIME. See
             * https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-filetime
             * 
             * @author Christoph Bimminger
             *
             */
            @FieldOrder({ "dwLowDateTime", "dwHighDateTime" })
            public static final class FILETIME extends Structure {
                public int dwLowDateTime;
                public int dwHighDateTime;
            }
        
            /**
             * Representation of native struct CREDENTIALW. See
             * https://docs.microsoft.com/en-us/windows/win32/api/wincred/ns-wincred-credentialw
             * 
             * @author Christoph Bimminger
             *
             */
            @FieldOrder({ "flags", "type", "targetName", "comment", "lastWritten", "credentialBlobSize", "credentialBlob",
                    "persist", "attributeCount", "attributes", "targetAlias", "userName" })
            public static class CREDENTIAL extends Structure {
                public int flags;
                public int type;
                public WString targetName;
                public WString comment;
                public FILETIME lastWritten;
                public int credentialBlobSize = 256;
                public Pointer credentialBlob;
                public int persist;
                public int attributeCount;
                public CREDENTIAL_ATTRIBUTE.ByReference attributes;
                public WString targetAlias;
                public WString userName;
        
                public static class ByReference extends CREDENTIAL implements Structure.ByReference {
                    public ByReference() {
                    }
        
                    public ByReference(Pointer memory) {
                        super(memory); // LINE 55
                    }
                }
        
                public CREDENTIAL() {
                    super();
                }
        
                public CREDENTIAL(Pointer memory) {
                    super(memory);
                    read(); // LINE 65
                }
        
            }
        
            public static class CREDENTIAL_ATTRIBUTE extends Structure {
                public String Keyword;
                public int Flags;
                public int ValueSize;
                public byte[] Value = new byte[128];
        
                public static class ByReference extends CREDENTIAL_ATTRIBUTE implements Structure.ByReference {
                }
        
                @Override
                protected List<String> getFieldOrder() {
                    return Arrays.asList(new String[] { "Keyword", "Flags", "ValueSize", "Value" });
                }
            }
        
            public static void main(String[] args) throws UnsupportedEncodingException {
                if (!Platform.isWindows())
                    throw new UnsatisfiedLinkError("This sample requires a windows environment, it uses wincred.h");
        
                { // --- SAVE
                    String password = "brillant";
        
                    // prepare the credential blob
                    byte[] credBlob = password.getBytes("UTF-16LE");
                    Memory credBlobMem = new Memory(credBlob.length);
                    credBlobMem.write(0, credBlob, 0, credBlob.length);
        
                    int cbCreds = credBlob.length;
        
                    CREDENTIAL.ByReference cred = new CREDENTIAL.ByReference();
                    cred.type = WinCrypt.Type.CRED_TYPE_GENERIC;
                    cred.targetName = new WString("FOO/account");
                    cred.credentialBlobSize = cbCreds;
                    cred.credentialBlob = credBlobMem;
                    cred.persist = WinCrypt.Persist.CRED_PERSIST_LOCAL_MACHINE;
                    cred.userName = new WString("paula");
        
                    try {
                        boolean ok = WinCrypt.INSTANCE.CredWriteW(cred, 0);
                    } catch (LastErrorException error) {
                        int rc = error.getErrorCode();
                        String errMsg = error.getMessage();
                        System.out.println(rc + ": " + errMsg);
                        System.exit(1);
        
                    }
                }
        
                ///////////////////// READ PASS
        
                try {
                    PointerByReference pptr = new PointerByReference();
                    boolean ok = WinCrypt.INSTANCE.CredReadW(new WString("FOO/account"), WinCrypt.Type.CRED_TYPE_GENERIC, 0,
                            pptr);
                    CREDENTIAL cred = new CREDENTIAL(pptr.getValue());
        
                    String password = new String(cred.credentialBlob.getByteArray(0, cred.credentialBlobSize), "UTF-16LE");
        
                    System.out.println(password);
                } catch (LastErrorException error) {
                    int rc = error.getErrorCode();
                    String errMsg = error.getMessage();
                    System.out.println(rc + ": " + errMsg);
                    System.exit(1);
        
                }
        
            }
        
        }
        

        【讨论】:

          猜你喜欢
          • 2023-03-18
          • 1970-01-01
          • 1970-01-01
          • 2015-12-08
          • 1970-01-01
          • 2020-11-09
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多